I figured this out by reading as much of the gBrowser source code as I could. Here’s gBrowser.addTab()
:
[object XULElement].addTab = function addTab(aURI, aReferrerURI, aCharset, aPostData, aOwner, aAllowThirdPartyFixup) {
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var aReferrerPolicy;
var aFromExternal;
var aRelatedToCurrent;
var aSkipAnimation;
var aAllowMixedContent;
var aForceNotRemote;
var aNoReferrer;
var aUserContextId;
if (arguments.length == 2 &&
typeof arguments[1] == "object" &&
!(arguments[1] instanceof Ci.nsIURI)) {
let params = arguments[1];
aReferrerURI = params.referrerURI;
aReferrerPolicy = params.referrerPolicy;
aCharset = params.charset;
aPostData = params.postData;
aOwner = params.ownerTab;
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
aFromExternal = params.fromExternal;
aRelatedToCurrent = params.relatedToCurrent;
aSkipAnimation = params.skipAnimation;
aAllowMixedContent = params.allowMixedContent;
aForceNotRemote = params.forceNotRemote;
aNoReferrer = params.noReferrer;
aUserContextId = params.userContextId;
}
// if we're adding tabs, we're past interrupt mode, ditch the owner
if (this.mCurrentTab.owner)
this.mCurrentTab.owner = null;
var t = document.createElementNS(NS_XUL, "tab");
var uriIsAboutBlank = !aURI || aURI == "about:blank";
if (aUserContextId)
t.setAttribute("usercontextid", aUserContextId);
t.setAttribute("crop", "end");
t.setAttribute("onerror", "this.removeAttribute('image');");
t.className = "tabbrowser-tab";
// The new browser should be remote if this is an e10s window and
// the uri to load can be loaded remotely.
let remote = gMultiProcessBrowser &&
!aForceNotRemote &&
E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
this.tabContainer._unlockTabSizing();
// When overflowing, new tabs are scrolled into view smoothly, which
// doesn't go well together with the width transition. So we skip the
// transition in that case.
let animate = !aSkipAnimation &&
this.tabContainer.getAttribute("overflow") != "true" &&
Services.prefs.getBoolPref("browser.tabs.animate");
if (!animate) {
t.setAttribute("fadein", "true");
setTimeout(function (tabContainer) {
tabContainer._handleNewTab(t);
}, 0, this.tabContainer);
}
// invalidate caches
this._browsers = null;
this._visibleTabs = null;
this.tabContainer.appendChild(t);
// If this new tab is owned by another, assert that relationship
if (aOwner)
t.owner = aOwner;
let b;
let usingPreloadedContent = false;
// If we open a new tab with the newtab URL in the default
// userContext, check if there is a preloaded browser ready.
// Private windows are not included because both the label and the
// icon for the tab would be set incorrectly (see bug 1195981).
if (aURI == BROWSER_NEW_TAB_URL && !aUserContextId &&
!PrivateBrowsingUtils.isWindowPrivate(window)) {
b = this._getPreloadedBrowser();
usingPreloadedContent = !!b;
}
if (!b) {
// No preloaded browser found, create one.
b = this._createBrowser({remote: remote,
uriIsAboutBlank: uriIsAboutBlank,
userContextId: aUserContextId});
}
let notificationbox = this.getNotificationBox(b);
var position = this.tabs.length - 1;
var uniqueId = this._generateUniquePanelID();
notificationbox.id = uniqueId;
t.linkedPanel = uniqueId;
t.linkedBrowser = b;
this._tabForBrowser.set(b, t);
t._tPos = position;
t.lastAccessed = Date.now();
this.tabContainer._setPositionalAttributes();
// Inject the <browser> into the DOM if necessary.
if (!notificationbox.parentNode) {
// NB: this appendChild call causes us to run constructors for the
// browser element, which fires off a bunch of notifications. Some
// of those notifications can cause code to run that inspects our
// state, so it is important that the tab element is fully
// initialized by this point.
this.mPanelContainer.appendChild(notificationbox);
}
// We've waited until the tab is in the DOM to set the label. This
// allows the TabLabelModified event to be properly dispatched.
if (!aURI || isBlankPageURL(aURI)) {
t.label = this.mStringBundle.getString("tabs.emptyTabTitle");
} else if (aURI.toLowerCase().startsWith("javascript:")) {
// This can go away when bug 672618 or bug 55696 are fixed.
t.label = aURI;
}
this.tabContainer.updateVisibility();
// wire up a progress listener for the new browser object.
var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank, usingPreloadedContent);
const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
.createInstance(Components.interfaces.nsIWebProgress);
filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
this.mTabListeners[position] = tabListener;
this.mTabFilters[position] = filter;
b.droppedLinkHandler = handleDroppedLink;
// Swap in a preloaded customize tab, if available.
if (aURI == "about:customizing") {
usingPreloadedContent = gCustomizationTabPreloader.newTab(t);
}
// Dispatch a new tab notification. We do this once we're
// entirely done, so that things are in a consistent state
// even if the event listener opens or closes tabs.
var evt = document.createEvent("Events");
evt.initEvent("TabOpen", true, false);
t.dispatchEvent(evt);
// If we didn't swap docShells with a preloaded browser
// then let's just continue loading the page normally.
if (!usingPreloadedContent && !uriIsAboutBlank) {
// pretend the user typed this so it'll be available till
// the document successfully loads
if (aURI && gInitialPages.indexOf(aURI) == -1)
b.userTypedValue = aURI;
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (aAllowThirdPartyFixup) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
}
if (aFromExternal)
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
if (aAllowMixedContent)
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
try {
b.loadURIWithFlags(aURI, {
flags: flags,
referrerURI: aNoReferrer ? null: aReferrerURI,
referrerPolicy: aReferrerPolicy,
charset: aCharset,
postData: aPostData,
});
} catch (ex) {
Cu.reportError(ex);
}
}
// We start our browsers out as inactive, and then maintain
// activeness in the tab switcher.
b.docShellIsActive = false;
// When addTab() is called with an URL that is not "about:blank" we
// set the "nodefaultsrc" attribute that prevents a frameLoader
// from being created as soon as the linked <browser> is inserted
// into the DOM. We thus have to register the new outerWindowID
// for non-remote browsers after we have called browser.loadURI().
//
// Note: Only do this of we still have a docShell. The TabOpen
// event was dispatched above and a gBrowser.removeTab() call from
// one of its listeners could cause us to fail here.
if (!remote && b.docShell) {
this._outerWindowIDBrowserMap.set(b.outerWindowID, b);
}
// Check if we're opening a tab related to the current tab and
// move it to after the current tab.
// aReferrerURI is null or undefined if the tab is opened from
// an external application or bookmark, i.e. somewhere other
// than the current tab.
if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) &&
Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
let newTabPos = (this._lastRelatedTab ||
this.selectedTab)._tPos + 1;
if (this._lastRelatedTab)
this._lastRelatedTab.owner = null;
else
t.owner = this.selectedTab;
this.moveTabTo(t, newTabPos);
this._lastRelatedTab = t;
}
if (animate) {
requestAnimationFrame(function () {
this.tabContainer._handleTabTelemetryStart(t, aURI);
// kick the animation off
t.setAttribute("fadein", "true");
}.bind(this));
}
return t;
}
Most of it is boilerplate or leftover cruft from old features. I am a little bit surprised that the one line I keyed in on in the middle, this.tabContainer.appendChild(t);
, worked out. I was just trying to see if I could at least manipulate the UI, and that seemed like a simple place to start. It turned out instead to be exactly what you wanted.