/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
let label = ContextualIdentityService.getUserContextLabel(userContextId);
document.getElementById("userContext-label").textContent = label; // Also set the container label as the tooltip so we can only show the icon // in small windows.
hbox.setAttribute("tooltiptext", label);
let indicator = document.getElementById("userContext-indicator");
replaceContainerClass("icon", indicator, identity.icon);
hbox.hidden = false;
}
async function getTotalMemoryUsage() { const procInfo = await ChromeUtils.requestProcInfo();
let totalMemoryUsage = procInfo.memory; for (const child of procInfo.children) {
totalMemoryUsage += child.memory;
} return totalMemoryUsage;
}
// Used for links dropped on browser content areas. Can be called with a null // event argument when dropping links in the content process. // handleDroppedLink has the following 2 overloads: // handleDroppedLink(tabbrowser, browser, event, url, name, triggeringPrincipal) // handleDroppedLink(tabbrowser, browser, event, links, triggeringPrincipal)
async function handleDroppedLink(
tabbrowser,
browser,
event,
urlOrLinks,
nameOrTriggeringPrincipal,
triggeringPrincipal
) { // If links are dropped in content process, event.preventDefault() should be // called in the content process. Otherwise, we do it here. if (event) { // Keep the event from being handled by the dragDrop listeners // built-in to gecko if they happen to be above us.
event.preventDefault();
}
let links; if (Array.isArray(urlOrLinks)) {
links = urlOrLinks;
triggeringPrincipal = nameOrTriggeringPrincipal;
} else {
links = [{ url: urlOrLinks, nameOrTriggeringPrincipal, type: "" }];
}
let lastLocationChange = browser.lastLocationChange;
let userContextId = browser.getAttribute("usercontextid");
// event is null if links are dropped in content process. // inBackground should be false, as it's loading into current browser.
let inBackground = false; if (event) {
inBackground = Services.prefs.getBoolPref( "browser.tabs.loadInBackground"
); if (event.shiftKey) {
inBackground = !inBackground;
}
}
if (
links.length >=
Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
) { // Sync dialog cannot be used inside drop event handler.
let answer = await tabbrowser.OpenInTabsUtils.promiseConfirmOpenInTabs(
links.length,
window
); if (!answer) { return;
}
}
let urls = [];
let postDatas = []; for (let link of links) {
let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url);
urls.push(data.url);
postDatas.push(data.postData);
} if (lastLocationChange == browser.lastLocationChange) {
tabbrowser.loadTabs(urls, {
inBackground,
replace: true,
allowThirdPartyFixup: false,
postDatas,
userContextId,
targetTab: tabbrowser.getTabForBrowser(browser),
triggeringPrincipal,
});
}
}
window.Tabbrowser = class { static create(window) {
window.gBrowser = new window.Tabbrowser();
window.gBrowser.init();
}
this._defaultDropLinkHandler = function (...args) { // The droppedLinkHandler gets invoked with `this` being the browser // element on which the drop took place.
let browser = this;
let tabbrowser = browser.getTabBrowser();
handleDroppedLink(tabbrowser, browser, ...args);
}; this._setupInitialBrowserAndTab();
// We take over setting the document title, so remove the l10n id to // avoid it being re-translated and overwriting document content if // we ever switch languages at runtime.
document.querySelector("title").removeAttribute("data-l10n-id");
_setupInitialBrowserAndTab() { // See browser.js for the meaning of window.arguments. // Bug 1485961 covers making this more sane.
let userContextId = window.arguments && window.arguments[5];
let openWindowInfo = window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow).initialOpenWindowInfo;
let extraOptions; if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) {
extraOptions = window.arguments[1];
}
// If our opener provided a remoteType which was responsible for creating // this pop-up window, we'll fall back to using that remote type when no // other remote type is available.
let triggeringRemoteType; if (extraOptions?.hasKey("triggeringRemoteType")) {
triggeringRemoteType = extraOptions.getPropertyAsACString( "triggeringRemoteType"
);
}
let tabArgument = gBrowserInit.getTabToAdopt();
// If we have a tab argument with browser, we use its remoteType. Otherwise, // if e10s is disabled or there's a parent process opener (e.g. parent // process about: page) for the content tab, we use a parent // process remoteType. Otherwise, we check the URI to determine // what to do - if there isn't one, we default to the default remote type. // // When adopting a tab, we'll also use that tab's browsingContextGroupId, // if available, to ensure we don't spawn a new process.
let remoteType;
let initialBrowsingContextGroupId;
if (tabArgument && tabArgument.hasAttribute("usercontextid")) { // The window's first argument is a tab if and only if we are swapping tabs. // We must set the browser's usercontextid so that the newly created remote // tab child has the correct usercontextid.
userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
}
if (openWindowInfo) {
userContextId = openWindowInfo.originAttributes.userContextId;
}
let remoteTypeOptions = { window, userContextId }; if (triggeringRemoteType) { // NOTE: We intentionally don't allow setting preferredRemoteType to // NOT_REMOTE (null), as we don't want to choose the parent process.
remoteTypeOptions.preferredRemoteType = triggeringRemoteType;
}
if (tabArgument && tabArgument.linkedBrowser) {
remoteType = tabArgument.linkedBrowser.remoteType;
initialBrowsingContextGroupId =
tabArgument.linkedBrowser.browsingContext?.group.id;
} elseif (openWindowInfo) { if (openWindowInfo.isRemote) {
remoteType = ChromeUtils.predictRemoteTypeForURI( null,
remoteTypeOptions
);
} else {
remoteType = E10SUtils.NOT_REMOTE;
}
} else {
let uriToLoad = gBrowserInit.uriToLoadPromise; if (uriToLoad && Array.isArray(uriToLoad)) {
uriToLoad = uriToLoad[0]; // we only care about the first item
}
if (uriToLoad && typeof uriToLoad == "string") {
remoteType = ChromeUtils.predictRemoteTypeForURI(
uriToLoad,
remoteTypeOptions
);
} else { // If we reach here, we don't have the url to load. This means that // `uriToLoad` is most likely a promise which is waiting on SessionStore // initialization. We can't delay setting up the browser here, as that // would mean that `gBrowser.selectedBrowser` might not always exist, // which is the current assumption.
if (Cu.isInAutomation) {
ChromeUtils.releaseAssert(
!triggeringRemoteType, "Unexpected triggeringRemoteType with no uriToLoad"
);
}
// In this case we default to the privileged about process as that's // the best guess we can make, and we'll likely need it eventually.
remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
}
}
if (AIWindow.isAIWindowActive(window)) {
let uriToLoad = gBrowserInit.uriToLoadPromise;
let firstURI = Array.isArray(uriToLoad) ? uriToLoad[0] : uriToLoad;
if (!this._allowTransparentBrowser) { // firstURI may be a Promise (uriToLoadPromise still resolving while // SessionStore restores) or empty; only build a URI from a real // string, otherwise default to transparent like the no-URI case.
browser.toggleAttribute( "transparent",
!firstURI || typeof firstURI != "string" ||
AIWindow.isAIWindowContentPage(Services.io.newURI(firstURI))
);
}
}
let uniqueId = this._generateUniquePanelID();
let panel = this.getPanel(browser);
panel.id = uniqueId; this.tabpanels.appendChild(panel);
if (userContextId) {
tab.setAttribute("usercontextid", userContextId);
ContextualIdentityService.setTabStyle(tab);
}
updateUserContextUIIndicator();
this.#tabForBrowser.set(browser, tab);
this.appendStatusPanel();
// This is the initial browser, so it's usually active; the default is false // so we have to update it:
browser.docShellIsActive = this.shouldActivateDocShell(browser);
// Hook the browser up with a progress listener.
let tabListener = new TabProgressListener(tab, browser, true, false);
let filter = Cc[ "@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress);
filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); this.#tabListeners.set(tab, tabListener); this.#tabFilters.set(tab, filter);
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
}
/** *BEGINFORWARDEDBROWSERPROPERTIES.IFYOUADDAPROPERTYTOTHEBROWSERELEMENT *MAKESURETOADDITHEREASWELL.
*/
get canGoBack() { returnthis.selectedBrowser.canGoBack;
}
get canGoBackIgnoringUserInteraction() { returnthis.selectedBrowser.canGoBackIgnoringUserInteraction;
}
get canGoForward() { returnthis.selectedBrowser.canGoForward;
}
for (const tab of this.selectedTabs) { const browser = tab.linkedBrowser; const url = browser.currentURI; const urlSpec = url.spec; // We need to cache the content principal here because the browser will be // reconstructed when the remoteness changes and the content prinicpal will // be cleared after reconstruction. const principal = tab.linkedBrowser.contentPrincipal; if (this.updateBrowserRemotenessByURL(browser, urlSpec)) { // If the remoteness has changed, the new browser doesn't have any // information of what was loaded before, so we need to load the previous // URL again. if (tab.linkedPanel) {
loadBrowserURI(browser, url, principal);
} else { // Shift to fully loaded browser and make // sure load handler is instantiated.
tab.addEventListener( "SSTabRestoring",
() => loadBrowserURI(browser, url, principal),
{ once: true }
); this._insertBrowser(tab);
}
} else {
unchangedRemoteness.push(tab);
}
}
if (!unchangedRemoteness.length) { return;
}
// Reset temporary permissions on the remaining tabs to reload. // This is done here because we only want to reset // permissions on user reload. for (const tab of unchangedRemoteness) {
SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser); // Also reset DOS mitigations for the basic auth prompt on reload. delete tab.linkedBrowser.authPromptAbuseCounter;
}
gIdentityHandler.hidePopup();
gPermissionPanel.hidePopup();
if (document.hasValidTransientUserGestureActivation) {
reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_USER_ACTIVATION;
}
for (const tab of unchangedRemoteness) {
ReducedProtectionNotification.markUserReload(tab.linkedBrowser);
reloadBrowser(tab);
}
function reloadBrowser(tab) { if (tab.linkedPanel) { const { browsingContext } = tab.linkedBrowser; const { sessionHistory } = browsingContext; if (sessionHistory) {
sessionHistory.reload(reloadFlags);
} else {
browsingContext.reload(reloadFlags);
}
} else { // Shift to fully loaded browser and make // sure load handler is instantiated.
tab.addEventListener( "SSTabRestoring",
() => tab.linkedBrowser.browsingContext.reload(reloadFlags),
{
once: true,
}
);
gBrowser._insertBrowser(tab);
}
}
/** *Getthefindbar,andcreateitifitdoesn'texist. * *@returnthefindbar(ornullifthewindowortabisclosed/closingintheinterim).
*/
async getFindBar(aTab = this.selectedTab) {
let findBar = this.getCachedFindBar(aTab); if (findBar) { return findBar;
}
// Avoid re-entrancy by caching the promise we're about to return. if (!aTab._pendingFindBar) {
aTab._pendingFindBar = this._createFindBar(aTab);
} return aTab._pendingFindBar;
}
/** *Createafindbarinstance. * *@paramaTabthetabtocreatethefindbarfor. *@returnthecreatedfindbar,ornullifthewindowortabisclosed/closing.
*/
async _createFindBar(aTab) {
let findBar = document.createXULElement("findbar");
let browser = this.getBrowserForTab(aTab);
#notifyPinnedStatus(
aTab,
{ telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
) { // browsingContext is expected to not be defined on discarded tabs. if (aTab.linkedBrowser.browsingContext) {
aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned;
}
this.showTab(aTab); this.#handleTabMove(aTab, () => {
let periphery = document.getElementById( "pinned-tabs-container-periphery"
); // If periphery is null, append to end this.pinnedTabsContainer.insertBefore(aTab, periphery);
});
this.#handleTabMove(aTab, () => { // we remove this attribute first, so that allTabs represents // the moving of a tab from the pinned tabs container // and back into arrowscrollbox.
aTab.removeAttribute("pinned"); this.tabContainer.arrowScrollbox.prepend(aTab);
});
_updateVisibleNotificationBox(aBrowser) { if (!this._tabNotificationDeck) { // If the deck hasn't been created we don't need to create it here. return;
}
let notificationBox = this.readNotificationBox(aBrowser); if (
notificationBox?._stack &&
notificationBox._stack.parentNode !== this.getTabNotificationDeck()
) { // Notification box is not in the deck (e.g. in a Split View panel). return;
} this.getTabNotificationDeck().selectedViewName = notificationBox
? notificationBox.stack.getAttribute("name")
: "";
}
getTabDialogBox(aBrowser) { if (!aBrowser) { thrownew Error("aBrowser is required");
} if (!aBrowser.tabDialogBox) {
aBrowser.tabDialogBox = new TabDialogBox(aBrowser);
} return aBrowser.tabDialogBox;
}
getTabFromAudioEvent(aEvent) { if (!aEvent.isTrusted) { returnnull;
}
var browser = aEvent.originalTarget; var tab = this.getTabForBrowser(browser); return tab;
}
function callListeners(listeners, args) { for (let p of listeners) { if (aMethod in p) { try { if (!p[aMethod].apply(p, args)) {
rv = false;
}
} catch (e) { // don't inhibit other listeners
console.error(e);
}
}
}
}
aBrowser = aBrowser || this.selectedBrowser;
if (aCallGlobalListeners && aBrowser == this.selectedBrowser) {
callListeners(this.#progressListeners, aArguments);
}
if (aCallTabsListeners) {
aArguments.unshift(aBrowser);
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
) {
console.error(
`Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.`
); return;
}
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
if (aIconURL != aTab.getAttribute("image")) { if (aClearImageFirst) {
aTab.removeAttribute("image");
} if (aIconURL) {
let url = aIconURL; if ( this._remoteSVGIconDecoding &&
url.startsWith(this.FaviconUtils.SVG_DATA_URI_PREFIX)
) {
url = this.#getMozRemoteImageURLForSvg(browser, url);
}
aTab.setAttribute("image", url);
} else {
aTab.removeAttribute("image");
} this._tabAttrModified(aTab, ["image"]);
}
// The aOriginalURL argument is currently only used by tests. this._callProgressListeners(browser, "onLinkIconAvailable", [
aIconURL,
aOriginalURL,
]);
}
// Used for refreshing the icons when the color scheme changes.
#maybeRefreshIcons() { if (!this._remoteSVGIconDecoding) { return;
}
for (const tab of this.tabs) {
let browser = this.getBrowserForTab(tab);
let iconURL = browser.mIconURL; if (
!iconURL ||
!iconURL.startsWith(this.FaviconUtils.SVG_DATA_URI_PREFIX)
) { continue;
}
#getMozRemoteImageURLForSvg(browser, aUrl) {
let options = { // 16px is hardcoded for .tab-icon-image in tabs.css
size: Math.floor(16 * window.devicePixelRatio),
colorScheme: window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
};
// Use the tab's child process (if available) to load and render the favicon.
let contentParentId =
browser.browsingContext?.currentWindowGlobal?.contentParentId; if (contentParentId !== undefined) {
options.contentParentId = contentParentId;
}
/** *DeterminesthecontentofthewindowtitlethatrelatestotheTaskbar *Tab.ThisincludesthenameoftheTaskbarTab,ofthecontainer,and *oftheprofile. * *IfnoTaskbarTabisinuse,theprofileisaddedby *getWindowTitleForBrowserandthisreturnsnull. * *@returns{string|null}Thepartofthetitlethatwasdeterminedfrom *theTaskbarTab,ornullifnothingisneeded.
*/
#determineTaskbarTabTitle(aProfile) { if (!this._shouldExposeContentTitle) { // The Taskbar Tab and container info expose what site the user's on. returnnull;
}
#determineContentTitle(browser) {
let title = ""; if (
!this._shouldExposeContentTitle ||
(PrivateBrowsingUtils.isWindowPrivate(window) &&
!this._shouldExposeContentTitlePbm)
) { return title;
}
let docElement = document.documentElement; // If location bar is hidden and the URL type supports a host, // add the scheme and host to the title to prevent spoofing. // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239 try { if (docElement.getAttribute("chromehidden").includes("location")) { const uri = Services.io.createExposableURI(browser.currentURI);
let prefix = uri.prePath; if (uri.scheme == "about") {
prefix = uri.spec;
} elseif (uri.scheme == "moz-extension") { const ext = WebExtensionPolicy.getByHostname(uri.host); if (ext && ext.name) {
let extensionLabel = document.getElementById( "urlbar-label-extension"
);
prefix = `${extensionLabel.value} (${ext.name})`;
}
}
title = prefix + " - ";
}
} catch (e) { // ignored
}
if (docElement.hasAttribute("titlepreface")) {
title += docElement.getAttribute("titlepreface");
}
let tab = this.getTabForBrowser(browser); if (tab._labelIsContentTitle) { // Strip out any null bytes in the content title, since the // underlying widget implementations of nsWindow::SetTitle pass // null-terminated strings to system APIs.
title += tab.getAttribute("label").replace(/\0/g, "");
} return title;
}
getWindowTitleForBrowser(browser) { if (!this.#cachedTitleInfo) { this.#populateTitleCache();
}
let contentTitle = this.#determineContentTitle(browser);
let docElement = document.documentElement;
let isTemporaryPrivateWindow =
docElement.getAttribute("privatebrowsingmode") == "temporary";
let profileIdentifier =
SelectableProfileService?.isEnabled &&
SelectableProfileService.getCachedProfileCount() > 1 &&
SelectableProfileService.currentProfile?.name.replace(/\0/g, ""); // Note that empty/falsy bits get filtered below.
let taskbarTabTitle = this.#determineTaskbarTabTitle(profileIdentifier);
let parts = [contentTitle, taskbarTabTitle ?? profileIdentifier];
// On macOS PB windows, add the private window suffix if we have a content // title. We'll add the brand name and private window suffix for all other // platforms below. if (
AppConstants.platform == "macosx" &&
contentTitle &&
isTemporaryPrivateWindow
) {
parts.push(this.#cachedTitleInfo.privateWindowSuffixForContent);
}
// Show the brand name if we aren't a Taskbar Tab, since that is done in // #determineTaskbarTabTitle. On macOS we only do this if we don't have a // content title; elsewhere, the brand becomes a suffix in the title bar. if (
!taskbarTabTitle &&
(!contentTitle || AppConstants.platform != "macosx")
) {
parts.push( this.#cachedTitleInfo[
isTemporaryPrivateWindow ? "privateWindowTitle" : "mainWindowTitle"
]
);
}
updateCurrentBrowser(aForceUpdate) {
let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex); if (this.selectedBrowser == newBrowser && !aForceUpdate) { return;
}
let oldBrowser = this.selectedBrowser; // Once the async switcher starts, it's unpredictable when it will touch // the address bar, thus we store its state immediately.
gURLBar?.saveSelectionStateForBrowser(oldBrowser);
let newTab = this.getTabForBrowser(newBrowser);
let timerId; if (!aForceUpdate) {
timerId = Glean.browserTabswitch.update.start();
// Preview mode should not reset the owner if (!this.#previewMode && !oldTab.selected) {
oldTab.owner = null;
}
let lastRelatedTab = this.#lastRelatedTabMap.get(oldTab); if (lastRelatedTab) { if (!lastRelatedTab.selected) {
lastRelatedTab.owner = null;
}
} this.#lastRelatedTabMap = new WeakMap();
let oldBrowserPopupsBlocked =
oldBrowser.popupAndRedirectBlocker.getBlockedPopupCount();
let newBrowserPopupsBlocked =
newBrowser.popupAndRedirectBlocker.getBlockedPopupCount(); if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedPopupsEvent();
}
let oldBrowserRedirectBlocked =
oldBrowser.popupAndRedirectBlocker.isRedirectBlocked();
let newBrowserRedirectBlocked =
newBrowser.popupAndRedirectBlocker.isRedirectBlocked(); if (oldBrowserRedirectBlocked != newBrowserRedirectBlocked) {
newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedRedirectEvent();
}
// Update the URL bar.
let webProgress = newBrowser.webProgress; this._callProgressListeners( null, "onLocationChange",
[webProgress, null, newBrowser.currentURI, 0, true], true, false
);
let securityUI = newBrowser.securityUI; if (securityUI) { this._callProgressListeners( null, "onSecurityChange",
[webProgress, null, securityUI.state], true, false
); // Include the true final argument to indicate that this event is // simulated (instead of being observed by the webProgressListener). this._callProgressListeners( null, "onContentBlockingEvent",
[webProgress, null, newBrowser.getContentBlockingEvents(), true], true, false
);
}
if (!this.#previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.documentGlobal == BrowserWindowTracker.getTopWindow()) {
newTab.updateLastSeenActive();
oldTab.updateLastSeenActive();
}
let oldFindBar = oldTab._findBar; if (
oldFindBar &&
oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
!oldFindBar.hidden
) { this._lastFindValue = oldFindBar._findField.value;
}
// The tab has been selected, it's not unselected anymore. // Call the current browser's unselectedTabHover() with false // to dispatch an event.
newBrowser.unselectedTabHover(false);
}
// If the new tab is busy, and our current state is not busy, then // we need to fire a start to all progress listeners. if (newTab.hasAttribute("busy") && !this._isBusy) { this._isBusy = true; this._callProgressListeners( null, "onStateChange",
[
webProgress, null,
Ci.nsIWebProgressListener.STATE_START |
Ci.nsIWebProgressListener.STATE_IS_NETWORK, 0,
], true, false
);
}
// If the new tab is not busy, and our current state is busy, then // we need to fire a stop to all progress listeners. if (!newTab.hasAttribute("busy") && this._isBusy) { this._isBusy = false; this._callProgressListeners( null, "onStateChange",
[
webProgress, null,
Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_NETWORK, 0,
], true, false
);
}
// TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code // that might rely upon the other changes suppressed. // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window if (!this.#previewMode) { // We've selected the new tab, so go ahead and notify listeners.
let event = new CustomEvent("TabSelect", {
bubbles: true,
cancelable: false,
detail: {
previousTab: oldTab,
},
});
newTab.dispatchEvent(event);
// Check if switched repeatedly between tabs in the last minute this._checkIfShouldTriggerTabSelectMessage(oldTab, newTab);
this._startMultiSelectChange(); this.#multiSelectChangeSelected = true; this.clearMultiSelectedTabs(); if (this.#multiSelectChangeAdditions.size) { // Some tab has been multiselected just before switching tabs. // The tab that was selected at that point should also be multiselected. this.addToMultiSelectedTabs(oldTab);
}
if (!gMultiProcessBrowser) { this._adjustFocusBeforeTabSwitch(oldTab, newTab); this._adjustFocusAfterTabSwitch(newTab);
}
// Bug 1781806 - A forced update can indicate the tab was already // selected. To ensure the internal state of the Urlbar is kept in // sync, notify it as if focus changed. Alternatively, if there is no // force update but the load context is not using remote tabs, there // can be a focus change due to the _adjustFocus above. if (aForceUpdate || !gMultiProcessBrowser) {
gURLBar.afterTabSwitchFocusChange();
}
}
// Enable touch events to start a native dragging // session to allow the user to easily drag the selected tab. // This is currently only supported on Windows.
oldTab.removeAttribute("touchdownstartsdrag");
newTab.setAttribute("touchdownstartsdrag", "true");
if (!gMultiProcessBrowser) {
document.commandDispatcher.unlock();
let event = new CustomEvent("TabSwitchDone", {
bubbles: true,
cancelable: true,
}); this.dispatchEvent(event);
}
if (!aForceUpdate) {
Glean.browserTabswitch.update.stopAndAccumulate(timerId);
}
}
// Only look at entries from the last minute this.#tabSelectTimestamps = this.#tabSelectTimestamps.filter(
entry => now - entry.timestamp < ONE_MINUTE_MS
);
if (this._asyncTabSwitching) {
newBrowser._userTypedValueAtBeforeTabSwitch = newBrowser.userTypedValue;
}
if (this.isFindBarInitialized(oldTab)) {
let findBar = this.getCachedFindBar(oldTab);
oldTab._findBarFocused =
!findBar.hidden &&
findBar._findField.getAttribute("focused") == "true";
}
let activeEl = document.activeElement; // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) {
newTab.focus();
} elseif (
gMultiProcessBrowser &&
activeEl != newBrowser &&
activeEl != newTab
) { // In e10s, if focus isn't already in the tabstrip or on the new browser, // and the new browser's previous focus wasn't in the url bar but focus is // there now, we need to adjust focus further.
let keepFocusOnUrlBar =
newBrowser &&
gURLBar.getBrowserState(newBrowser).urlbarFocused &&
gURLBar.focused; if (!keepFocusOnUrlBar) { // Clear focus so that _adjustFocusAfterTabSwitch can detect if // some element has been focused and respect that.
document.activeElement.blur();
}
}
}
_adjustFocusAfterTabSwitch(newTab) { // Don't steal focus from the tab bar. if (document.activeElement == newTab) { return;
}
let newBrowser = this.getBrowserForTab(newTab);
if (newBrowser.hasAttribute("tabDialogShowing")) {
newBrowser.tabDialogBox.focus(); return;
} // Focus the location bar if it was previously focused for that tab. // In full screen mode, only bother making the location bar visible // if the tab is a blank one. if (gURLBar.getBrowserState(newBrowser).urlbarFocused) {
let selectURL = () => { if (this._asyncTabSwitching) { // Set _awaitingSetURI flag to suppress popup notification // explicitly while tab switching asynchronously.
newBrowser._awaitingSetURI = true;
// The onLocationChange event called in updateCurrentBrowser() will // be captured in browser.js, then it calls gURLBar.setURI(). In case // of that doing processing of here before doing above processing, // the selection status that gURLBar.select() does will be releasing // by gURLBar.setURI(). To resolve it, we call gURLBar.select() after // finishing gURLBar.setURI(). const currentActiveElement = document.activeElement;
gURLBar.inputField.addEventListener( "SetURI",
() => { delete newBrowser._awaitingSetURI;
// If the user happened to type into the URL bar for this browser // by the time we got here, focusing will cause the text to be // selected which could cause them to overwrite what they've // already typed in.
let userTypedValueAtBeforeTabSwitch =
newBrowser._userTypedValueAtBeforeTabSwitch; delete newBrowser._userTypedValueAtBeforeTabSwitch; if (
newBrowser.userTypedValue &&
newBrowser.userTypedValue != userTypedValueAtBeforeTabSwitch
) { return;
}
// This inDOMFullscreen attribute indicates that the page has something // such as a video in fullscreen mode. Opening a new tab will cancel // fullscreen mode, so we need to wait for that to happen and then // select the url field. if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
window.addEventListener("MozDOMFullscreen:Exited", selectURL, {
once: true,
wantsUntrusted: false,
}); return;
}
if (!window.fullScreen || newTab.isEmpty) {
selectURL(); return;
}
}
// Focus the find bar if it was previously focused for that tab. if (
gFindBarInitialized &&
!gFindBar.hidden && this.selectedTab._findBarFocused
) {
gFindBar._findField.focus(); return;
}
// Don't focus the content area if something has been focused after the // tab switch was initiated. if (gMultiProcessBrowser && document.activeElement != document.body) { return;
}
// We're now committed to focusing the content area.
let fm = Services.focus;
let focusFlags = fm.FLAG_NOSCROLL;
if (!gMultiProcessBrowser) {
let newFocusedElement = fm.getFocusedElementForWindow(
window.content, true,
{}
);
// for anchors, use FLAG_SHOWRING so that it is clear what link was // last clicked when switching back to that tab if (
newFocusedElement &&
(HTMLAnchorElement.isInstance(newFocusedElement) ||
newFocusedElement.getAttributeNS( "http://www.w3.org/1999/xlink", "type"
) == "simple")
) {
focusFlags |= fm.FLAG_SHOWRING;
}
}
fm.setFocus(newBrowser, focusFlags);
}
_tabAttrModified(aTab, aChanged) { if (aTab.closing) { return;
}
let event = new CustomEvent("TabAttrModified", {
bubbles: true,
cancelable: false,
detail: {
changed: aChanged,
},
});
aTab.dispatchEvent(event);
}
resetBrowserSharing(aBrowser) {
let tab = this.getTabForBrowser(aBrowser); if (!tab) { return;
} // If WebRTC was used, leave object to enable tracking of grace periods.
aBrowser._sharingState = aBrowser._sharingState?.webRTC
? { webRTC: {} }
: {};
tab.removeAttribute("sharing"); this._tabAttrModified(tab, ["sharing"]); if (aBrowser == this.selectedBrowser) {
gPermissionPanel.updateSharingIndicator();
}
}
updateBrowserSharing(aBrowser, aState) {
let tab = this.getTabForBrowser(aBrowser); if (!tab) { return;
}
if ("webRTC" in aState) { if (aBrowser._sharingState.webRTC?.sharing) { if (aBrowser._sharingState.webRTC.paused) {
tab.removeAttribute("sharing");
} else {
tab.setAttribute("sharing", aState.webRTC.sharing);
}
} else {
tab.removeAttribute("sharing");
} this._tabAttrModified(tab, ["sharing"]);
}
if (aBrowser == this.selectedBrowser) {
gPermissionPanel.updateSharingIndicator();
}
}
getTabSharingState(aTab) { // Normalize the state object for consumers (ie.extensions).
let browser = aTab.linkedBrowser;
let state = Object.assign(
{},
browser._sharingState && browser._sharingState.webRTC
); return {
camera: !!state.camera,
microphone: !!state.microphone,
screen: state.screen && state.screen.replace("Paused", ""),
};
}
setInitialTabTitle(aTab, aTitle, aOptions = {}) { // Convert some non-content title (actually a url) to human readable title if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) {
aTitle = this.tabContainer.emptyTabTitle;
}
if (aTitle) { if (!aTab.getAttribute("label")) {
aTab._labelIsInitialTitle = true;
}
this._setTabLabel(aTab, aTitle, aOptions);
}
}
#dataURLRegEx = /^data:[^,]+;base64,/i;
// Regex to test if a string (potential tab label) consists of only non- // printable characters. We consider Unicode categories Separator // (spaces & line-breaks) and Other (control chars, private use, non- // character codepoints) to be unprintable, along with a few specific // characters whose expected rendering is blank: // U+2800 BRAILLE PATTERN BLANK (category So) // U+115F HANGUL CHOSEONG FILLER (category Lo) // U+1160 HANGUL JUNGSEONG FILLER (category Lo) // U+3164 HANGUL FILLER (category Lo) // U+FFA0 HALFWIDTH HANGUL FILLER (category Lo) // We also ignore combining marks, as in the absence of a printable base // character they are unlikely to be usefully rendered, and may well be // clipped away entirely.
#nonPrintingRegEx =
/^[\p{Z}\p{C}\p{M}\u{115f}\u{1160}\u{2800}\u{3164}\u{ffa0}]*$/u;
setTabTitle(aTab) { var browser = this.getBrowserForTab(aTab); var title = browser.contentTitle;
if (aTab.hasAttribute("customizemode")) {
title = this.tabLocalization.formatValueSync( "tabbrowser-customizemode-tab-title"
);
}
// Don't replace an initially set label with the URL while the tab // is loading. if (aTab._labelIsInitialTitle) { if (!title) { returnfalse;
} delete aTab._labelIsInitialTitle;
}
let isURL = false;
// Trim leading and trailing whitespace from the title.
title = title.trim();
// If the title contains only non-printing characters (or only combining // marks, but no base character for them), we won't use it. if (this.#nonPrintingRegEx.test(title)) {
title = "";
}
let isContentTitle = !!title; if (!title) { // See if we can use the URI as the title. if (browser.currentURI.displaySpec) { try {
title = Services.io.createExposableURI(
browser.currentURI
).displaySpec;
} catch (ex) {
title = browser.currentURI.displaySpec;
}
}
if (title && !isBlankPageURL(title)) {
isURL = true; if (title.length <= 500 || !this.#dataURLRegEx.test(title)) { // Try to unescape not-ASCII URIs using the current character set. try {
let characterSet = browser.characterSet;
title = Services.textToSubURI.unEscapeNonAsciiURI(
characterSet,
title
);
} catch (ex) { /* Do nothing. */
}
}
} else { // No suitable URI? Fall back to our untitled string.
title = this.tabContainer.emptyTabTitle;
}
}
// While an auth prompt from a base domain different than the current sites is open, we do not want to show the tab title of the current site, // but of the origin that is requesting authentication. // This is to prevent possible auth spoofing scenarios. // See bug 791594 for reference.
setTabLabelForAuthPrompts(aTab, aLabel) { returnthis._setTabLabel(aTab, aLabel);
}
// If it's a long data: URI that uses base64 encoding, truncate to a // reasonable length rather than trying to display the entire thing, // which can hang or crash the browser. // We can't shorten arbitrary URIs like this, as bidi etc might mean // we need the trailing characters for display. But a base64-encoded // data-URI is plain ASCII, so this is OK for tab-title display. // (See bug 1408854.) if (isURL && aLabel.length > 500 && this.#dataURLRegEx.test(aLabel)) {
aLabel = aLabel.substring(0, 500) + "\u2026";
}
aTab._fullLabel = aLabel;
if (!isContentTitle) { // Remove protocol and "www." if (!("_regex_shortenURLForTabLabel" in this)) { this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
}
aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
}
if (aLabel.length > TAB_LABEL_MAX_LENGTH) { // Clamp overly-long titles to avoid DoS-type hangs (bug 736194).
aLabel = aLabel.substring(0, TAB_LABEL_MAX_LENGTH);
}
aTab._labelIsContentTitle = isContentTitle;
if (aTab.getAttribute("label") == aLabel) { returnfalse;
}
let dwu = window.windowUtils;
let isRTL =
dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
// Dispatch TabAttrModified event unless we're setting the label // before the TabOpen event was dispatched. if (!beforeTabOpen) { this._tabAttrModified(aTab, ["label"]);
}
// The tab selected after this new tab is closed (i.e. the new tab's // "owner") is the next adjacent tab (i.e. not the previously viewed tab) // when several urls are opened here (i.e. closing the first should select // the next of many URLs opened) or if the pref to have UI links opened in // the background is set (i.e. the link is not being opened modally) // // i.e. // Number of URLs Load UI Links in BG Focus Last Viewed? // == 1 false YES // == 1 true NO // > 1 false/true NO var multiple = aURIs.length > 1; var owner = multiple || inBackground ? null : this.selectedTab; var firstTabAdded = null; var targetTabIndex = -1;
if (typeof elementIndex == "number") {
newIndex = this.#elementIndexToTabIndex(elementIndex);
} if (typeof newIndex != "number") {
newIndex = -1;
}
// When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent // if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks // open in the same order they are in the folder. if (
multiple &&
newIndex < 0 &&
Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")
) {
newIndex = this.selectedTab._tPos + 1;
}
if (replace) { if (this.isTabGroupLabel(targetTab)) { thrownew Error( "Replacing a tab group label with a tab is not supported"
);
}
let browser; if (targetTab) {
browser = this.getBrowserForTab(targetTab);
targetTabIndex = targetTab._tPos;
} else {
browser = this.selectedBrowser;
targetTabIndex = this.tabContainer.selectedIndex;
}
let loadFlags = LOAD_FLAGS_NONE; if (allowThirdPartyFixup) {
loadFlags |=
LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
} if (!allowInheritPrincipal) {
loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
} if (fromExternal) {
loadFlags |= LOAD_FLAGS_FROM_EXTERNAL;
} if (newWindowLoad) {
browser.initiatedFromNonWebControlled = true;
} try {
browser.fixupAndLoadURIString(aURIs[0], {
loadFlags,
postData: postDatas && postDatas[0],
triggeringPrincipal,
policyContainer,
});
} catch (e) { // Ignore failure in case a URI is wrong, so we can continue // opening the next ones.
}
tabs.push(targetTab || this.selectedTab);
} else {
let params = {
allowInheritPrincipal,
ownerTab: owner,
skipAnimation: multiple,
allowThirdPartyFixup,
postData: postDatas && postDatas[0],
userContextId,
triggeringPrincipal,
bulkOrderedOpen: multiple,
policyContainer,
fromExternal,
tabGroup,
}; if (newIndex > -1) {
params.tabIndex = newIndex;
}
firstTabAdded = this.addTab(aURIs[0], params);
tabs.push(firstTabAdded); if (newIndex > -1) {
targetTabIndex = firstTabAdded._tPos;
}
}
let tabNum = targetTabIndex; for (let i = 1; i < aURIs.length; ++i) {
let params = {
allowInheritPrincipal,
skipAnimation: true,
allowThirdPartyFixup,
postData: postDatas && postDatas[i],
userContextId,
triggeringPrincipal,
bulkOrderedOpen: true,
policyContainer,
fromExternal,
tabGroup,
}; if (targetTabIndex > -1) {
params.tabIndex = ++tabNum;
}
let tab = this.addTab(aURIs[i], params);
tabs.push(tab);
}
if (firstTabAdded && !inBackground) { this.selectedTab = firstTabAdded;
}
// We have to be careful with this here, as the "no remote type" is null, // not a string. Make sure to check only for undefined, since null is // allowed. if (remoteType === undefined) { thrownew Error("Remote type must be set!");
}
let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
if (!gMultiProcessBrowser && shouldBeRemote) { thrownew Error( "Cannot switch to remote browser in a window " + "without the remote tabs load context."
);
}
// Abort if we're not going to change anything
let oldRemoteType = aBrowser.remoteType; if (
isRemote == shouldBeRemote &&
!newFrameloader &&
(!isRemote || oldRemoteType == remoteType)
) { returnfalse;
}
let tab = this.getTabForBrowser(aBrowser); // aBrowser needs to be inserted now if it hasn't been already. this._insertBrowser(tab);
let evt = document.createEvent("Events");
evt.initEvent("BeforeTabRemotenessChange", true, false);
tab.dispatchEvent(evt);
// Unhook our progress listener.
let filter = this.#tabFilters.get(tab);
let listener = this.#tabListeners.get(tab); // We should always have a filter, but if we fail to create a content // process when creating a new tab, we can end up here trying to switch // remoteness to load about:tabcrashed, without a filter/listener. if (filter) {
aBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
}
// We'll be creating a new listener, so destroy the old one.
listener?.destroy();
let oldUserTypedValue = aBrowser.userTypedValue;
let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
// Change the "remote" attribute.
// Make sure the browser is destroyed so it unregisters from observer notifications
aBrowser.destroy();
// This call actually switches out our frameloaders. Do this as late as // possible before rebuilding the browser, as we'll need the new browser // state set up completely first.
aBrowser.changeRemoteness({
remoteType,
});
// Once we have new frameloaders, this call sets the browser back up.
aBrowser.construct();
aBrowser.userTypedValue = oldUserTypedValue; if (hadStartedLoad) {
aBrowser.urlbarChangeTracker.startedLoad();
}
// This shouldn't really be necessary, however, this has the side effect // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote // frames, which the tab switcher depends on. // // eslint-disable-next-line no-self-assign
aBrowser.docShellIsActive = aBrowser.docShellIsActive;
// Create a new tab progress listener for the new browser we just injected, // since tab progress listeners have logic for handling the initial about:blank // load
listener = new TabProgressListener(tab, aBrowser, true, false); this.#tabListeners.set(tab, listener); if (!filter) {
filter = Cc[ "@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress); this.#tabFilters.set(tab, filter);
}
filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
// Restore the progress listener.
aBrowser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
// Restore the securityUI state.
let securityUI = aBrowser.securityUI;
let state = securityUI
? securityUI.state
: Ci.nsIWebProgressListener.STATE_IS_INSECURE; this._callProgressListeners(
aBrowser, "onSecurityChange",
[aBrowser.webProgress, null, state], true, false
);
let event = aBrowser.getContentBlockingEvents(); // Include the true final argument to indicate that this event is // simulated (instead of being observed by the webProgressListener). this._callProgressListeners(
aBrowser, "onContentBlockingEvent",
[aBrowser.webProgress, null, event, true], true, false
);
if (shouldBeRemote) { // Switching the browser to be remote will connect to a new child // process so the browser can no longer be considered to be // crashed.
tab.removeAttribute("crashed");
}
// If the findbar has been initialised, reset its browser reference. if (this.isFindBarInitialized(tab)) { this.getCachedFindBar(tab).browser = aBrowser;
}
// If this URL can't load in the current browser then flip it to the // correct type. if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) { returnthis.updateBrowserRemoteness(aBrowser, aOptions);
}
returnfalse;
}
createBrowser({
isPreloadBrowser,
name,
openWindowInfo,
remoteType,
initialBrowsingContextGroupId,
uriIsAboutBlank,
userContextId,
skipLoad,
} = {}) {
let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the // permanentKey is held by something after this window closes, it // doesn't keep the window alive.
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
// Ensure that the browser will be created in a specific initial // BrowsingContextGroup. This may change the process selection behaviour // of the newly created browser, and is often used in combination with // "remoteType" to ensure that the initial about:blank load occurs // within the same process as another window. if (initialBrowsingContextGroupId) {
b.setAttribute( "initialBrowsingContextGroupId",
initialBrowsingContextGroupId
);
}
// Propagate information about the opening content window to the browser. if (openWindowInfo) {
b.openWindowInfo = openWindowInfo;
}
// This will be used by gecko to control the name of the opened // window. if (name) { // XXX: The `name` property is special in HTML and XUL. Should // we use a different attribute name for this?
b.setAttribute("name", name);
}
if (AIWindow.isAIWindowActive(window) || this._allowTransparentBrowser) {
b.setAttribute("transparent", "true");
}
let stack = document.createXULElement("stack");
stack.className = "browserStack";
stack.appendChild(b);
let browserContainer = document.createXULElement("vbox");
browserContainer.className = "browserContainer";
browserContainer.appendChild(stack);
let browserSidebarContainer = document.createXULElement("hbox");
browserSidebarContainer.className = "browserSidebarContainer";
browserSidebarContainer.appendChild(browserContainer);
// Prevent the superfluous initial load of a blank document // if we're going to load something other than about:blank. if (!uriIsAboutBlank || skipLoad) {
b.setAttribute("nodefaultsrc", "true");
}
return b;
}
_createLazyBrowser(aTab) {
let browser = aTab.linkedBrowser;
let names = this.#browserBindingProperties;
for (let i = 0; i < names.length; i++) {
let name = names[i];
let getter;
let setter; switch (name) { case"audioMuted":
getter = () => aTab.hasAttribute("muted"); break; case"contentTitle":
getter = () => SessionStore.getLazyTabValue(aTab, "title"); break; case"currentURI":
getter = () => { // Avoid recreating the same nsIURI object over and over again... if (browser._cachedCurrentURI) { return browser._cachedCurrentURI;
}
let url =
SessionStore.getLazyTabValue(aTab, "url") || "about:blank"; return (browser._cachedCurrentURI = Services.io.newURI(url));
}; break; case"didStartLoadSinceLastUserTyping":
getter = () => () => false; break; case"fullZoom": case"textZoom":
getter = () => 1; break; case"tabHasCustomZoom":
getter = () => false; break; case"getTabBrowser":
getter = () => () => this; break; case"isRemoteBrowser":
getter = () => browser.hasAttribute("remote"); break; case"permitUnload":
getter = () => () => ({ permitUnload: true }); break; case"reload": case"reloadWithFlags":
getter = () => params => { // Wait for load handler to be instantiated before // initializing the reload.
aTab.addEventListener( "SSTabRestoring",
() => {
browser[name](params);
},
{ once: true }
);
gBrowser._insertBrowser(aTab);
}; break; case"remoteType":
getter = () => {
let url =
SessionStore.getLazyTabValue(aTab, "url") || "about:blank"; return ChromeUtils.predictRemoteTypeForURI(url, {
window,
userContextId: aTab.getAttribute("usercontextid"),
});
}; break; case"userTypedValue": case"userTypedClear":
getter = () => SessionStore.getLazyTabValue(aTab, name); break; default:
getter = () => { if (AppConstants.NIGHTLY_BUILD) {
let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
Services.console.logStringMessage(message + new Error().stack);
} this._insertBrowser(aTab); return browser[name];
};
setter = value => { if (AppConstants.NIGHTLY_BUILD) {
let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
Services.console.logStringMessage(message + new Error().stack);
} this._insertBrowser(aTab); return (browser[name] = value);
};
}
Object.defineProperty(browser, name, {
get: getter,
set: setter,
configurable: true,
enumerable: true,
});
}
}
// If browser is already inserted or window is closed don't do anything. if (aTab.linkedPanel || window.closed) { return;
}
let browser = aTab.linkedBrowser;
// If browser is a lazy browser, delete the substitute properties. if (this.#browserBindingProperties[0] in browser) { for (let name of this.#browserBindingProperties) { delete browser[name];
}
}
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
panel.id = uniqueId;
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary. if (!panel.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. // AppendChild will cause a synchronous about:blank load. this.tabpanels.appendChild(panel);
}
// wire up a progress listener for the new browser object.
let tabListener = new TabProgressListener(
aTab,
browser,
uriIsAboutBlank,
usingPreloadedContent
); const filter = Cc[ "@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress);
filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
); this.#tabListeners.set(aTab, tabListener); this.#tabFilters.set(aTab, filter);
// Most of the time, we start our browser's docShells out as inactive, // and then maintain activeness in the tab switcher. Preloaded about:newtab's // are already created with their docShell's as inactive, but then explicitly // render their layers to ensure that we can switch to them quickly. We avoid // setting docShellIsActive to false again in this case, since that'd cause // the layers for the preloaded tab to be dropped, and we'd see a flash // of empty content instead. // // So for all browsers except for the preloaded case, we set the browser // docShell to inactive. if (!usingPreloadedContent) {
browser.docShellIsActive = false;
}
// If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { this.tabs[0].linkedBrowser.browsingContext.hasSiblings = true; this.tabs[1].linkedBrowser.browsingContext.hasSiblings = true;
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
if (aTab.userContextId) {
browser.setAttribute("usercontextid", aTab.userContextId);
}
browser.browsingContext.isAppTab = aTab.pinned;
// We don't want to update the container icon and identifier if // this is not the selected browser. if (aTab.selected) {
updateUserContextUIIndicator();
}
// Only fire this event if the tab is already in the DOM // and will be handled by a listener. if (aTab.isConnected) { var evt = new CustomEvent("TabBrowserInserted", {
bubbles: true,
detail: { insertedOnTabCreation: aInsertedOnTabCreation },
});
aTab.dispatchEvent(evt);
}
}
_mayDiscardBrowser(aTab, aForceDiscard) {
let browser = aTab.linkedBrowser;
let action = aForceDiscard ? "unload" : "dontUnload";
// discarding a browser will dismiss any dialogs, so don't // allow this unless we're forcing it. if (
!aForceDiscard && this.getTabDialogBox(browser)._tabDialogManager._dialogs.length
) { returnfalse;
}
returntrue;
}
async prepareDiscardBrowser(aTab) {
let browser = aTab.linkedBrowser; // This is similar to the checks in _mayDiscardBrowser, but // doesn't have to be complete (and we want to be sure not to // fire the beforeunload event). Calling TabStateFlusher.flush() // and then not unloading the browser is fine. if (aTab.closing || this.#windowIsClosing || !browser.isRemoteBrowser) { return;
}
// Flush the tab's state so session restore has the latest data.
await this.TabStateFlusher.flush(browser);
}
discardBrowser(aTab, aForceDiscard) { "use strict";
let browser = aTab.linkedBrowser;
if (!this._mayDiscardBrowser(aTab, aForceDiscard)) { returnfalse;
}
// Abort any dialogs since the browser is about to be discarded.
let tabDialogBox = this.getTabDialogBox(browser);
tabDialogBox.abortAllDialogs();
// Set browser parameters for when browser is restored. Also remove // listeners and set up lazy restore data in SessionStore. This must // be done before browser is destroyed and removed from the document.
aTab._browserParams = {
uriIsAboutBlank: browser.currentURI.spec == "about:blank",
remoteType: browser.remoteType,
usingPreloadedContent: false,
};
SessionStore.resetBrowserToLazyState(aTab); // Indicate that this tab was explicitly unloaded (i.e. not // from a session restore) in case we want to style that // differently. if (aForceDiscard) {
aTab.toggleAttribute("discarded", true);
}
// Remove the tab's filter and progress listener.
let filter = this.#tabFilters.get(aTab);
let listener = this.#tabListeners.get(aTab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
listener.destroy();
// Reset the findbar and remove it if it is attached to the tab. if (aTab._findBar) {
aTab._findBar.close(true);
aTab._findBar.remove(); delete aTab._findBar;
}
// Remove potentially stale attributes.
let attributesToRemove = [ "activemedia-blocked", "busy", "pendingicon", "progress", "soundplaying",
];
let removedAttributes = []; for (let attr of attributesToRemove) { if (aTab.hasAttribute(attr)) {
removedAttributes.push(attr);
aTab.removeAttribute(attr);
}
} if (removedAttributes.length) { this._tabAttrModified(aTab, removedAttributes);
}
/** *Createsatabdirectlyafter`tab`. * *@param{MozTabbrowserTab}adjacentTab *@param{string}uriString *@param{object}[options] *Optionsfrom`Tabbrowser.addTab`.
*/
addAdjacentTab(adjacentTab, uriString, options = {}) { // If the caller opted out of tab group membership but `tab` is in a // tab group, insert the tab after `tab`'s group. Otherwise, insert the // new tab right after `tab`. const tabIndex =
!options.tabGroup && adjacentTab.group
? adjacentTab.group.tabs.at(-1)._tPos + 1
: adjacentTab._tPos + 1;
if (!UserInteraction.running("browser.tabs.opening", window)) {
UserInteraction.start("browser.tabs.opening", "initting", window);
}
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
// if we're adding tabs, we're past interrupt mode, ditch the owner if (this.selectedTab.owner) { this.selectedTab.owner = null;
}
// Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the // user context ID. // // If we have a browser opener (which is usually the browser // element from a remote window.open() call), use that. // // Otherwise, if the tab is related to the current tab (e.g., // because it was opened by a link click), use the selected tab as // the owner. If referrerInfo is set, and we don't have an // explicit relatedToCurrent arg, we assume that the tab is // related to the current tab, since referrerURI is null or // undefined if the tab is opened from an external application or // bookmark (i.e. somewhere other than an existing tab). if (relatedToCurrent == null) {
relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer);
}
let openerTab =
(openerBrowser && this.getTabForBrowser(openerBrowser)) ||
(relatedToCurrent && this.selectedTab) || null;
// 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 =
!skipAnimation &&
!pinned &&
!this.tabContainer.verticalMode &&
!this.tabContainer.overflowing &&
!gReduceMotion;
let uriInfo = this._determineURIToLoad(uriString, createLazyBrowser);
let { uri, uriIsAboutBlank, lazyBrowserURI } = uriInfo; // Have to overwrite this if we're lazy-loading. Should go away // with bug 1818777.
({ uriString } = uriInfo);
let usingPreloadedContent = false;
let b, t;
try {
t = this._createTab({
uriString,
animate,
userContextId,
openerTab,
pinned,
noInitialLabel,
skipBackgroundNotify,
}); if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, {
elementIndex,
tabIndex,
ownerTab,
openerTab,
pinned,
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
}
if (focusUrlBar) {
gURLBar.getBrowserState(b).urlbarFocused = true;
}
// If the caller opts in, create a lazy browser. if (createLazyBrowser) { this._createLazyBrowser(t);
if (lazyBrowserURI) { // Lazy browser must be explicitly registered so tab will appear as // a switch-to-tab candidate in autocomplete. this.UrlbarProviderOpenTabs.registerOpenTab(
lazyBrowserURI.spec,
t.userContextId,
tabGroup?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
);
b.registeredOpenURI = lazyBrowserURI;
} // If we're not inserting the tab into the DOM, we can't set the tab // state meaningfully. Session restore (the only caller who does this) // will have to do this work itself later, when the tabs have been // inserted. if (insertTab) {
SessionStore.setTabState(t, {
entries: [
{
url: lazyBrowserURI?.spec || "about:blank",
title: lazyTabTitle,
triggeringPrincipal_base64:
E10SUtils.serializePrincipal(triggeringPrincipal),
},
], // Make sure to store the userContextId associated to the lazy tab // otherwise it would be created as a default tab when recreated on a // session restore (See Bug 1819794).
userContextId,
});
}
} else { this._insertBrowser(t, true); // If we were called by frontend and don't have openWindowInfo, // but we were opened from another browser, set the cross group // opener ID: if (openerBrowser?.browsingContext && !openWindowInfo) {
b.browsingContext.crossGroupOpener = openerBrowser.browsingContext;
}
}
} catch (e) {
console.error("Failed to create tab");
console.error(e);
t?.remove(); if (t?.linkedBrowser) { this.#tabFilters.delete(t); this.#tabListeners.delete(t); this.getPanel(t.linkedBrowser).remove();
} returnnull;
}
if (insertTab) { // Fire a TabOpen event const tabOpenDetail = {
...eventDetail,
fromExternal,
}; this._fireTabOpen(t, tabOpenDetail);
// This field is updated regardless if we actually animate // since it's important that we keep this count correct in all cases. this.tabAnimationsInProgress++;
if (animate) { // Kick the animation off. // TODO: we should figure out a better solution here. We use RAF // to avoid jank of the animation due to synchronous work happening // on tab open. // With preloaded content though a single RAF happens too early. and // both the transition and the transitionend event don't happen. if (usingPreloadedContent) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
t.setAttribute("fadein", "true");
});
});
} else {
requestAnimationFrame(() => {
t.setAttribute("fadein", "true");
});
}
}
if (!inBackground) { this.selectedTab = t;
} return t;
}
#elementIndexToTabIndex(elementIndex) { if (elementIndex < 0) { return -1;
} if (elementIndex >= this.tabContainer.dragAndDropElements.length) { returnthis.tabs.length;
}
let element = this.tabContainer.dragAndDropElements[elementIndex]; if (this.isTabGroupLabel(element)) {
element = element.group.tabs[0];
} if (this.isSplitViewWrapper(element)) {
element = element.tabs[0];
} return element._tPos;
}
/** *@param{number}id *@returns{MozTabSplitViewWrapper}
*/
_createTabSplitView(id) { if (id && typeof id !== "number") { thrownew Error("Unexpected id type: " + typeof id);
} if (!id) {
id = SessionStore.getNextSplitViewId();
}
let splitview = document.createXULElement("tab-split-view-wrapper", {
is: "tab-split-view-wrapper",
});
splitview.splitViewId = id; return splitview;
}
// Capture group information before tabs are moved
let tabGroupInfo = null; if (trigger && tabs.length >= 2) { const primaryGroup = tabs[0]?.group; const secondaryGroup = tabs[1]?.group;
tabGroupInfo = { primaryGroup, secondaryGroup };
}
// Bail out if the split view is empty at this point. This can happen if all // provided tabs are pinned and therefore cannot be grouped. if (!splitview.tabs.length) {
splitview.remove(); returnnull;
}
// Record telemetry for split view creation if (trigger && tabGroupInfo) { const tab_layout = this.tabContainer.verticalMode
? "vertical"
: "horizontal";
let tabgroup; const { primaryGroup, secondaryGroup } = tabGroupInfo;
if (!color) {
color = this.tabGroupMenu.nextUnusedColor;
}
if (!id) { // Note: If this changes, make sure to also update the // getExtTabGroupIdForInternalTabGroupId implementation in // browser/components/extensions/parent/ext-browser.js. // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
}
let group = this._createTabGroup(
id,
color, false,
label,
isAdoptingGroup
); this.tabContainer.insertBefore(
group,
insertBefore?.group ?? insertBefore
);
group.addTabs(tabsAndSplitViews);
// Bail out if the group is empty at this point. This can happen if all // provided tabs are pinned and therefore cannot be grouped. if (!group.tabs.length) {
group.remove(); returnnull;
}
if (isUserTriggered) {
group.dispatchEvent( new CustomEvent("TabGroupCreateByUser", {
bubbles: true,
detail: {
telemetryUserCreateSource,
},
})
);
}
// Fixes bug1953801 and bug1954689 // Ensure that the tab state cache is updated immediately after creating // a group. This is necessary because we consider group creation a // deliberate user action indicating the tab has importance for the user. // Without this, it is not possible to save and close a tab group with // a short lifetime.
group.tabs.forEach(tab => { this.TabStateFlusher.flush(tab.linkedBrowser);
});
if (!options.skipPermitUnload) { // Process permit unload handlers and allow user cancel
let cancel = await this.runBeforeUnloadForTabs(group.tabs); if (cancel) { if (SessionStore.getSavedTabGroup(group.id)) { // If this group is currently saved, it's being removed as part of a // save & close operation. We need to forget the saved group // if the close is canceled.
SessionStore.forgetSavedTabGroup(group.id);
} return;
}
options.skipPermitUnload = true;
}
if (group.tabs.length == this.tabs.length) { // explicit calls to removeTabGroup are not expected to save groups. // if removing this group closes a window, we need to tell the window // not to save the group.
group.saveOnWindowClose = false;
}
// This needs to be fired before tabs are removed because session store // needs to respond to this while tabs are still part of the group
group.dispatchEvent( new CustomEvent("TabGroupRemoveRequested", {
bubbles: true,
detail: {
skipSessionStore: options.skipSessionStore,
isUserTriggered: options.isUserTriggered,
telemetrySource: options.telemetrySource,
},
})
);
// Skip session store on a per-tab basis since these tabs will get // recorded as part of a group
options.skipSessionStore = true;
// tell removeTabs not to subprocess groups since we're removing a group.
options.skipGroupCheck = true;
let oldSelectedTab =
selectTab && group.documentGlobal.gBrowser.selectedTab;
let newTabs = [];
let adoptedTab;
let splitview;
// bug1969925 adopting a tab group will cause the window to close if it // is the only thing on the tab strip // In this case, the `TabUngrouped` event will not fire, so we have to do it manually
let noOtherTabsInWindow =
group.documentGlobal.gBrowser.nonHiddenTabs.every(
t => t.group == group
);
// We dispatch this event in a separate for loop because the tab extension API // expects event.detail to be a tab. if (noOtherTabsInWindow) { for (let element of group.tabs) {
group.dispatchEvent( new CustomEvent("TabUngrouped", {
bubbles: true,
detail: element,
})
);
}
}
for (let element of group.tabsAndSplitViews) { if (element.tagName == "tab-split-view-wrapper") {
splitview = this.adoptSplitView(element, {
elementIndex,
tabIndex,
});
newTabs.push(splitview);
tabIndex = splitview.tabs[0]._tPos + splitview.tabs.length;
} else {
adoptedTab = this.adoptTab(element, {
elementIndex,
tabIndex,
selectTab: element === oldSelectedTab,
});
newTabs.push(adoptedTab); // Put next tab after current one.
elementIndex = undefined;
tabIndex = adoptedTab._tPos + 1;
}
}
let oldSelectedTab =
selectTab && container.documentGlobal.gBrowser.selectedTab;
let newTabs = [];
// When tabs are adopted across windows, they exit the tab split of the // source window, moved to the new window, and finally moved into a split // view in the new window. Although the splitViewId stays effectively the // same, the TabMove event fire a few times for these transitions. // To reduce noise in extension API events, we temporarily flag these // tabs to allow ext-tabs.js to filter out such TabMove events. for (let tab of container.tabs) {
tab.removedByAdoption = true;
let adoptedTab = this.adoptTab(tab, {
selectTab: tab === oldSelectedTab,
tabIndex,
elementIndex,
});
adoptedTab.addedByAdoption = true;
newTabs.push(adoptedTab); // Put next tab after current one.
elementIndex = undefined;
tabIndex = adoptedTab._tPos + 1;
}
getTabGroupById(id) { for (const win of BrowserWindowTracker.getOrderedWindows({ private: PrivateBrowsingUtils.isWindowPrivate(window),
})) { for (const group of win.gBrowser.tabGroups) { if (group.id === id) { return group;
}
}
} returnnull;
}
_determineURIToLoad(uriString, createLazyBrowser) {
uriString = uriString || "about:blank";
let aURIObject = null; try {
aURIObject = Services.io.newURI(uriString);
} catch (ex) { /* we'll try to fix up this URL later */
}
/** *@param{object}options *@returns{MozTabbrowserTab}
*/
_createTab({
uriString,
userContextId,
openerTab,
pinned,
noInitialLabel,
skipBackgroundNotify,
animate,
}) { var t = document.createXULElement("tab", { is: "tabbrowser-tab" }); // Tag the tab as being created so extension code can ignore events // prior to TabOpen.
t.initializingTab = true;
t.openerTab = openerTab;
// Related tab inherits current tab's user context unless a different // usercontextid is specified if (userContextId == null && openerTab) {
userContextId = openerTab.getAttribute("usercontextid") || 0;
}
if (!noInitialLabel) { if (isBlankPageURL(uriString)) {
t.setAttribute("label", this.tabContainer.emptyTabTitle);
} else { // Set URL as label so that the tab isn't empty initially. this.setInitialTabTitle(t, uriString, {
beforeTabOpen: true,
isURL: true,
});
}
}
if (userContextId) {
t.setAttribute("usercontextid", userContextId);
ContextualIdentityService.setTabStyle(t);
}
if (skipBackgroundNotify) {
t.setAttribute("skipbackgroundnotify", true);
}
if (pinned) {
t.setAttribute("pinned", "true");
}
t.classList.add("tabbrowser-tab");
this.tabContainer._unlockTabSizing();
if (!animate) {
UserInteraction.update("browser.tabs.opening", "not-animated", window);
t.setAttribute("fadein", "true");
// Call _handleNewTab asynchronously as it needs to know if the // new tab is selected.
setTimeout( function (tabContainer) {
tabContainer._handleNewTab(t);
}, 0, this.tabContainer
);
} else {
UserInteraction.update("browser.tabs.opening", "animated", window);
}
return t;
}
/** * *@param{object}options *@param{nsIPrincipal}[options.originPrincipal] *IfuriStringisgiven,urimightinheritprincipals,andnopreloadedbrowserisused, *thisistheoriginprincipaltobeinheritedbytheinitialabout:blank. *@param{nsIPrincipal}[options.originStoragePrincipal] *IfuriStringisgiven,urimightinheritprincipals,andnopreloadedbrowserisused, *thisistheoriginstorageprincipaltobeinheritedbytheinitialabout:blank.
*/
_createBrowserForTab(
tab,
{
uriString,
uri,
name,
preferredRemoteType,
openerBrowser,
uriIsAboutBlank,
referrerInfo,
forceNotRemote,
initialBrowsingContextGroupId,
openWindowInfo,
skipLoad,
triggeringRemoteType,
}
) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and // we have a remote triggering remote type, use that instead. if (!preferredRemoteType && triggeringRemoteType) {
preferredRemoteType = triggeringRemoteType;
}
// If we don't have a preferred remote type, and we have a remote // opener, use the opener's remote type. if (!preferredRemoteType && openerBrowser) {
preferredRemoteType = openerBrowser.remoteType;
}
let { userContextId } = tab;
// If URI is about:blank and we don't have a preferred remote type, // then we need to use the referrer, if we have one, to get the // correct remote type for the new tab. if (
uriIsAboutBlank &&
!preferredRemoteType &&
referrerInfo &&
referrerInfo.originalReferrer
) {
preferredRemoteType = ChromeUtils.predictRemoteTypeForURI(
referrerInfo.originalReferrer,
{ window, userContextId }
);
}
let b,
usingPreloadedContent = false; // If we open a new tab with the newtab URL in the default // userContext, check if there is a preloaded browser ready. if (uriString == BROWSER_NEW_TAB_URL && !userContextId) {
b = NewTabPagePreloading.getPreloadedBrowser(window); if (b) {
usingPreloadedContent = true;
}
}
if (!b) { // No preloaded browser found, create one.
b = this.createBrowser({
remoteType,
uriIsAboutBlank,
userContextId,
initialBrowsingContextGroupId,
openWindowInfo,
name,
skipLoad,
});
}
// Hack to ensure that the about:newtab, and about:welcome favicon is loaded // instantaneously, to avoid flickering and improve perceived performance. this.setDefaultIcon(tab, uri);
return { browser: b, usingPreloadedContent };
}
_kickOffBrowserLoad(
browser,
{
uri,
uriString,
usingPreloadedContent,
triggeringPrincipal,
originPrincipal,
originStoragePrincipal,
uriIsAboutBlank,
allowInheritPrincipal,
allowThirdPartyFixup,
fromExternal,
forceAllowDataURI,
isCaptivePortalTab,
skipLoad,
referrerInfo,
charset,
postData,
policyContainer,
globalHistoryOptions,
triggeringRemoteType,
schemelessInput,
hasValidUserGestureActivation,
textDirectiveUserActivation,
}
) { const shouldInheritSecurityContext = (() => { if (
!usingPreloadedContent &&
originPrincipal &&
originStoragePrincipal &&
uriString
) {
let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; // Unless we know for sure we're not inheriting principals, // force the about:blank viewer to have the right principal: if (!uri || doGetProtocolFlags(uri) & URI_INHERITS_SECURITY_CONTEXT) { returntrue;
}
} returnfalse;
})();
if (shouldInheritSecurityContext) {
browser.createAboutBlankDocumentViewer(
originPrincipal,
originStoragePrincipal
);
}
// If we didn't swap docShells with a preloaded browser // then let's just continue loading the page normally. if (
!usingPreloadedContent &&
(!uriIsAboutBlank || !allowInheritPrincipal) &&
!skipLoad
) { // pretend the user typed this so it'll be available till // the document successfully loads if (uriString && !gInitialPages.includes(uriString)) {
browser.userTypedValue = uriString;
}
let loadFlags = LOAD_FLAGS_NONE; if (allowThirdPartyFixup) {
loadFlags |=
LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
} if (fromExternal) {
loadFlags |= LOAD_FLAGS_FROM_EXTERNAL;
} elseif (!triggeringPrincipal.isSystemPrincipal) { // XXX this code must be reviewed and changed when bug 1616353 // lands. // The purpose of LOAD_FLAGS_FIRST_LOAD is to close a new // tab if it turns out to be a download.
loadFlags |= LOAD_FLAGS_FIRST_LOAD;
} if (!allowInheritPrincipal) {
loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
} if (isCaptivePortalTab) {
loadFlags |= LOAD_FLAGS_DISABLE_TRR;
} if (forceAllowDataURI) {
loadFlags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
} try {
browser.fixupAndLoadURIString(uriString, {
loadFlags,
triggeringPrincipal,
referrerInfo,
charset,
postData,
policyContainer,
globalHistoryOptions,
triggeringRemoteType,
schemelessInput,
hasValidUserGestureActivation,
textDirectiveUserActivation,
isCaptivePortalTab,
});
} catch (ex) {
console.error(ex);
}
}
}
/** *@param{boolean}restoreTabsLazily *@param{number}selectTabseeSessionStoreInternal.restoreTabs{aSelectTab} *@param{TabStateData[]}tabDataList *@param{TabGroupStateData[]}tabGroupDataList *@param{TabSplitViewStateData[]}splitViewDataList *@returns{MozTabbrowserTab[]}
*/
createTabsForSessionRestore(
restoreTabsLazily,
selectTab,
tabDataList,
tabGroupDataList,
splitViewDataList = []
) {
let tabs = [];
let tabsFragment = document.createDocumentFragment();
let tabToSelect = null;
let hiddenTabs = new Map(); /** @type {Map<TabGroupStateData['id'], TabGroupWorkingData>} */
let tabGroupWorkingData = new Map(); /** @type {Map<TabSplitViewStateData['id'], SplitViewWorkingData>} */
let splitViewWorkingData = new Map();
for (const tabGroupData of tabGroupDataList) {
tabGroupWorkingData.set(tabGroupData.id, {
stateData: tabGroupData,
node: undefined,
containingTabsFragment: document.createDocumentFragment(),
});
} for (const splitViewData of splitViewDataList) {
splitViewWorkingData.set(splitViewData.id, {
numberOfTabs: splitViewData.numberOfTabs,
node: undefined,
tabs: [],
});
}
// We create each tab and browser, but only insert them // into a document fragment so that we can insert them all // together. This prevents synch reflow for each tab // insertion. for (var i = 0; i < tabDataList.length; i++) {
let tabData = tabDataList[i];
let userContextId = tabData.userContextId;
let select = i == selectTab - 1;
let tab;
let tabWasReused = false;
// Re-use existing selected tab if possible to avoid the overhead of // selecting a new tab. For now, we only do this for horizontal tabs; // we'll let tabs.js handle pinning for vertical tabs until we unify // the logic for both horizontal and vertical tabs in bug 1910097. if (
select && this.selectedTab.userContextId == userContextId &&
!SessionStore.isTabRestoring(this.selectedTab) &&
!this.tabContainer.verticalMode
) {
tabWasReused = true;
tab = this.selectedTab; if (!tabData.pinned) { this.unpinTab(tab);
} else { this.pinTab(tab);
}
}
// Add a new tab if needed. if (!tab) {
let createLazyBrowser =
restoreTabsLazily && !select && !tabData.pinned;
let url = "about:blank"; if (tabData.entries?.length) {
let activeIndex = (tabData.index || tabData.entries.length) - 1; // Ensure the index is in bounds.
activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
activeIndex = Math.max(activeIndex, 0);
url = tabData.entries[activeIndex].url;
}
let preferredRemoteType = ChromeUtils.predictRemoteTypeForURI(url, {
window,
userContextId,
});
// If we're creating a lazy browser, let tabbrowser know the future // URI because progress listeners won't get onLocationChange // notification before the browser is inserted. // // Setting noInitialLabel is a perf optimization. Rendering tab labels // would make resizing the tabs more expensive as we're adding them. // Each tab will get its initial label set in restoreTab.
tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", {
createLazyBrowser,
skipAnimation: true,
noInitialLabel: true,
userContextId,
skipBackgroundNotify: true,
bulkOrderedOpen: true,
insertTab: false,
skipLoad: true,
preferredRemoteType,
});
if (select) {
tabToSelect = tab;
}
}
let splitView =
tabData.splitViewId && splitViewWorkingData.get(tabData.splitViewId); if (splitView) {
splitView.tabs.push(tab); // Only create the split view when we've got the last tab it would contain if (splitView.tabs.length == splitView.numberOfTabs) {
splitView.node = this._createTabSplitView(tabData.splitViewId);
}
}
tabs.push(tab);
if (tabData.pinned) { this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {});
} elseif (tabData.groupId) {
let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group // processing
if (tabGroup) { if (!splitView) {
tabGroup.containingTabsFragment.appendChild(tab);
} elseif (splitView?.node) {
tabGroup.containingTabsFragment.appendChild(splitView.node);
}
// if this is the first time encountering a tab group, create its // DOM node once and place it in the tabs bar fragment if (!tabGroup.node) {
tabGroup.node = this._createTabGroup(
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
tabGroup.stateData.name
);
tabsFragment.appendChild(tabGroup.node);
}
}
} else { if (tab.hidden) {
tab.hidden = true;
hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy);
}
if (!splitView) {
tabsFragment.appendChild(tab);
} elseif (splitView?.node) {
tabsFragment.appendChild(splitView.node);
}
if (tabWasReused) { this.tabContainer._invalidateCachedTabs();
}
}
tab.initialize();
}
// inject the top-level tab and tab group DOM nodes this.tabContainer.appendChild(tabsFragment);
// inject tab DOM nodes into the now-connected tab group and split view DOM nodes for (const tabGroup of tabGroupWorkingData.values()) { if (tabGroup.node) {
tabGroup.node.appendChild(tabGroup.containingTabsFragment);
}
} for (const splitView of splitViewWorkingData.values()) { if (splitView.node) {
splitView.node.addTabs(splitView.tabs, { isSessionRestore: true });
}
}
for (let [tab, hiddenBy] of hiddenTabs) {
let event = document.createEvent("Events");
event.initEvent("TabHide", true, false);
tab.dispatchEvent(event); if (hiddenBy) {
SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy);
}
}
this.tabContainer._invalidateCachedTabs();
// We need to wait until after all tabs have been appended to the DOM // to remove the old selected tab. if (tabToSelect) {
let leftoverTab = this.selectedTab; this.selectedTab = tabToSelect; this.removeTab(leftoverTab);
}
if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert();
TabBarVisibility.update();
for (let tab of tabs) { // If tabToSelect is a tab, we didn't reuse the selected tab. if (tabToSelect || !tab.selected) { // Fire a TabOpen event for all unpinned tabs, except reused selected // tabs. if (!tab.pinned) { this._fireTabOpen(tab, {});
}
// Fire a TabBrowserInserted event on all tabs that have a connected, // real browser, except for reused selected tabs. if (tab.linkedPanel) { var evt = new CustomEvent("TabBrowserInserted", {
bubbles: true,
detail: { insertedOnTabCreation: true },
});
tab.dispatchEvent(evt);
}
}
}
}
return tabs;
}
moveTabsToStart(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab]; // Walk the array in reverse order so the tabs are kept in order. for (let i = tabs.length - 1; i >= 0; i--) { this.moveTabToStart(tabs[i]);
}
}
moveTabsToEnd(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab]; for (let tab of tabs) { this.moveTabToEnd(tab);
}
}
warnAboutClosingTabs(tabsToClose, aCloseTabs) { // We want to warn about closing duplicates even if there was only a // single duplicate, so we intentionally place this above the check for // tabsToClose <= 1. const shownDupeDialogPref = "browser.tabs.haveShownCloseAllDuplicateTabsWarning"; var ps = Services.prompt; if (
aCloseTabs == this.closingTabsEnum.ALL_DUPLICATES &&
!Services.prefs.getBoolPref(shownDupeDialogPref, false)
) { // The first time a user closes all duplicate tabs, tell them what will // happen and give them a chance to back away.
Services.prefs.setBoolPref(shownDupeDialogPref, true);
// Our prompt to close this window is most important, so replace others.
gDialogBox.replaceDialogIfOpen();
// default to true: if it were false, we wouldn't get this far var warnOnClose = { value: true };
// focus the window before prompting. // this will raise any minimized window, which will // make it obvious which window the prompt is for and will // solve the problem of windows "obscuring" the prompt. // see bug #350299 for more details
window.focus(); const [title, button, checkbox] = this.tabLocalization.formatValuesSync([
{
id: "tabbrowser-confirm-close-tabs-title",
args: { tabCount: tabsToClose },
},
{ id: "tabbrowser-confirm-close-tabs-button" },
{ id: "tabbrowser-ask-close-tabs-checkbox" },
]);
let flags =
ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1;
let checkboxLabel =
aCloseTabs == this.closingTabsEnum.ALL ? checkbox : null; var buttonPressed = ps.confirmEx(
window,
title, null,
flags,
button, null, null,
checkboxLabel,
warnOnClose
);
var reallyClose = buttonPressed == 0;
// don't set the pref unless they press OK and it's false if (
aCloseTabs == this.closingTabsEnum.ALL &&
reallyClose &&
!warnOnClose.value
) {
Services.prefs.setBoolPref(pref, false);
}
return reallyClose;
}
/** *ThisdetermineswherethetabshouldbeinsertedwithinthetabContainer, *andinsertsit. * *@param{MozTabbrowserTab}tab *@param{object}[options] *@param{number}[options.elementIndex] *@param{number}[options.tabIndex] *@param{MozTabbrowserTabGroup}[options.tabGroup] *Arelatedtabgroupwherethistabshouldbeadded,whenapplicable.
*/
#insertTabAtIndex(
tab,
{
tabIndex,
elementIndex,
ownerTab,
openerTab,
pinned,
bulkOrderedOpen,
tabGroup,
} = {}
) { // If this new tab is owned by another, assert that relationship if (ownerTab) {
tab.owner = ownerTab;
}
// Ensure we have an index if one was not provided. if (typeof elementIndex != "number" && typeof tabIndex != "number") { // Move the new tab after another tab if needed, to the end otherwise.
elementIndex = Infinity; if (
!bulkOrderedOpen &&
((openerTab &&
Services.prefs.getBoolPref( "browser.tabs.insertRelatedAfterCurrent"
)) ||
Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent"))
) {
let lastRelatedTab =
openerTab && this.#lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab; if (!tabGroup) {
tabGroup = previousTab.group;
} if (
Services.prefs.getBoolPref( "browser.tabs.insertAfterCurrentExceptPinned"
) &&
previousTab.pinned
) {
elementIndex = Infinity;
} elseif (previousTab.visible && previousTab.splitview) {
elementIndex = this.tabContainer.dragAndDropElements.indexOf(
previousTab.splitview
) + 1;
} elseif (previousTab.visible) {
elementIndex = previousTab.elementIndex + 1;
} elseif (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
if (lastRelatedTab) {
lastRelatedTab.owner = null;
} elseif (openerTab) {
tab.owner = openerTab;
} // Always set related map if opener exists. if (openerTab) { this.#lastRelatedTabMap.set(openerTab, tab);
}
}
}
let allItems;
let index; if (typeof elementIndex == "number") {
allItems = this.tabContainer.dragAndDropElements;
index = elementIndex;
} else {
allItems = this.tabs;
index = tabIndex;
} // Ensure index is within bounds. if (tab.pinned) {
index = Math.max(index, 0);
index = Math.min(index, this.pinnedTabCount);
} else {
index = Math.max(index, this.pinnedTabCount);
index = Math.min(index, allItems.length);
} /** @type {MozTabbrowserTab|undefined} */
let itemAfter = allItems.at(index);
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
} elseif (itemAfter?.splitview) {
let splitview = itemAfter.splitview;
itemAfter =
itemAfter === splitview.tabs[0]
? splitview
: splitview.nextElementSibling || null;
} // Prevent a flash of unstyled content by setting up the tab content // and inherited attributes before appending it (see Bug 1592054):
tab.initialize();
this.tabContainer._invalidateCachedTabs();
if (tabGroup) { if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) || this.isSplitViewWrapper(itemAfter)
) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter);
} else { // Place tab at the end of the contextual tab group because one of: // 1) no `itemAfter` so `tab` should be the last tab in the tab strip // 2) `itemAfter` is in a different tab group
tabGroup.appendChild(tab);
}
} elseif (
(this.isTab(itemAfter) && itemAfter.group?.tabs[0] == itemAfter) || this.isTabGroupLabel(itemAfter)
) { // If there is ambiguity around whether or not a tab should be inserted // into a group (i.e. because the new tab is being inserted on the // edges of the group), prefer not to insert the tab into the group. // // We only need to handle the case where the tab is being inserted at // the starting boundary of a group because `insertBefore` called on // the tab just after a tab group will not add it to the group by // default. this.tabContainer.insertBefore(tab, itemAfter.group);
} else { // Place ungrouped tab before `itemAfter` by default const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
tabContainer.insertBefore(tab, itemAfter);
}
if (tab.group?.collapsed) { // Bug 1997096: automatically expand the group if we are adding a new // tab to a collapsed group, and that tab does not have automatic focus // (i.e. if the user right clicks and clicks "Open in New Tab")
tab.group.collapsed = false;
}
this._updateTabsAfterInsert();
if (pinned) { this._updateTabBarForPinnedTabs();
}
/** *@param{MozTabbrowserTab}aTab *@returns{MozTabbrowserTab[]}
*/
_getTabsToTheStartFrom(aTab) {
let tabsToStart = []; if (!aTab.visible) { return tabsToStart;
}
let tabs = this.openTabs; for (let i = 0; i < tabs.length; ++i) { if (tabs[i] == aTab) { break;
} // Ignore pinned and hidden tabs. if (tabs[i].pinned || tabs[i].hidden) { continue;
} // In a multi-select context, select all unselected tabs // starting from the context tab. if (aTab.multiselected && tabs[i].multiselected) { continue;
}
tabsToStart.push(tabs[i]);
} return tabsToStart;
}
/** *@param{MozTabbrowserTab}aTab *@returns{MozTabbrowserTab[]}
*/
_getTabsToTheEndFrom(aTab) {
let tabsToEnd = []; if (!aTab.visible) { return tabsToEnd;
}
let tabs = this.openTabs; for (let i = tabs.length - 1; i >= 0; --i) { if (tabs[i] == aTab) { break;
} // Ignore pinned and hidden tabs. if (tabs[i].pinned || tabs[i].hidden) { continue;
} // In a multi-select context, select all unselected tabs // starting from the context tab. if (aTab.multiselected && tabs[i].multiselected) { continue;
}
tabsToEnd.push(tabs[i]);
} return tabsToEnd;
}
getDuplicateTabsToClose(aTab) { // One would think that a set is better, but it would need to copy all // the strings instead of just keeping references to the nsIURI objects, // and the array is presumed to be small anyways.
let keys = [];
let keyForTab = tab => {
let uri = tab.linkedBrowser?.currentURI; if (!uri) { returnnull;
} return {
uri,
userContextId: tab.userContextId,
};
};
let keyEquals = (a, b) => { return a.userContextId == b.userContextId && a.uri.equals(b.uri);
}; if (aTab.multiselected) { for (let tab of this.selectedTabs) {
let key = keyForTab(tab); if (key) {
keys.push(key);
}
}
} else {
let key = keyForTab(aTab); if (key) {
keys.push(key);
}
}
if (!keys.length) { return [];
}
let duplicateTabs = []; for (let tab of this.tabs) { if (tab == aTab || tab.pinned) { continue;
} if (aTab.multiselected && tab.multiselected) { continue;
}
let key = keyForTab(tab); if (key && keys.some(k => keyEquals(k, key))) {
duplicateTabs.push(tab);
}
}
return duplicateTabs;
}
getAllDuplicateTabsToClose() {
let lastSeenTabs = this.tabs.toSorted(
(a, b) => b.lastSeenActive - a.lastSeenActive
);
let duplicateTabs = []; /** @type {Map<string, Set<number>>} */
let userContextIdsPerUri = new Map(); for (let tab of lastSeenTabs) { const uri = tab.linkedBrowser?.currentURI; if (!uri) { // Can't tell if it's a duplicate without a URI. // Safest to leave it be. continue;
}
let userContextIds = userContextIdsPerUri.getOrInsertComputed(
uri.spec,
() => new Set()
);
let userContextId = tab.userContextId; if (!tab.pinned && userContextIds.has(userContextId)) {
duplicateTabs.push(tab);
}
userContextIds.add(userContextId);
} return duplicateTabs;
}
removeAllDuplicateTabs() { // I would like to have the caller provide this target, // but the caller lives in a different document.
let alltabsButton = document.getElementById("alltabs-button"); this._removeDuplicateTabs(
alltabsButton, this.getAllDuplicateTabsToClose(), this.closingTabsEnum.ALL_DUPLICATES
);
}
/** *Inamulti-selectcontext,thetabs(exceptpinnedtabs)thatarelocatedtothe *leftoftheleftmostselectedtabwillberemoved.
*/
removeTabsToTheStartFrom(aTab, options) {
let tabs = this._getTabsToTheStartFrom(aTab); if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
) { return;
}
this.removeTabs(tabs, options);
}
/** *Inamulti-selectcontext,thetabs(exceptpinnedtabs)thatarelocatedtothe *rightoftherightmostselectedtabwillberemoved.
*/
removeTabsToTheEndFrom(aTab, options) {
let tabs = this._getTabsToTheEndFrom(aTab); if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
) { return;
}
/** @type {function(MozTabbrowserTab):boolean} */
let filterFn;
// If enabled also filter by selected or pinned state. if (skipPinnedOrSelectedTabs) { if (aTab?.multiselected) {
filterFn = tab => !tab.multiselected && !tab.pinned && !tab.hidden;
} else {
filterFn = tab => tab != aTab && !tab.pinned && !tab.hidden;
}
} else { // Exclude just aTab from being removed.
filterFn = tab => tab != aTab;
}
let tabsToRemove = this.openTabs.filter(filterFn);
// If enabled show the tab close warning. if (
!skipWarnAboutClosingTabs &&
!this.warnAboutClosingTabs(
tabsToRemove.length, this.closingTabsEnum.OTHER
)
) { return;
}
/** *StartstoremovetabsfromtheUI:checkingforbeforeunloadhandlers, *closingtabswherepossibleandtriggeringrunningoftheunloadhandlers. * *@param{object[]}tabs *Thesetoftabstoremove. *@param{object}options *@param{boolean}options.animate *Whetherornottoanimateclosing. *@param{boolean}options.suppressWarnAboutClosingWindow *Thiswillsuppressthewarningaboutclosingawindowwiththelasttab. *@param{boolean}options.skipPermitUnload *Skipsthebeforeunloadchecksforthetabs.Onlysetthistotruewhen *usingitintandemwith`runBeforeUnloadForTabs`. *@param{boolean}options.skipRemoves *Skipsactuallyremovingthetabs.Thebeforeunloadhandlersstillrun. *@param{boolean}options.skipSessionStore *Iftrue,don'trecordtheclosedtabsinSessionStore. *@returns{_startRemoveTabsReturnValue}
*/
_startRemoveTabs(
tabs,
{
animate, // See bug 1883051 // eslint-disable-next-line no-unused-vars
suppressWarnAboutClosingWindow,
skipPermitUnload,
skipRemoves,
skipSessionStore,
isUserTriggered,
telemetrySource,
}
) { // Note: if you change any of the unload algorithm, consider also // changing `runBeforeUnloadForTabs` above. /** @type {MozTabbrowserTab[]} */
let tabsWithBeforeUnloadPrompt = []; /** @type {MozTabbrowserTab[]} */
let tabsWithoutBeforeUnload = []; /** @type {Promise<void>[]} */
let beforeUnloadPromises = []; /** @type {MozTabbrowserTab|undefined} */
let lastToClose;
for (let tab of tabs) { if (!skipRemoves) {
tab._closedInMultiselection = true;
} if (!skipRemoves && tab.selected) {
lastToClose = tab;
let toBlurTo = this._findTabToBlurTo(lastToClose, tabs); if (toBlurTo) { this._getSwitcher().warmupTab(toBlurTo);
}
} elseif (!skipPermitUnload && this._hasBeforeUnload(tab)) {
let timerId = Glean.browserTabclose.permitUnloadTime.start(); // We need to block while calling permitUnload() because it // processes the event queue and may lead to another removeTab() // call before permitUnload() returns.
tab._pendingPermitUnload = true;
beforeUnloadPromises.push( // To save time, we first run the beforeunload event listeners in all // content processes in parallel. Tabs that would have shown a prompt // will be handled again later.
tab.linkedBrowser.asyncPermitUnload("dontUnload").then(
({ permitUnload }) => {
tab._pendingPermitUnload = false;
Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(
timerId
); if (tab.closing) { // The tab was closed by the user while we were in permitUnload, don't // attempt to close it a second time.
} elseif (permitUnload) { if (!skipRemoves) { // OK to close without prompting, do it immediately. this.removeTab(tab, {
animate,
prewarmed: true,
skipPermitUnload: true,
skipSessionStore,
});
}
} else { // We will need to prompt, queue it so it happens sequentially.
tabsWithBeforeUnloadPrompt.push(tab);
}
},
err => {
console.error("error while calling asyncPermitUnload", err);
tab._pendingPermitUnload = false;
Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(
timerId
);
}
)
);
} else {
tabsWithoutBeforeUnload.push(tab);
}
}
// Now that all the beforeunload IPCs have been sent to content processes, // we can queue unload messages for all the tabs without beforeunload listeners. // Doing this first would cause content process main threads to be busy and delay // beforeunload responses, which would be user-visible. if (!skipRemoves) { for (let tab of tabsWithoutBeforeUnload) { this.removeTab(tab, {
animate,
prewarmed: true,
skipPermitUnload,
skipSessionStore,
isUserTriggered,
telemetrySource,
});
}
}
// Now run again sequentially the beforeunload listeners that will result in a prompt. for (let tab of tabsWithBeforeUnloadPrompt) {
tab._pendingPermitUnload = true;
let { permitUnload } = this.getBrowserForTab(tab).permitUnload();
tab._pendingPermitUnload = false; if (!permitUnload) { returntrue;
}
}
} catch (e) {
console.error(e);
} returnfalse;
}
/** *Givenanarrayoftabs,returnsatuple[groups,leftoverTabs]suchthat: *-groupscontainsallgroupswhosetabsareasubsetoftheinitialarray *-leftoverTabscontainstheremainingtabs * *@param{Array}tabslistoftabs *@returns{Array}atuplewherethefirstelementisanarrayofgroups *andthesecondisanarrayoftabs
*/
#separateWholeGroups(tabs) { /** *Mapoftabgrouptosurvivingtabsinthegroup. *Ifanyofthe`tabs`toberemovedbelongtoatabgroup,keeptrack *ofhowmanytabsinthetabgroupwillbeleftafterremoving`tabs`. *Foranytabgroupwith0survivingtabs,wecanknowthatthattab *groupwillberemovedasaconsequenceofremovingthese`tabs`. * *@type{Map<MozTabbrowserTabGroup,Set<MozTabbrowserTab>>}
*/
let tabGroupSurvivingTabs = new Map();
let wholeGroups = []; for (let tab of tabs) { if (tab.group) { if (!tabGroupSurvivingTabs.has(tab.group)) {
tabGroupSurvivingTabs.set(tab.group, new Set(tab.group.tabs));
}
tabGroupSurvivingTabs.get(tab.group).delete(tab);
}
}
for (let [tabGroup, survivingTabs] of tabGroupSurvivingTabs.entries()) { if (!survivingTabs.size) {
wholeGroups.push(tabGroup);
tabs = tabs.filter(t => !tabGroup.tabs.includes(t));
}
}
return [wholeGroups, tabs];
}
/** *Removesmultipletabsfromthetabbrowser. * *@param{MozTabbrowserTab[]}tabs *Thesetoftabstoremove. *@param{object}[options] *@param{boolean}[options.animate] *Whetherornottoanimateclosing,defaultstotrue. *@param{boolean}[options.suppressWarnAboutClosingWindow] *Thiswillsuppressthewarningaboutclosingawindowwiththelasttab. *@param{boolean}[options.skipPermitUnload] *Skipsthebeforeunloadchecksforthetabs.Onlysetthistotruewhen *usingitintandemwith`runBeforeUnloadForTabs`. *@param{boolean}[options.skipSessionStore] *Iftrue,don'trecordtheclosedtabsinSessionStore. *@param{boolean}[options.skipGroupCheck] *Skipseparateprocessingofwholetabgroupsfromthesetoftabs. *UsedbyremoveTabGroup. *@param{boolean}[options.isUserTriggered] *Whetherornottheremovalisthedirectresultofauseraction. *Usedfortelemetry. *@param{string}[options.telemetrySource] *Thesystem,surface,orcontroltheuserusedtotakethisaction. *@seeTabMetrics.METRIC_SOURCEforpossiblevalues.
*/
removeTabs(
tabs,
{
animate = true,
suppressWarnAboutClosingWindow = false,
skipPermitUnload = false,
skipSessionStore = false,
skipGroupCheck = false,
isUserTriggered = false,
telemetrySource,
} = {}
) { // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( this.tabs.length == tabs.length &&
Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
) {
window.closeWindow( true,
suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow, "close-last-tab"
); return;
}
if (!skipSessionStore) {
SessionStore.resetLastClosedTabCount(window);
} this.#clearMultiSelectionLocked = true;
// Guarantee that #clearMultiSelectionLocked lock gets released. try { // If selection includes entire groups, we might want to save them if (!skipGroupCheck) {
let [groups, leftoverTabs] = this.#separateWholeGroups(tabs);
groups.forEach(group => { if (!skipSessionStore) {
group.save();
} this.removeTabGroup(group, {
animate,
skipSessionStore,
skipPermitUnload,
isUserTriggered,
telemetrySource,
});
});
tabs = leftoverTabs;
}
// Wait for all the beforeunload events to have been processed by content processes. // The permitUnload() promise will, alas, not call its resolution // callbacks after the browser window the promise lives in has closed, // so we have to check for that case explicitly.
let done = false;
beforeUnloadComplete.then(() => {
done = true;
});
Services.tm.spinEventLoopUntilOrQuit( "tabbrowser.js:removeTabs",
() => done || window.closed
); if (!done) { return;
}
// Now run again sequentially the beforeunload listeners that will result in a prompt. for (let tab of tabsWithBeforeUnloadPrompt) { this.removeTab(tab, aParams); if (!tab.closing) { // If we abort the closing of the tab.
tab._closedInMultiselection = false;
}
}
// Avoid changing the selected browser several times by removing it, // if appropriate, lastly. if (lastToClose) { this.removeTab(lastToClose, aParams);
}
} catch (e) {
console.error(e);
}
// Telemetry stopwatches may already be running if removeTab gets // called again for an already closing tab. if (!aTab._closeTimeAnimTimerId && !aTab._closeTimeNoAnimTimerId) { // Speculatevely start both stopwatches now. We'll cancel one of // the two later depending on whether we're animating.
aTab._closeTimeAnimTimerId = Glean.browserTabclose.timeAnim.start();
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
// Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { this._endRemoveTab(aTab); return;
}
let isVisibleTab = aTab.visible; // We have to sample the tab width now, since _beginRemoveTab might // end up modifying the DOM in such a way that aTab gets a new // frame created for it (for example, by updating the visually selected // state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab); if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
skipPermitUnload,
closeWindowWithLastTab,
prewarmed,
skipSessionStore,
isUserTriggered,
telemetrySource,
})
) {
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId);
aTab._closeTimeNoAnimTimerId = null; return;
}
if (
!animate /* the caller didn't opt in */ ||
gReduceMotion ||
isLastTab ||
aTab.pinned ||
!isVisibleTab || this.tabContainer.verticalMode || this._removingTabs.size > 3/* don't want lots of concurrent animations */ ||
!aTab.hasAttribute( "fadein"
) /* fade-in transition hasn't been triggered yet */ ||
tabWidth == 0/* fade-in transition hasn't moved yet */
) { // We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null; this._endRemoveTab(aTab); return;
}
// We're animating, so we can cancel the non-animation stopwatch.
Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId);
aTab._closeTimeNoAnimTimerId = null;
var browser = this.getBrowserForTab(aTab); if (
!skipPermitUnload &&
!adoptedByTab &&
aTab.linkedPanel &&
!aTab._pendingPermitUnload &&
(!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
) { if (!prewarmed) {
let blurTab = this._findTabToBlurTo(aTab); if (blurTab) { this.warmupTab(blurTab);
}
}
let timerId = Glean.browserTabclose.permitUnloadTime.start();
// We need to block while calling permitUnload() because it // processes the event queue and may lead to another removeTab() // call before permitUnload() returns.
aTab._pendingPermitUnload = true;
let { permitUnload } = browser.permitUnload();
aTab._pendingPermitUnload = false;
// If we were closed during onbeforeunload, we return false now // so we don't (try to) close the same tab again. Of course, we // also stop if the unload was cancelled by the user: if (aTab.closing || !permitUnload) { returnfalse;
}
}
this.tabContainer._invalidateCachedVisibleTabs();
// this._switcher would normally cover removing a tab from this // cache, but we may not have one at this time.
let tabCacheIndex = this._tabLayerCache.indexOf(aTab); if (tabCacheIndex != -1) { this._tabLayerCache.splice(tabCacheIndex, 1);
}
// Delay hiding the the active tab if we're screen sharing. // See Bug 1642747.
let screenShareInActiveTab =
aTab == this.selectedTab && browser._sharingState?.webRTC?.screen;
if (!screenShareInActiveTab) { this._blurTab(aTab);
}
var closeWindow = false; var newTab = false; if (this.#isLastTabInWindow(aTab)) {
closeWindow =
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, // so avoid calling it again:
window.skipNextCanClose = true;
}
// Closing the tab and replacing it with a blank one is notably slower // than closing the window right away. If the caller opts in, take // the fast path. if (closeWindow && closeWindowFastpath && !this._removingTabs.size) { // This call actually closes the window, unless the user // cancels the operation. We are finished here in both cases. this.#windowIsClosing = window.closeWindow( true,
window.warnAboutClosingWindow, "close-last-tab"
); returnfalse;
}
// swapBrowsersAndCloseOther will take care of closing the window without animation. if (closeWindow && adoptedByTab) { // Remove the tab's filter and progress listener to avoid leaking. if (aTab.linkedPanel) { const filter = this.#tabFilters.get(aTab);
browser.webProgress.removeProgressListener(filter); const listener = this.#tabListeners.get(aTab);
filter.removeProgressListener(listener);
listener.destroy(); this.#tabListeners.delete(aTab); this.#tabFilters.delete(aTab);
} returntrue;
}
if (!aTab._fullyOpen) { // If the opening tab animation hasn't finished before we start closing the // tab, decrement the animation count since _handleNewTab will not get called. this.tabAnimationsInProgress--;
}
this.tabAnimationsInProgress++;
// Mute audio immediately to improve perceived speed of tab closure. if (!adoptedByTab && aTab.hasAttribute("soundplaying")) { // Don't persist the muted state as this wasn't a user action. // This lets undo-close-tab return it to an unmuted state.
aTab.linkedBrowser.mute(true);
}
// Invalidate hovered tab state tracking for this closing tab.
aTab._mouseleave();
if (newTab) { this.addTrustedTab(BROWSER_NEW_TAB_URL, {
skipAnimation: true, // In the event that insertAfterCurrent is set and the current tab is // inside a group that is being closed we want to avoid creating the // new tab inside that group.
tabIndex: 0,
});
} else {
TabBarVisibility.update();
}
// Splice this tab out of any lines of succession before any events are // dispatched. this.replaceInSuccession(aTab, aTab.successor); this.setSuccessor(aTab, null);
// We're committed to closing the tab now. // Dispatch a notification. // We dispatch it before any teardown so that event listeners can // inspect the tab that's about to close.
let evt = new CustomEvent("TabClose", {
bubbles: true,
detail: {
adoptedBy: adoptedByTab,
skipSessionStore,
isUserTriggered,
telemetrySource,
},
});
aTab.dispatchEvent(evt);
if (this.tabs.length == 2) { // We're closing one of our two open tabs, inform the other tab that its // sibling is going away. for (let tab of this.tabs) {
let bc = tab.linkedBrowser.browsingContext; if (bc) {
bc.hasSiblings = false;
}
}
}
let notificationBox = this.readNotificationBox(browser);
notificationBox?._stack?.remove();
if (aTab.linkedPanel) { if (!adoptedByTab && !gMultiProcessBrowser) { // Prevent this tab from showing further dialogs, since we're closing it
browser.contentWindow.windowUtils.disableDialogs();
}
// Remove the tab's filter and progress listener. const filter = this.#tabFilters.get(aTab);
if (browser.registeredOpenURI && !adoptedByTab) {
let userContextId = browser.getAttribute("usercontextid") || 0; this.UrlbarProviderOpenTabs.unregisterOpenTab(
browser.registeredOpenURI.spec,
userContextId,
aTab.group?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
); delete browser.registeredOpenURI;
}
// We are no longer the primary content area.
browser.removeAttribute("primary");
returntrue;
}
_endRemoveTab(aTab) { if (!aTab || !aTab._endRemoveArgs) { return;
}
var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
aTab._endRemoveArgs = null;
if (this.#windowIsClosing) {
aCloseWindow = false;
aNewTab = false;
}
this.tabAnimationsInProgress--;
this.#lastRelatedTabMap = new WeakMap();
// update the UI early for responsiveness
aTab.collapsed = true; this._blurTab(aTab);
this._removingTabs.delete(aTab);
if (aCloseWindow) { this.#windowIsClosing = true; for (let tab of this._removingTabs) { this._endRemoveTab(tab);
}
} elseif (!this.#windowIsClosing) { if (aNewTab) {
gURLBar.select();
}
}
// We're going to remove the tab and the browser now. this.#tabFilters.delete(aTab); this.#tabListeners.delete(aTab);
var browser = this.getBrowserForTab(aTab);
if (aTab.linkedPanel) { // Because of the fact that we are setting JS properties on // the browser elements, and we have code in place // to preserve the JS objects for any elements that have // JS properties set on them, the browser element won't be // destroyed until the document goes away. So we force a // cleanup ourselves. // This has to happen before we remove the child since functions // like `getBrowserContainer` expect the browser to be parented.
browser.destroy();
}
// Remove the tab ...
aTab.remove(); this.tabContainer._invalidateCachedTabs();
// ... and fix up the _tPos properties immediately. for (let i = aTab._tPos; i < this.tabs.length; i++) { this.tabs[i]._tPos = i;
}
if (!this.#windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons();
// update tab positional properties and attributes this.selectedTab._selected = true;
// Removing the panel requires fixing up selectedPanel immediately // (see below), which would be hindered by the potentially expensive // browser removal. So we remove the browser and the panel in two // steps.
var panel = this.getPanel(browser);
// In the multi-process case, it's possible an asynchronous tab switch // is still underway. If so, then it's possible that the last visible // browser is the one we're in the process of removing. There's the // risk of displaying preloaded browsers that are at the end of the // deck if we remove the browser before the switch is complete, so // we alert the switcher in order to show a spinner instead. if (this._switcher) { this._switcher.onTabRemoved(aTab);
}
// This will unload the document. An unload handler could remove // dependant tabs, so it's important that the tabbrowser is now in // a consistent state (tab removed, tab positions updated, etc.).
browser.remove();
// Release the browser in case something is erroneously holding a // reference to the tab after its removal. this.#tabForBrowser.delete(aTab.linkedBrowser);
aTab.linkedBrowser = null;
panel.remove();
// closeWindow might wait an arbitrary length of time if we're supposed // to warn about closing the window, so we'll just stop the tab close // stopwatches here instead. if (aTab._closeTimeAnimTimerId) {
Glean.browserTabclose.timeAnim.stopAndAccumulate(
aTab._closeTimeAnimTimerId
);
aTab._closeTimeAnimTimerId = null;
} if (aTab._closeTimeNoAnimTimerId) {
Glean.browserTabclose.timeNoAnim.stopAndAccumulate(
aTab._closeTimeNoAnimTimerId
);
aTab._closeTimeNoAnimTimerId = null;
}
/** *ClosesalltabsmatchingthelistofnsURIs. *ThisdoesnotcloseanytabsthathaveabeforeUnloadprompt. * *@param{nsURI[]}urisToClose *Thesetofuristoremove. *@returns{number}Thecountofsuccessfullyclosedtabs.
*/
async closeTabsByURI(urisToClose) {
let tabsToRemove = []; for (let tab of this.tabs) {
let currentURI = tab.linkedBrowser.currentURI; // Find any URI that matches the current tab's URI const matchedIndex = urisToClose.findIndex(uriToClose =>
uriToClose.equals(currentURI)
);
if (matchedIndex > -1) {
tabsToRemove.push(tab);
}
}
// _startRemoveTabs doesn't close the last tab in the window // for this use case, we simply close it if (lastToClose) { this.removeTab(lastToClose);
closedCount++;
}
} return closedCount;
}
async explicitUnloadTabs(tabs) {
let unloadBlocked = await this.runBeforeUnloadForTabs(tabs); if (unloadBlocked) { return;
}
let unloadSelectedTab = false;
let allTabsUnloaded = false; if (tabs.some(tab => tab.selected || tab.splitview?.hasActiveTab)) { // Unloading the currently selected tab. // Need to select a different one before unloading. // Avoid selecting any tab we're unloading now or // any tab that is already unloaded.
unloadSelectedTab = true;
let tabsToExclude = tabs.concat( this.tabContainer.allTabs.filter(tab => !tab.linkedPanel)
); for (const tab of tabs) { if (tab.splitview) {
tabsToExclude.push(
...tab.splitview.tabs.filter(splitViewTab => splitViewTab != tab)
);
}
}
let newTab = this._findTabToBlurTo(this.selectedTab, tabsToExclude); if (newTab) { this.selectedTab = newTab;
} else {
allTabsUnloaded = true; // all tabs are unloaded - show Firefox View if it's present, otherwise open a new tab // Firefox View counts as present if its tab is already open, or if the button // is visible, so as to not do this in private browsing mode or if the user // has removed the button from their toolbar (bug 1946432, bug 1989429)
let firefoxViewAvailable =
FirefoxViewHandler.tab &&
FirefoxViewHandler.button?.checkVisibility({
checkVisibilityCSS: true,
visibilityProperty: true,
}); if (firefoxViewAvailable) {
FirefoxViewHandler.openTab("opentabs");
} else { this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
skipAnimation: true,
});
}
}
}
let memoryUsageBeforeUnload = await getTotalMemoryUsage();
let timeBeforeUnload = performance.now();
let numberOfTabsUnloaded = 0;
await Promise.all(tabs.map(tab => this.prepareDiscardBrowser(tab)));
/** *Handlesopeninganewtabwithmousemiddleclick. * *@paramnode *@paramevent *Theclickevent
*/
handleNewTabMiddleClick(node, event) { // We should be using the disabled property here instead of the attribute, // but some elements that this function is used with don't support it (e.g. // menuitem). if (node.hasAttribute("disabled")) { return;
} // Do nothing
if (event.button == 1) {
BrowserCommands.openTab({ event }); // Stop the propagation of the click event, to prevent the event from being // handled more than once. // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4
event.stopPropagation();
event.preventDefault();
}
}
// If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. if (aTab.successor && !excludeTabs.has(aTab.successor)) { return aTab.successor;
}
// Try to find a remaining tab that comes after the given tab
let remainingTabs = Array.prototype.filter.call( this.visibleTabs,
tab => !excludeTabs.has(tab)
);
// If no qualifying visible tab was found, see if there is a tab in // a collapsed tab group that could be selected.
let eligibleTabs = new Set(this.tabsInCollapsedTabGroups).difference(
excludeTabs
);
/** *@returns{boolean} *Falseifswappingisn'tpermitted,trueotherwise.
*/
swapBrowsersAndCloseOther(aOurTab, aOtherTab) { // Do not allow transfering a private tab to a non-private window // and vice versa. if (
PrivateBrowsingUtils.isWindowPrivate(window) !=
PrivateBrowsingUtils.isWindowPrivate(aOtherTab.documentGlobal)
) { returnfalse;
}
// Do not allow transfering a useRemoteSubframes tab to a // non-useRemoteSubframes window and vice versa. if (gFissionBrowser != aOtherTab.documentGlobal.gFissionBrowser) { returnfalse;
}
let ourBrowser = this.getBrowserForTab(aOurTab);
let otherBrowser = aOtherTab.linkedBrowser;
// Can't swap between chrome and content processes. if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) { returnfalse;
}
// Keep the userContextId if set on other browser if (otherBrowser.hasAttribute("usercontextid")) {
ourBrowser.setAttribute( "usercontextid",
otherBrowser.getAttribute("usercontextid")
);
}
// That's gBrowser for the other window, not the tab's browser! var remoteBrowser = aOtherTab.documentGlobal.gBrowser; var isPending = aOtherTab.hasAttribute("pending");
let otherTabListener = remoteBrowser._getTabProgressListener(aOtherTab);
let stateFlags = 0; if (otherTabListener) {
stateFlags = otherTabListener._stateFlags;
}
// Expedite the removal of the icon if it was already scheduled. if (aOtherTab._soundPlayingAttrRemovalTimer) {
clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
aOtherTab._soundPlayingAttrRemovalTimer = 0;
aOtherTab.removeAttribute("soundplaying");
remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
}
// First, start teardown of the other browser. Make sure to not // fire the beforeunload event in the process. Close the other // window if this was its last tab. if (
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
})
) { returnfalse;
}
// If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted.
let [closeWindow] = aOtherTab._endRemoveArgs; if (closeWindow) {
let win = aOtherTab.documentGlobal;
win.windowUtils.suppressAnimation(true); // Only suppressing window animations isn't enough to avoid // an empty content area being painted.
let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
baseWin.visibility = false;
}
let modifiedAttrs = []; if (aOtherTab.hasAttribute("muted")) {
aOurTab.toggleAttribute("muted", true);
aOurTab.muteReason = aOtherTab.muteReason; // For non-lazy tabs, mute() must be called. if (aOurTab.linkedPanel) {
ourBrowser.mute();
}
modifiedAttrs.push("muted");
} if (aOtherTab.hasAttribute("discarded")) {
aOurTab.toggleAttribute("discarded", true);
modifiedAttrs.push("discarded");
} if (aOtherTab.hasAttribute("undiscardable")) {
aOurTab.toggleAttribute("undiscardable", true);
modifiedAttrs.push("undiscardable");
} if (aOtherTab.hasAttribute("soundplaying")) {
aOurTab.toggleAttribute("soundplaying", true);
modifiedAttrs.push("soundplaying");
} if (aOtherTab.hasAttribute("usercontextid")) {
aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
modifiedAttrs.push("usercontextid");
} if (aOtherTab.hasAttribute("sharing")) {
aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
modifiedAttrs.push("sharing");
ourBrowser._sharingState = otherBrowser._sharingState;
webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
} if (aOtherTab.hasAttribute("pictureinpicture")) {
aOurTab.toggleAttribute("pictureinpicture", true);
modifiedAttrs.push("pictureinpicture");
let event = new CustomEvent("TabSwapPictureInPicture", {
detail: aOurTab,
});
aOtherTab.dispatchEvent(event);
}
// Copy tab note-related properties of the tab.
aOurTab.hasTabNote = aOtherTab.hasTabNote;
aOurTab.canonicalUrl = aOtherTab.canonicalUrl;
if (otherBrowser.isDistinctProductPageVisit) {
ourBrowser.isDistinctProductPageVisit = true;
}
let srcBrowserId = otherBrowser.browserId;
// Add a reference to the original registeredOpenURI to the closing // tab so that events operating on the tab before close can reference it.
aOtherTab._originalRegisteredOpenURI = otherBrowser.registeredOpenURI;
// If the other tab is pending (i.e. has not been restored, yet) // then do not switch docShells but retrieve the other tab's state // and apply it to our tab. if (isPending) { // Tag tab so that the extension framework can ignore tab events that // are triggered amidst the tab/browser restoration process // (TabHide, TabPinned, TabUnpinned, "muted" attribute changes, etc.).
aOurTab.initializingTab = true; delete ourBrowser._cachedCurrentURI;
SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab)); delete aOurTab.initializingTab;
// Make sure to unregister any open URIs. this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
} else { // Workarounds for bug 458697 // Icon might have been set on DOMLinkAdded, don't override that. if (!ourBrowser.mIconURL && otherBrowser.mIconURL) { this.setIcon(aOurTab, otherBrowser.mIconURL);
} var isBusy = aOtherTab.hasAttribute("busy"); if (isBusy) {
aOurTab.setAttribute("busy", "true");
modifiedAttrs.push("busy"); if (aOurTab.selected) { this._isBusy = true;
}
}
// Unregister the previously opened URI if (otherBrowser.registeredOpenURI) {
let userContextId = otherBrowser.getAttribute("usercontextid") || 0; this.UrlbarProviderOpenTabs.unregisterOpenTab(
otherBrowser.registeredOpenURI.spec,
userContextId,
aOtherTab.group?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
); delete otherBrowser.registeredOpenURI;
}
// Handle findbar data (if any)
let otherFindBar = aOtherTab._findBar; if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
let oldValue = otherFindBar._findField.value;
let wasHidden = otherFindBar.hidden;
let ourFindBarPromise = this.getFindBar(aOurTab);
ourFindBarPromise.then(ourFindBar => { if (!ourFindBar) { return;
}
ourFindBar._findField.value = oldValue; if (!wasHidden) {
ourFindBar.onFindCommand();
}
});
}
// Finish tearing down the tab that's going away. if (closeWindow) {
aOtherTab.documentGlobal.close();
} else {
remoteBrowser._endRemoveTab(aOtherTab);
}
this.setTabTitle(aOurTab);
// If the tab was already selected (this happens in the scenario // of replaceTabWithWindow), notify onLocationChange, etc. if (aOurTab.selected) { this.updateCurrentBrowser(true);
}
if (modifiedAttrs.length) { this._tabAttrModified(aOurTab, modifiedAttrs);
}
returntrue;
}
// The progress listener and filter Maps are #-private, but tab swapping // reaches across windows: each window evaluates this script separately, so // another window's gBrowser is an instance of a different Tabbrowser class // and its private fields can't be read directly. These thin accessors run // in the owning window's realization, so callers can route through them // (e.g. otherWindowGBrowser._getTabProgressListener(tab)).
_getTabProgressListener(aTab) { returnthis.#tabListeners.get(aTab);
}
swapBrowsers(aOurTab, aOtherTab) {
let otherBrowser = aOtherTab.linkedBrowser;
let otherTabBrowser = otherBrowser.getTabBrowser();
// We aren't closing the other tab so, we also need to swap its tablisteners.
let filter = otherTabBrowser._getTabProgressFilter(aOtherTab);
let tabListener = otherTabBrowser._getTabProgressListener(aOtherTab);
otherBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(tabListener);
// Perform the docshell swap through the common mechanism. this._swapBrowserDocShells(aOurTab, otherBrowser);
// Restore the listeners for the swapped in tab.
tabListener = new otherTabBrowser.documentGlobal.TabProgressListener(
aOtherTab,
otherBrowser, false, false
);
otherTabBrowser._setTabProgressListener(aOtherTab, tabListener);
_swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) { // aOurTab's browser needs to be inserted now if it hasn't already. this._insertBrowser(aOurTab);
// Unhook our progress listener const filter = this.#tabFilters.get(aOurTab);
let tabListener = this.#tabListeners.get(aOurTab);
let ourBrowser = this.getBrowserForTab(aOurTab);
ourBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(tabListener);
// Make sure to unregister any open URIs. this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
let remoteBrowser = aOtherBrowser.documentGlobal.gBrowser;
// If switcher is active, it will intercept swap events and // react as needed. if (!this._switcher) {
aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser);
}
let ourBrowserContainer =
ourBrowser.ownerDocument.getElementById("browser");
let otherBrowserContainer =
aOtherBrowser.ownerDocument.getElementById("browser");
let ourBrowserContainerWasHidden = ourBrowserContainer.hidden;
let otherBrowserContainerWasHidden = otherBrowserContainer.hidden;
// #browser is hidden in Customize Mode; this breaks docshell swapping, // so we need to toggle 'hidden' to make swapping work in this case.
ourBrowserContainer.hidden = otherBrowserContainer.hidden = false;
// Swap the docshells
ourBrowser.swapDocShells(aOtherBrowser);
_swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) { // Swap the registeredOpenURI properties of the two browsers
let tmp = aOurBrowser.registeredOpenURI; delete aOurBrowser.registeredOpenURI; if (aOtherBrowser.registeredOpenURI) {
aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; delete aOtherBrowser.registeredOpenURI;
} if (tmp) {
aOtherBrowser.registeredOpenURI = tmp;
}
}
reloadTabs(tabs) { for (let tab of tabs) { try { this.getBrowserForTab(tab).reload();
} catch (e) { // ignore failure to reload so others will be reloaded
}
}
}
reloadTab(aTab) {
let browser = this.getBrowserForTab(aTab); // Reset temporary permissions on the current tab. This is done here // because we only want to reset permissions on user reload.
SitePermissions.clearTemporaryBlockPermissions(browser); // Also reset DOS mitigations for the basic auth prompt on reload. delete browser.authPromptAbuseCounter;
gIdentityHandler.hidePopup();
gPermissionPanel.hidePopup();
browser.reload();
}
addProgressListener(aListener) { if (arguments.length != 1) {
console.error( "gBrowser.addProgressListener was " + "called with a second argument, " + "which is not supported. See bug " + "608628. Call stack: ", new Error().stack
);
}
this.#progressListeners.push(aListener);
}
removeProgressListener(aListener) { this.#progressListeners = this.#progressListeners.filter(
l => l != aListener
);
}
hideTab(aTab, aSource) { if (
aTab.hidden ||
aTab.pinned ||
aTab.selected ||
aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden.
aTab.linkedBrowser._sharingState?.webRTC?.sharing
) { return;
}
aTab.setAttribute("hidden", "true"); this.tabContainer._invalidateCachedVisibleTabs();
this.tabContainer._updateCloseButtons(); if (aTab.multiselected) { this._updateMultiselectedTabCloseButtonTooltip();
}
// Splice this tab out of any lines of succession before any events are // dispatched. this.replaceInSuccession(aTab, aTab.successor); this.setSuccessor(aTab, null);
let event = document.createEvent("Events");
event.initEvent("TabHide", true, false);
aTab.dispatchEvent(event); if (aSource) {
SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
}
}
selectTabAtIndex(aIndex, aEvent) {
let tabs = this.visibleTabs;
// count backwards for aIndex < 0 if (aIndex < 0) {
aIndex += tabs.length; // clamp at index 0 if still negative. if (aIndex < 0) {
aIndex = 0;
}
} elseif (aIndex >= tabs.length) { // clamp at right-most tab if out of range.
aIndex = tabs.length - 1;
}
this.selectedTab = tabs[aIndex];
if (aEvent) {
aEvent.preventDefault();
aEvent.stopPropagation();
}
}
/** *Movesatabtoanewbrowserwindow,unlessit'salreadytheonlytab *inthecurrentwindow,inwhichcasethiswilldonothing. * *@param{MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement}aTab *@param{object}[aOptions={}] *Key-valuepairsthatwillbeserializedintothefeaturesstring.
*/
replaceTabWithWindow(aTab, aOptions = {}) { if (this.tabs.length == 1) { returnnull;
} // TODO bug 1967925: Consider handling the case where aTab is a tab group // and also the only tab group in its window.
// Play the tab closing animation to give immediate feedback while // waiting for the new window to appear. if (!gReduceMotion && this.isTab(aTab)) {
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
aTab.removeAttribute("fadein");
}
// tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab); return BrowserWindowTracker.openWindow({ private: PrivateBrowsingUtils.isWindowPrivate(window),
features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`)
.join(","),
openerWindow: window,
args,
});
}
let elements; if (contextTab.multiselected) {
elements = this.selectedElements;
} else {
elements = [contextTab.splitview ?? contextTab];
}
if (this.tabs.length == elements.length) { returnnull;
}
if (elements.length == 1) { returnthis.replaceTabWithWindow(elements[0], aOptions);
}
// Play the closing animation for all selected tabs to give // immediate feedback while waiting for the new window to appear. if (!gReduceMotion) { for (let element of elements) {
element.style.maxWidth = ""; // ensure that fade-out transition happens
element.removeAttribute("fadein");
}
}
// Create a new window and make it adopt the tabs, preserving their relative order. // The initial tab of the new window will be selected, so it should adopt the // selected tab of the original window, if applicable, or else the first moving tab. // This avoids tab-switches in the new window, preserving tab laziness. // However, to avoid multiple tab-switches in the original window, the other tabs // should be adopted before the selected one.
let { selectedTab } = gBrowser; if (
!elements.includes(selectedTab) &&
!elements.includes(selectedTab.splitview)
) {
selectedTab = this.isSplitViewWrapper(elements[0])
? elements[0].tabs[0]
: elements[0];
}
let win = this.replaceTabWithWindow(selectedTab, aOptions);
win.addEventListener( "before-initial-tab-adopted",
() => {
let tabIndex = 0; for (let element of elements) { if (element !== selectedTab && element !== selectedTab.splitview) { const newTab = win.gBrowser.isSplitViewWrapper(element)
? win.gBrowser.adoptSplitView(element, {
elementIndex: tabIndex,
})
: win.gBrowser.adoptTab(element, { tabIndex }); if (!newTab) { // The adoption failed. Restore "fadein" and don't increase the index.
element.setAttribute("fadein", "true"); continue;
}
}
++tabIndex;
} // Restore tab selection
let winVisibleTabs = win.gBrowser.visibleTabs;
let winTabLength = winVisibleTabs.length;
win.gBrowser.addRangeToMultiSelectedTabs(
winVisibleTabs[0],
winVisibleTabs[winTabLength - 1]
);
win.gBrowser.lockClearMultiSelectionOnce();
},
{ once: true }
); return win;
}
_updateTabsAfterInsert() { for (let i = 0; i < this.tabs.length; i++) { this.tabs[i]._tPos = i; this.tabs[i]._selected = false;
}
// If we're in the midst of an async tab switch while calling // moveTabTo, we can get into a case where _visuallySelected // is set to true on two different tabs. // // What we want to do in moveTabTo is to remove logical selection // from all tabs, and then re-add logical selection to selectedTab // (and visual selection as well if we're not running with e10s, which // setting _selected will do automatically). // // If we're running with e10s, then the visual selection will not // be changed, which is fine, since if we weren't in the midst of a // tab switch, the previously visually selected tab should still be // correct, and if we are in the midst of a tab switch, then the async // tab switcher will set the visually selected tab once the tab switch // has completed. this.selectedTab._selected = true;
}
// Return early if the tab is already in the right spot. if ( this.isTab(element) &&
element._tPos == tabIndex &&
!(element.group && forceUngrouped)
) { return;
}
// When asked to move a tab group label, we need to move the whole group // instead. if (this.isTabGroupLabel(element)) {
element = element.group;
} if (this.isTabGroup(element)) {
forceUngrouped = true;
} // When asked to move a tab in a splitview, move the entire wrapper instead. if (element.splitview) {
element = element.splitview;
} // When moving backwards, placing an element at tabIndex works trivially. // When moving forwards to a higher tabIndex, we need to increase the // index to account for the fact that the act of moving (multiple) tabs // causes all following tabs to have a decreased index.
let movingForwards = false; if (this.isTab(element)) {
movingForwards = tabIndex > element._tPos;
} else { // tab group or split view (mutually exclusive with being pinned).
let tabsInElement = element.tabs;
movingForwards = tabIndex > tabsInElement[0]._tPos; if (movingForwards) { // -1 because element will be *after* tabIndex instead of at it.
tabIndex += tabsInElement.length - 1;
tabIndex = Math.min(tabIndex, this.tabs.length);
}
} this.#handleTabMove(
element,
() => {
let neighbor = this.tabs[tabIndex]; if (forceUngrouped && neighbor?.group) {
neighbor = neighbor.group;
} if (neighbor?.splitview) {
neighbor = neighbor.splitview; if (neighbor === element) { // Already inside the same split view, don't move. // If you want to swap tabs, use splitview.reverseTabs() instead. return;
}
}
/** *@param{MozTabbrowserTab|MozTabbrowserTabGroup}element *Thetabortabgrouptomove.Alsoacceptsatabgrouplabelasa *stand-inforitsgroup. *@param{MozTabbrowserTab|MozTabbrowserTabGroup}targetElement *@param{boolean}[moveBefore=false] *@param{TabMetricsContext}[metricsContext]
*/
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group; if (!moveBefore && !targetElement.collapsed) { // Right after the tab group label = before the first tab in the tab group
targetElement = targetElement.tabs[0];
moveBefore = true;
}
} if (this.isTabGroupLabel(element)) {
element = element.group; if (targetElement?.group) {
targetElement = targetElement.group;
}
}
// Don't allow mixing pinned and unpinned tabs. if (element.pinned && !targetElement?.pinned) {
targetElement = this.tabs[this.pinnedTabCount - 1];
moveBefore = false;
} elseif (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element // in the tab strip. Potential scenarios: // 1. Moving an unpinned tab and the first unpinned tab is ungrouped: // move the unpinned tab right before the first unpinned tab. // 2. Moving an unpinned tab and the first unpinned tab is grouped: // move the unpinned tab right before the tab group. // 3. Moving a tab group and the first unpinned tab is ungrouped: // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group.
targetElement = this.tabs[this.pinnedTabCount]; if (targetElement.group) {
targetElement = targetElement.group;
}
moveBefore = true;
}
// We want to include the splitview wrapper if it's the targetElement, but // not in the case where we want to reverse tabs within the same splitview. if (targetElement?.splitview && !element.splitview) {
targetElement = targetElement.splitview;
}
let getContainer = () =>
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
/** public API to the below private method */
handleTabMove(element, moveActionCallback, metricsContext) { this.#handleTabMove(element, moveActionCallback, metricsContext);
}
/** *@param{MozTabbrowserTab|MozTabbrowserTabGroup|MozTabSplitViewWrapper}element *@param{function():void}moveActionCallback *@param{TabMetricsContext}[metricsContext]
*/
#handleTabMove(element, moveActionCallback, metricsContext) {
let tabs; // TODO bug 2024173: consider removing element.splitview check. if (this.isTab(element) && element.splitview?.shouldMoveAllTabsAtOnce) {
tabs = element.splitview.tabs;
} elseif (this.isTab(element)) {
tabs = [element];
} elseif (this.isTabGroup(element) || this.isSplitViewWrapper(element)) {
tabs = element.tabs;
} else { thrownew Error( "Can only move a tab, tab group, or split view within the tab bar"
);
}
let wasFocused = document.activeElement == this.selectedTab;
let previousTabStates = tabs.map(tab => this.#getTabMoveState(tab));
moveActionCallback();
// Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); this.#lastRelatedTabMap = new WeakMap(); this._updateTabsAfterInsert();
if (wasFocused) { this.selectedTab.focus();
}
// When multiple tabs are moved forwards (tab group, split view), reverse // the order of TabMove, so that the index in previousTabState values are // still accurate until the event is dispatched. If we were to start with // the front tab, then logically that tab moves, and all following tabs // would shift, which would invalidate the index in previousTabState.
let reverseEvents =
tabs.length > 1 && tabs[0]._tPos > previousTabStates[0].tabIndex;
for (let i = 0; i < tabs.length; i++) {
let ii = reverseEvents ? tabs.length - i - 1 : i;
let tab = tabs[ii]; if (tab.selected) { this.tabContainer._handleTabSelect(true);
}
let currentFirst = this.#getTabMoveState(tabs[0]); if ( this.isTabGroup(element) &&
previousTabStates[0].tabIndex != currentFirst.tabIndex
) {
let event = new CustomEvent("TabGroupMoved", { bubbles: true });
element.dispatchEvent(event);
}
}
/** *Adoptsatabfromanotherbrowserwindow,andinsertsitatthegivenindex. * *@returns{object} *Thenewtabinthecurrentwindow,nullifthetabcouldn'tbeadopted.
*/
adoptTab(aTab, { elementIndex, tabIndex, selectTab = false } = {}) { // Swap the dropped tab with a new one we create and then close // it in the other window (making it seem to have moved between // windows). We also ensure that the tab we create to swap into has // the same remote type and process as the one we're swapping in. // This makes sure we don't get a short-lived process for the new tab.
let linkedBrowser = aTab.linkedBrowser;
let createLazyBrowser = !aTab.linkedPanel;
let index;
let nextElement; if (typeof elementIndex == "number") {
index = elementIndex;
nextElement = this.tabContainer.dragAndDropElements.at(elementIndex);
} else {
index = tabIndex;
nextElement = this.tabs.at(tabIndex);
}
let tabInGroup = !!aTab.group;
let params = {
eventDetail: { adoptedTab: aTab },
preferredRemoteType: linkedBrowser.remoteType,
initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
skipAnimation: true,
elementIndex,
tabIndex,
tabGroup: this.isTab(nextElement) && nextElement.group,
createLazyBrowser,
};
// We want to explicitly set this param rather than carry it over to // avoid situations like an unpinned tab being dragged between pinned // tabs but not getting pinned as expected.
let numPinned = this.pinnedTabCount; if (index < numPinned || (aTab.pinned && index == numPinned)) {
params.pinned = true;
}
if (aTab.hasAttribute("usercontextid")) { // new tab must have the same usercontextid as the old one
params.userContextId = aTab.getAttribute("usercontextid");
}
params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params);
_endMultiSelectChange() {
let noticeable = false;
let { selectedTab } = this; if (this.#multiSelectChangeAdditions.size) { if (!selectedTab.multiselected) {
this.addToMultiSelectedTabs(selectedTab);
}
noticeable = true;
} if (this.#multiSelectChangeRemovals.size) { if (this.#multiSelectChangeRemovals.has(selectedTab)) {
this._switchToNextMultiSelectedTab();
}
this._avoidSingleSelectedTab();
noticeable = true;
} if (noticeable) {
this._updateMultiselectedTabCloseButtonTooltip(
this.#multiSelectChangeRemovals
);
}
this.#multiSelectChangeStarted = false; if (noticeable || this.#multiSelectChangeSelected) {
this.#multiSelectChangeSelected = false;
this.#multiSelectChangeAdditions.clear();
this.#multiSelectChangeRemovals.clear();
this.dispatchEvent( new CustomEvent("TabMultiSelect", { bubbles: true })
);
}
}
toggleMuteAudioOnMultiSelectedTabs(aTab) {
let tabMuted = aTab.linkedBrowser.audioMuted;
let tabsToToggle = this.selectedTabs.filter(
tab => tab.linkedBrowser.audioMuted == tabMuted
); for (let tab of tabsToToggle) {
tab.toggleMuteAudio();
}
}
resumeDelayedMediaOnMultiSelectedTabs() { for (let tab of this.selectedTabs) {
tab.resumeDelayedMedia();
}
}
pinMultiSelectedTabs() { for (let tab of this.selectedTabs) {
this.pinTab(tab);
}
}
unpinMultiSelectedTabs() { // The selectedTabs getter returns the tabs // in visual order. We need to unpin in reverse // order to maintain visual order.
let selectedTabs = this.selectedTabs; for (let i = selectedTabs.length - 1; i >= 0; i--) {
let tab = selectedTabs[i];
this.unpinTab(tab);
}
}
_getSwitcher() { if (!this._switcher) {
this._switcher = new this.AsyncTabSwitcher(this);
} return this._switcher;
}
warmupTab(aTab) { if (gMultiProcessBrowser) {
this._getSwitcher().warmupTab(aTab);
}
}
/** *_maybeRequestReplyFromRemoteContentmaycall *aEvent.requestReplyFromRemoteContentifnecessary. * *@paramaEventThehandlingevent. *@returntrueifthehandlershouldwaitareplyevent. *falseifthehandlecanhandletheimmediately.
*/
_maybeRequestReplyFromRemoteContent(aEvent) { if (aEvent.defaultPrevented) { returnfalse;
} // If the event target is a remote browser, and the event has not been // handled by the remote content yet, we should wait a reply event // from the content. if (aEvent.isWaitingReplyFromRemoteContent) { return true; // Somebody called requestReplyFromRemoteContent already.
} if (
!aEvent.isReplyEventFromRemoteContent &&
aEvent.target?.isRemoteBrowser === true
) {
aEvent.requestReplyFromRemoteContent(); return true;
} returnfalse;
}
on_keydown(aEvent) { if (!aEvent.isTrusted) { // Don't let untrusted events mess with tabs. return;
}
// Skip this only if something has explicitly cancelled it. if (aEvent.defaultCancelled) { return;
}
// Skip if chrome code has cancelled this or keyboard lock prevented if (aEvent.defaultPrevented) { return;
}
// We only want to request reply if this is an actual action. const action = ShortcutUtils.getSystemActionForEvent(aEvent); if (
action != null &&
this.KeyboardLockUtils.mustWaitForKeyboardLockRequestedReply(aEvent)
) { return;
}
// Don't check if the event was already consumed because tab // navigation should always work for better user experience. switch (action) { case ShortcutUtils.TOGGLE_CARET_BROWSING:
this._maybeRequestReplyFromRemoteContent(aEvent); return; case ShortcutUtils.MOVE_TAB_BACKWARD:
this.moveTabBackward();
aEvent.preventDefault(); return; case ShortcutUtils.MOVE_TAB_FORWARD:
this.moveTabForward();
aEvent.preventDefault(); return; case ShortcutUtils.CLOSE_TAB: if (gBrowser.multiSelectedTabsCount) {
gBrowser.removeMultiSelectedTabs();
} elseif (!this.selectedTab.pinned) {
this.removeCurrentTab({ animate: true });
}
aEvent.preventDefault();
}
}
on_keypress(aEvent) { if (!aEvent.isTrusted) { // Don't let untrusted events mess with tabs. return;
}
// Skip this only if something has explicitly cancelled it. if (aEvent.defaultCancelled) { return;
}
// Skip if chrome code has cancelled this: if (aEvent.defaultPreventedByChrome) { return;
}
switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) { case ShortcutUtils.TOGGLE_CARET_BROWSING: if (
aEvent.defaultPrevented ||
this._maybeRequestReplyFromRemoteContent(aEvent)
) { break;
}
this.#toggleCaretBrowsing(); break;
case ShortcutUtils.NEXT_TAB: if (AppConstants.platform == "macosx") {
this.tabContainer.advanceSelectedTab(DIRECTION_FORWARD, true);
aEvent.preventDefault();
} break; case ShortcutUtils.PREVIOUS_TAB: if (AppConstants.platform == "macosx") {
this.tabContainer.advanceSelectedTab(DIRECTION_BACKWARD, true);
aEvent.preventDefault();
} break;
}
}
on_framefocusrequested(aEvent) {
let tab = this.getTabForBrowser(aEvent.target); if (!tab || tab == this.selectedTab) { // Let the focus manager try to do its thing by not calling // preventDefault(). It will still raise the window if appropriate. return;
}
this.selectedTab = tab;
window.focus();
aEvent.preventDefault();
}
on_visibilitychange() { if (!this._switcher) { const inactive = document.hidden; for (const browser of this.selectedBrowsers) {
browser.preserveLayers(inactive);
browser.docShellIsActive = !inactive;
}
}
}
on_TabGrouped(aEvent) {
let tab = aEvent.detail;
let uri =
tab.linkedBrowser?.registeredOpenURI || tab._originalRegisteredOpenURI; if (uri) {
this.UrlbarProviderOpenTabs.unregisterOpenTab(
uri.spec,
tab.userContextId,
null,
PrivateBrowsingUtils.isWindowPrivate(window)
);
this.UrlbarProviderOpenTabs.registerOpenTab(
uri.spec,
tab.userContextId,
tab.group?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
);
}
}
on_TabUngrouped(aEvent) {
let tab = aEvent.detail;
let uri =
tab.linkedBrowser?.registeredOpenURI || tab._originalRegisteredOpenURI; if (uri) { // By the time the tab makes it to us it is already ungrouped, but // the original group is preserved in the event target.
let originalGroup = aEvent.target;
this.UrlbarProviderOpenTabs.unregisterOpenTab(
uri.spec,
tab.userContextId,
originalGroup.id,
PrivateBrowsingUtils.isWindowPrivate(window)
);
this.UrlbarProviderOpenTabs.registerOpenTab(
uri.spec,
tab.userContextId,
null,
PrivateBrowsingUtils.isWindowPrivate(window)
);
}
}
/** * *@param{MozTabbrowserTab}tab
*/ #isFirstOrLastInTabGroup(tab) { if (tab.group) {
let groupTabs = tab.group.tabs; if (groupTabs.at(0) == tab || groupTabs.at(-1) == tab) { return true;
}
} returnfalse;
}
getTabPids(tab) { if (!tab?.linkedBrowser) { return [];
}
// Get the PIDs of the content process and remote subframe processes
let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
tab.linkedBrowser,
gFissionBrowser
);
let pids = contentPid ? [contentPid] : []; return pids.concat(framePids.sort());
}
/** *@param{MozTabbrowserTab}tab *@param{boolean}[includeLabel=true] *Includethetab'stitle/fulllabelinthetooltip.Defaultstotrue, *Canbedisabledforcontextswhereincludingthetitleinthetooltip *stringwouldbeduplicativewouldalreadyavailableinformation, *e.g.accessibilitydescriptions. *@returns{string}
*/
getTabTooltip(tab, includeLabel = true) {
let labelArray = []; if (includeLabel) {
labelArray.push(tab._fullLabel || tab.getAttribute("label"));
} if (this.showPidAndActiveness) { const pids = this.getTabPids(tab);
let debugStringArray = []; if (pids.length) {
let pidLabel = pids.length > 1 ? "pids" : "pid";
debugStringArray.push(`(${pidLabel} ${pids.join(", ")})`);
}
if (tab.linkedBrowser.docShellIsActive) {
debugStringArray.push("[A]");
}
if (this.SponsorProtection.isProtectedBrowser(tab.linkedBrowser)) {
debugStringArray.push("[S]");
}
if (debugStringArray.length) {
labelArray.push(debugStringArray.join(" "));
}
}
// Add a line to the tooltip with additional tab context (e.g. container
let containerName = tab.userContextId
? ContextualIdentityService.getUserContextLabel(tab.userContextId)
: "";
let tabGroupName = this.#isFirstOrLastInTabGroup(tab)
? tab.group.name ||
this.tabLocalization.formatValueSync("tab-group-name-default")
: "";
if (tab.soundPlaying) {
let audioPlayingString = this.tabLocalization.formatValueSync( "tabbrowser-tab-audio-playing-description"
);
labelArray.push(audioPlayingString);
} return labelArray.join("\n");
}
createTooltip(event) {
event.stopPropagation();
let tab = event.target.triggerNode?.closest("tab"); if (!tab) { if (event.target.triggerNode?.getRootNode()?.host?.closest("tab")) { // Check if triggerNode is within shadowRoot of moz-button
tab = event.target.triggerNode?.getRootNode().host.closest("tab");
} else {
event.preventDefault(); return;
}
}
const tabCount = this.selectedTabs.includes(tab)
? this.selectedTabs.length
: 1; if (tab._overPlayingIcon || tab._overAudioButton) {
let l10nId; const l10nArgs = { tabCount }; if (tab.selected) {
l10nId = tab.linkedBrowser.audioMuted
? "tabbrowser-unmute-tab-audio-tooltip"
: "tabbrowser-mute-tab-audio-tooltip"; const keyElem = document.getElementById("key_toggleMute");
l10nArgs.shortcut = ShortcutUtils.prettifyShortcut(keyElem);
} elseif (tab.hasAttribute("activemedia-blocked")) {
l10nId = "tabbrowser-unblock-tab-audio-tooltip";
} else {
l10nId = tab.linkedBrowser.audioMuted
? "tabbrowser-unmute-tab-audio-background-tooltip"
: "tabbrowser-mute-tab-audio-background-tooltip";
}
tooltip.label = "";
document.l10n.setAttributes(tooltip, l10nId, l10nArgs);
} else { // Prevent the tooltip from appearing if card preview is enabled, but // only if the user is not hovering over the media play icon or the // close button if (this._showTabCardPreview) {
event.preventDefault(); return;
}
tooltip.label = this.getTabTooltip(tab, true);
}
}
handleEvent(aEvent) {
let methodName = `on_${aEvent.type}`; if (methodName in this) { this[methodName](aEvent);
} else { thrownew Error(`Unexpected event ${aEvent.type}`);
}
}
observe(aSubject, aTopic) { switch (aTopic) { case"contextual-identity-updated": {
let identity = aSubject.wrappedJSObject; for (let tab of this.tabs) { if (tab.getAttribute("usercontextid") == identity.userContextId) {
ContextualIdentityService.setTabStyle(tab);
}
} break;
} case"intl:app-locales-changed": { this.#populateTitleCache(); this.updateTitlebar(); break;
}
}
}
refreshBlocked(actor, browser, data) { // The data object is expected to contain the following properties: // - URI (string) // The URI that a page is attempting to refresh or redirect to. // - delay (int) // The delay (in milliseconds) before the page was going to // reload or redirect. // - sameURI (bool) // true if we're refreshing the page. false if we're redirecting.
let notificationBox = this.getNotificationBox(browser);
let notification =
notificationBox.getNotificationWithValue("refresh-blocked");
_generateUniquePanelID() { if (!this._uniquePanelIDCounter) { this._uniquePanelIDCounter = 0;
}
let outerID = window.docShell.outerWindowID;
// We want panel IDs to be globally unique, that's why we include the // window ID. We switched to a monotonic counter as Date.now() lead // to random failures because of colliding IDs. return"panel-" + outerID + "-" + ++this._uniquePanelIDCounter;
}
this.addEventListener("DOMWindowClose", event => {
let browser = event.target; if (!browser.isRemoteBrowser) { if (!event.isTrusted) { // If the browser is not remote, then we expect the event to be trusted. // In the remote case, the DOMWindowClose event is captured in content, // a message is sent to the parent, and another DOMWindowClose event // is re-dispatched on the actual browser node. In that case, the event // won't be marked as trusted, since it's synthesized by JavaScript. return;
} // In the parent-process browser case, it's possible that the browser // that fired DOMWindowClose is actually a child of another browser. We // want to find the top-most browser to determine whether or not this is // for a tab or not. The chromeEventHandler will be the top-most browser.
browser = event.target.docShell.chromeEventHandler;
}
if (this.tabs.length == 1) { // We already did PermitUnload in the content process // for this tab (the only one in the window). So we don't // need to do it again for any tabs.
window.skipNextCanClose = true; // In the parent-process browser case, the nsCloseEvent will actually take // care of tearing down the window, but we need to do this ourselves in the // content-process browser case. Doing so in both cases doesn't appear to // hurt.
window.close(); return;
}
let tab = this.getTabForBrowser(browser); if (tab) { // Skip running PermitUnload since it already happened in // the content process. this.removeTab(tab, { skipPermitUnload: true }); // If we don't preventDefault on the DOMWindowClose event, then // in the parent-process browser case, we're telling the platform // to close the entire window. Calling preventDefault is our way of // saying we took care of this close request by closing the tab.
event.preventDefault();
}
});
this.addEventListener("pagetitlechanged", event => {
let browser = event.target;
let tab = this.getTabForBrowser(browser); if (!tab || tab.hasAttribute("pending")) { return;
}
// Ignore empty title changes on internal pages. This prevents the title // from changing while Fluent is populating the (initially-empty) title // element. if (
!browser.contentTitle &&
browser.contentPrincipal.isSystemPrincipal
) { return;
}
let titleChanged = this.setTabTitle(tab); if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
tab.setAttribute("titlechanged", "true");
}
});
this.addEventListener( "DOMWillOpenModalDialog",
event => { if (!event.isTrusted) { return;
}
let targetIsWindow = Window.isInstance(event.target);
// We're about to open a modal dialog, so figure out for which tab: // If this is a same-process modal dialog, then we're given its DOM // window as the event's target. For remote dialogs, we're given the // browser, but that's in the originalTarget and not the target, // because it's across the tabbrowser's XBL boundary.
let tabForEvent = targetIsWindow
? this.getTabForBrowser(event.target.docShell.chromeEventHandler)
: this.getTabForBrowser(event.originalTarget);
// Focus window for beforeunload dialog so it is seen but don't // steal focus from other applications. if (
event.detail &&
event.detail.tabPrompt &&
event.detail.inPermitUnload &&
Services.focus.activeWindow
) {
window.focus();
}
// Don't need to act if the tab is already selected or if there isn't // a tab for the event (e.g. for the webextensions options_ui remote // browsers embedded in the "about:addons" page): if (!tabForEvent || tabForEvent.selected) { return;
}
// We always switch tabs for beforeunload tab-modal prompts. if (
event.detail &&
event.detail.tabPrompt &&
!event.detail.inPermitUnload
) {
let docPrincipal = targetIsWindow
? event.target.document.nodePrincipal
: null; // At least one of these should/will be non-null:
let promptPrincipal =
event.detail.promptPrincipal ||
docPrincipal ||
tabForEvent.linkedBrowser.contentPrincipal;
// For null principals, we bail immediately and don't show the checkbox: if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
tabForEvent.attention = true; return;
}
// For non-system/expanded principals without permission, we bail and show the checkbox. if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
let permission = Services.perms.testPermissionFromPrincipal(
promptPrincipal, "focus-tab-by-prompt"
); if (permission != Services.perms.ALLOW_ACTION) { // Tell the prompt box we want to show the user a checkbox:
let tabPrompt = this.getTabDialogBox(tabForEvent.linkedBrowser);
tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
promptPrincipal
);
tabForEvent.attention = true; return;
}
} // ... so system and expanded principals, as well as permitted "normal" // URI-based principals, always get to steal focus for the tab when prompting.
}
// If permissions/origins dictate so, bring tab to the front. this.selectedTab = tabForEvent;
}, true
);
// When cancelling beforeunload tabmodal dialogs, reset the URL bar to // avoid spoofing risks. this.addEventListener( "DOMModalDialogClosed",
event => { if (
event.detail?.promptType != "beforeunload" ||
event.detail.areLeaving ||
event.target.nodeName != "browser"
) { return;
}
event.target.userTypedValue = null; if (event.target == this.selectedBrowser) {
gURLBar.setURI();
}
}, true
);
let onTabCrashed = event => { if (!event.isTrusted) { return;
}
let browser = event.originalTarget;
if (!event.isTopFrame) {
TabCrashHandler.onSubFrameCrash(browser, event.childID); return;
}
// Preloaded browsers do not actually have any tabs. If one crashes, // it should be released and removed. if (browser === this.preloadedBrowser) {
NewTabPagePreloading.removePreloadedBrowser(window); return;
}
let isRestartRequiredCrash =
event.type == "oop-browser-buildid-mismatch";
let icon = browser.mIconURL;
let tab = this.getTabForBrowser(browser);
let modifiedAttrs = []; if (tab.hasAttribute("soundplaying-scheduledremoval")) {
tab.removeAttribute("soundplaying-scheduledremoval");
modifiedAttrs.push("soundplaying-scheduledremoval");
}
if (!tab.hasAttribute("soundplaying")) {
tab.toggleAttribute("soundplaying", true);
modifiedAttrs.push("soundplaying");
}
if (modifiedAttrs.length) { // Flush style so that the opacity takes effect immediately, in // case the media is stopped before the style flushes naturally.
getComputedStyle(tab).opacity;
}
this._tabAttrModified(tab, modifiedAttrs);
});
this.addEventListener("DOMAudioPlaybackStopped", event => { var tab = this.getTabFromAudioEvent(event); if (!tab) { return;
}
if (tab.hasAttribute("soundplaying")) {
let removalDelay = Services.prefs.getIntPref( "browser.tabs.delayHidingAudioPlayingIconMS"
);
// Fired when Gecko has decided a <browser> element will change // remoteness. This allows persisting some state on this element across // process switches. this.addEventListener("WillChangeBrowserRemoteness", event => {
let browser = event.originalTarget;
let tab = this.getTabForBrowser(browser); if (!tab) { return;
}
// Dispatch the `BeforeTabRemotenessChange` event, allowing other code // to react to this tab's process switch.
let evt = document.createEvent("Events");
evt.initEvent("BeforeTabRemotenessChange", true, false);
tab.dispatchEvent(evt);
// Unhook our progress listener.
let filter = this.#tabFilters.get(tab);
let oldListener = this.#tabListeners.get(tab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(oldListener);
let stateFlags = oldListener._stateFlags;
let requestCount = oldListener._requestCount;
// We'll be creating a new listener, so destroy the old one.
oldListener.destroy();
let oldUserTypedValue = browser.userTypedValue;
let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();
let didChange = () => {
browser.userTypedValue = oldUserTypedValue; if (hadStartedLoad) {
browser.urlbarChangeTracker.startedLoad();
}
// This shouldn't really be necessary, however, this has the side effect // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote // frames, which the tab switcher depends on. // // eslint-disable-next-line no-self-assign
browser.docShellIsActive = browser.docShellIsActive;
// Create a new tab progress listener for the new browser we just // injected, since tab progress listeners have logic for handling the // initial about:blank load
let listener = new TabProgressListener(
tab,
browser, false, false,
stateFlags,
requestCount
); this.#tabListeners.set(tab, listener);
filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
// Restore the progress listener.
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
let cbEvent = browser.getContentBlockingEvents(); // Include the true final argument to indicate that this event is // simulated (instead of being observed by the webProgressListener). this._callProgressListeners(
browser, "onContentBlockingEvent",
[browser.webProgress, null, cbEvent, true], true, false
);
if (browser.isRemoteBrowser) { // Switching the browser to be remote will connect to a new child // process so the browser can no longer be considered to be // crashed.
tab.removeAttribute("crashed");
}
if (this.isFindBarInitialized(tab)) { this.getCachedFindBar(tab).browser = browser;
}
translateTabContextMenu() { if (this._tabContextMenuTranslated) { return;
}
MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl"); // Un-lazify the l10n-ids now that the FTL file has been inserted.
document
.getElementById("tabContextMenu")
.querySelectorAll("[data-lazy-l10n-id]")
.forEach(el => {
el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
el.removeAttribute("data-lazy-l10n-id");
}); this._tabContextMenuTranslated = true;
}
setSuccessor(aTab, successorTab) { if (aTab.documentGlobal != window) { thrownew Error("Cannot set the successor of another window's tab");
} if (successorTab == aTab) {
successorTab = null;
} if (successorTab && successorTab.documentGlobal != window) { thrownew Error("Cannot set the successor to another window's tab");
} if (aTab.successor) {
aTab.successor.predecessors.delete(aTab);
}
aTab.successor = successorTab; if (successorTab) { if (!successorTab.predecessors) {
successorTab.predecessors = new Set();
}
successorTab.predecessors.add(aTab);
}
}
/** *ForalltabswithaTabasasuccessor,setthesuccessortoaOtherTab *instead.
*/
replaceInSuccession(aTab, aOtherTab) { if (aTab.predecessors) { for (const predecessor of Array.from(aTab.predecessors)) { this.setSuccessor(predecessor, aOtherTab);
}
}
}
/** *Getthetriggeringprincipalforthelastnavigationinthesessionhistory.
*/
_getTriggeringPrincipalFromHistory(aBrowser) {
let sessionHistory = aBrowser?.browsingContext?.sessionHistory; if (
!sessionHistory ||
!sessionHistory.index ||
sessionHistory.count == 0
) { return undefined;
}
let currentEntry = sessionHistory.getEntryAtIndex(sessionHistory.index);
let triggeringPrincipal = currentEntry?.triggeringPrincipal; return triggeringPrincipal;
}
clearRelatedTabs() { this.#lastRelatedTabMap = new WeakMap();
}
};
/** *Awebprogresslistenerobjectdefinitionforagiventab.
*/ class TabProgressListener {
constructor(
aTab,
aBrowser,
aStartsBlank,
aWasPreloadedBrowser,
aOrigStateFlags,
aOrigRequestCount
) {
let stateFlags = aOrigStateFlags || 0; // Initialize _stateFlags to non-zero e.g. when creating a progress // listener for preloaded browsers as there was no progress listener // around when the content started loading. If the content didn't // quite finish loading yet, _stateFlags will very soon be overridden // with the correct value and end up at STATE_STOP again. if (aWasPreloadedBrowser) {
stateFlags =
Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_REQUEST;
}
_shouldShowProgress(aRequest) { if (this._blank) { returnfalse;
}
// Don't show progress indicators in tabs for about: URIs // pointing to local resources. if (
aRequest instanceof Ci.nsIChannel &&
aRequest.originalURI.schemeIs("about")
) { returnfalse;
}
// If the state has STATE_STOP, and no requests were in flight, then this // must be the initial "stop" for the initial about:blank document. if (
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && this._requestCount == 0 &&
!aLocation
) { returntrue;
}
// If we were ignoring some messages about the initial about:blank, and we // got the STATE_STOP for it, we'll want to pay attention to those messages // from here forward. Similarly, if we conclude that this state change // is one that we shouldn't be ignoring, then stop ignoring. if (
(ignoreBlank &&
aStateFlags & STATE_STOP &&
aStateFlags & STATE_IS_NETWORK) ||
(!ignoreBlank && this._blank)
) { this._blank = false;
}
if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) { this._requestCount++;
if (aWebProgress.isTopLevel) { // Need to use originalLocation rather than location because things // like about:home and about:privatebrowsing arrive with nsIRequest // pointing to their resolved jar: or file: URIs. if (
!(
originalLocation &&
gInitialPages.includes(originalLocation.spec) &&
originalLocation != "about:blank" && this._browser.initialPageLoadedFromUserAction !=
originalLocation.spec && this._browser.currentURI && this._browser.currentURI.spec == "about:blank"
)
) { // Indicating that we started a load will allow the location // bar to be cleared when the load finishes. // In order to not overwrite user-typed content, we avoid it // (see if condition above) in a very specific case: // If the load is of an 'initial' page (e.g. about:privatebrowsing, // about:newtab, etc.), was not explicitly typed in the location // bar by the user, is not about:blank (because about:blank can be // loaded by websites under their principal), and the current // page in the browser is about:blank (indicating it is a newly // created or re-created browser, e.g. because it just switched // remoteness or is a new tab/window). this._browser.urlbarChangeTracker.startedLoad();
// To improve the user experience and perceived performance when // opening links in new tabs, we show the url and tab title sooner, // but only if it's safe (from a phishing point of view) to do so, // thus there's no session history and the load starts from a // non-web-controlled blank page. if ( this._browser.browsingContext.sessionHistory?.count === 0 &&
(this._browser.initiatedFromNonWebControlled ||
BrowserUIUtils.checkEmptyPageOrigin( this._browser,
originalLocation
))
) {
gBrowser.setInitialTabTitle(this._tab, originalLocation.spec, {
isURL: true,
});
this._browser.browsingContext.nonWebControlledLoadingURI =
originalLocation; if (this._tab.selected && !gBrowser.userTypedValue) {
gURLBar.setURI();
}
}
} deletethis._browser.initialPageLoadedFromUserAction; deletethis._browser.initiatedFromNonWebControlled; // If the browser is loading it must not be crashed anymore this._tab.removeAttribute("crashed");
}
if (this._tab.selected) {
gBrowser._isBusy = true;
}
}
} elseif (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) { // since we (try to) only handle STATE_STOP of the last request, // the count of open requests should now be 0 this._requestCount = 0;
let modifiedAttrs = []; if (this._tab.hasAttribute("busy")) { this._tab.removeAttribute("busy");
modifiedAttrs.push("busy");
// Only animate the "burst" indicating the page has loaded if // the top-level page is the one that finished loading. if (
aWebProgress.isTopLevel &&
!aWebProgress.isLoadingDocument &&
Components.isSuccessCode(aStatus) &&
!gBrowser.tabAnimationsInProgress &&
!gReduceMotion
) { if (this._tab._notselectedsinceload) { this._tab.setAttribute("notselectedsinceload", "true");
} else { this._tab.removeAttribute("notselectedsinceload");
}
this._tab.setAttribute("bursting", "true");
}
}
if (this._tab.hasAttribute("progress")) { this._tab.removeAttribute("progress");
modifiedAttrs.push("progress");
}
if (aWebProgress.isTopLevel) {
let isSuccessful = Components.isSuccessCode(aStatus); if (!isSuccessful && !this._tab.isEmpty) { // Restore the current document's location in case the // request was stopped (possibly from a content script) // before the location changed.
this._browser.userTypedValue = null; // When SHIP is enabled and a load gets cancelled due to another one // starting, the error is NS_BINDING_CANCELLED_OLD_LOAD. // When these prefs are not enabled, the error is different and // that's why we still want to look at the isNavigating flag. // We could add a workaround and make sure that in the alternative // codepaths we would also omit the same error, but considering // how we will be enabling fission by default soon, we can keep // using isNavigating for now, and remove it when SHIP is enabled // by default. // Bug 1725716 has been filed to consider removing isNavigating // field alltogether.
let isNavigating = this._browser.isNavigating; if ( this._tab.selected &&
aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD &&
!isNavigating
) {
gURLBar.setURI();
}
} elseif (isSuccessful) { this._browser.urlbarChangeTracker.finishedLoad();
}
}
// If we don't already have an icon for this tab then clear the tab's // icon. Don't do this on the initial about:blank load to prevent // flickering. Don't clear the icon if we already set it from one of the // known defaults. Note we use the original URL since about:newtab // redirects to a prerendered page. const shouldRemoveFavicon =
!this._browser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS); if (shouldRemoveFavicon && this._tab.hasAttribute("image")) { this._tab.removeAttribute("image");
modifiedAttrs.push("image");
} elseif (!shouldRemoveFavicon) { // Bug 1804166: Allow new tabs to set the favicon correctly if the // new tabs behavior is set to open a blank page. // This is a no-op unless this._browser.documentURI is in // FAVICON_DEFAULTS.
gBrowser.setDefaultIcon(this._tab, this._browser.documentURI);
}
// For keyword URIs clear the user typed value since they will be changed into real URIs if (location.scheme == "keyword") { this._browser.userTypedValue = null;
}
if (this._tab.selected) {
gBrowser._isBusy = false;
}
if (modifiedAttrs.length) {
gBrowser._tabAttrModified(this._tab, modifiedAttrs);
}
}
if (aStateFlags & (STATE_START | STATE_STOP)) { // reset cached temporary values at beginning and end this._message = ""; this._totalProgress = 0;
} this._stateFlags = aStateFlags; this._status = aStatus;
} /* eslint-enable complexity */
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { // OnLocationChange is called for both the top-level content // and the subframes.
let topLevel = aWebProgress.isTopLevel;
let isSameDocument = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
); if (topLevel) {
let isReload = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
);
let isErrorPage = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE
);
// We need to clear the typed value // if the document failed to load, to make sure the urlbar reflects the // failed URI (particularly for SSL errors). However, don't clear the value // if the error page's URI is about:blank, because that causes complete // loss of urlbar contents for invalid URI errors (see bug 867957). // Another reason to clear the userTypedValue is if this was an anchor // navigation initiated by the user. // Finally, we do insert the URL if this is a same-document navigation // and the user cleared the URL manually. if ( this._browser.didStartLoadSinceLastUserTyping() ||
(isErrorPage && aLocation.spec != "about:blank") ||
(isSameDocument && this._browser.isNavigating) ||
(isSameDocument && !this._browser.userTypedValue)
) { this._browser.userTypedValue = null;
}
// If the tab has been set to "busy" outside the stateChange // handler below (e.g. by sessionStore.navigateAndRestore), and // the load results in an error page, it's possible that there // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy // attribute being removed. In this case we should remove the // attribute here. if (isErrorPage && this._tab.hasAttribute("busy")) { this._tab.removeAttribute("busy");
gBrowser._tabAttrModified(this._tab, ["busy"]);
}
if (!isSameDocument) { // If the browser was playing audio, we should remove the playing state. if (this._tab.hasAttribute("soundplaying")) {
clearTimeout(this._tab._soundPlayingAttrRemovalTimer); this._tab._soundPlayingAttrRemovalTimer = 0; this._tab.removeAttribute("soundplaying");
gBrowser._tabAttrModified(this._tab, ["soundplaying"]);
}
// If the browser was previously muted, we should restore the muted state. if (this._tab.hasAttribute("muted")) { this._tab.linkedBrowser.mute();
}
if (gBrowser.isFindBarInitialized(this._tab)) {
let findBar = gBrowser.getCachedFindBar(this._tab);
// Close the Find toolbar if we're in old-style TAF mode if (findBar.findMode != findBar.FIND_NORMAL) {
findBar.close();
}
}
// Don't clear the favicon if this tab is in the pending // state, as SessionStore will have set the icon for us even // though we're pointed at an about:blank. Also don't clear it // if the tab is in customize mode, to keep the one set by // gCustomizeMode.setTab (bug 1551239). Also don't clear it // if onLocationChange was triggered by a pushState or a // replaceState (bug 550565) or a hash change (bug 408415). if (
!this._tab.hasAttribute("pending") &&
!this._tab.hasAttribute("customizemode") &&
aWebProgress.isLoadingDocument
) { // Removing the tab's image here causes flickering, wait until the // load is complete. this._browser.mIconURL = null;
}
if (!isReload && aWebProgress.isLoadingDocument) {
let triggerer = gBrowser._getTriggeringPrincipalFromHistory( this._browser
); // Typing a url, searching or clicking a bookmark will load a new // document that is no longer tied to a navigation from the previous // content and will have a system principal as the triggerer. if (triggerer && triggerer.isSystemPrincipal) { // Reset the related tab map so that the next tab opened will be related // to this new document and not to tabs opened by the previous one.
gBrowser.clearRelatedTabs();
}
}
let userContextId = this._browser.getAttribute("usercontextid") || 0; if (this._browser.registeredOpenURI) {
let uri = this._browser.registeredOpenURI;
gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
uri.spec,
userContextId, this._tab.group?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
); deletethis._browser.registeredOpenURI;
} if (!isBlankPageURL(aLocation.spec)) {
gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
aLocation.spec,
userContextId, this._tab.group?.id,
PrivateBrowsingUtils.isWindowPrivate(window)
); this._browser.registeredOpenURI = aLocation;
// Record telemetry for URI loads in split view if (this._tab.splitview && aLocation.spec !== "about:opentabs") { const index = this._tab.splitview.tabs.indexOf(this._tab); const label = String(index + 1); // 0 -> "1" (LTR left), 1 -> "2" (LTR right)
Glean.splitview.uriCount[label].add(1);
}
}
if (this._tab != gBrowser.selectedTab) {
let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this._tab); if (tabCacheIndex != -1) {
gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
gBrowser._getSwitcher().cleanUpTabAfterEviction(this._tab);
}
}
}
if (!this._blank || this._browser.hasContentOpener) { this._callProgressListeners("onLocationChange", [
aWebProgress,
aRequest,
aLocation,
aFlags,
]); if (topLevel && !isSameDocument) { // Include the true final argument to indicate that this event is // simulated (instead of being observed by the webProgressListener). this._callProgressListeners("onContentBlockingEvent", [
aWebProgress, null, 0, true,
]);
}
}
let URILoadingWrapper = {
_normalizeLoadURIOptions(browser, loadURIOptions) { if (!loadURIOptions.triggeringPrincipal) { thrownew Error("Must load with a triggering Principal");
}
if (
loadURIOptions.userContextId &&
loadURIOptions.userContextId != browser.getAttribute("usercontextid")
) { thrownew Error("Cannot load with mismatched userContextId");
}
_loadFlagsToFixupFlags(browser, loadFlags) { // Attempt to perform URI fixup to see if we can handle this URI in chrome.
let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE; if (loadFlags & LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
} if (loadFlags & LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
} if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
} return fixupFlags;
},
// XXXgijs: If we switch to loading the URI we return from this method, // rather than redoing fixup in docshell (see bug 1815509), we need to // ensure that the loadURIOptions have the fixup flag removed here for // loads where `uriString` already parses if just passed immediately // to `newURI`. // Right now this happens in nsDocShellLoadState code. try {
let fixupInfo = Services.uriFixup.getFixupURIInfo(
uriString,
fixupFlags
); return fixupInfo.preferredURI;
} catch (e) { // getFixupURIInfo may throw. Just return null, our caller will deal.
} returnnull;
},
_updateTriggerMetadataForLoad(
browser,
uriString,
{ loadFlags, globalHistoryOptions }
) { if (globalHistoryOptions?.triggeringSponsoredURL) { if (globalHistoryOptions.triggeringSource == "newtab") {
gBrowser.SponsorProtection.addProtectedBrowser(browser);
}
try { // Browser may access URL after fixing it up, then store the URL into DB. // To match with it, fix the link up explicitly. const triggeringSponsoredURL = Services.uriFixup.getFixupURIInfo(
globalHistoryOptions.triggeringSponsoredURL, this._loadFlagsToFixupFlags(browser, loadFlags)
).fixedURI.spec;
browser.setAttribute( "triggeringSponsoredURL",
triggeringSponsoredURL
); const time =
globalHistoryOptions.triggeringSponsoredURLVisitTimeMS ||
Date.now();
browser.setAttribute("triggeringSponsoredURLVisitTimeMS", time);
browser.setAttribute( "triggeringSource",
globalHistoryOptions.triggeringSource
);
} catch (e) {}
} else {
gBrowser.SponsorProtection.removeProtectedBrowser(browser);
}
// Both of these are used to override functions on browser-custom-element.
fixupAndLoadURIString(browser, uriString, loadURIOptions = {}) { this._internalMaybeFixupLoadURI(browser, uriString, null, loadURIOptions);
},
loadURI(browser, uri, loadURIOptions = {}) { this._internalMaybeFixupLoadURI(browser, "", uri, loadURIOptions);
},
// A shared function used by both remote and non-remote browsers to // load a string URI or redirect it to the correct process.
_internalMaybeFixupLoadURI(browser, uriString, uri, loadURIOptions) { this._normalizeLoadURIOptions(browser, loadURIOptions); // Some callers pass undefined/null when calling // loadURI/fixupAndLoadURIString. Just load about:blank instead: if (!uriString && !uri) {
uri = Services.io.newURI("about:blank");
}
// We need a URI in frontend code for checking various things. Ideally // we would then also pass that URI to webnav/browsingcontext code // for loading, but we historically haven't. Changing this would alter // fixup scenarios in some non-obvious cases.
let startedWithURI = !!uri; if (!uri) { // Note: this may return null if we can't make a URI out of the input.
uri = this._fixupURIString(browser, uriString, loadURIOptions);
}
if (loadURIOptions.isCaptivePortalTab) {
browser.browsingContext.isCaptivePortalTab = true;
}
// XXX(nika): Is `browser.isNavigating` necessary anymore? // XXX(gijs): Unsure. But it mirrors docShell.isNavigating, but in the parent process // (and therefore imperfectly so).
browser.isNavigating = true;
try { // Should more generally prefer loadURI here - see bug 1815509. if (startedWithURI) {
browser.webNavigation.loadURI(uri, loadURIOptions);
} else {
browser.webNavigation.fixupAndLoadURIString(
uriString,
loadURIOptions
);
}
} finally {
browser.isNavigating = false;
}
},
};
} // end private scope for gBrowser
var StatusPanel = { // This is useful for debugging (set to `true` in the interesting state for // the panel to remain in that state).
_frozen: false,
get isVisible() { return !this.panel.hasAttribute("inactive");
},
update() { if (BrowserHandler.kiosk || this._frozen) { return;
}
let text;
let type;
let types = ["overLink"]; if (XULBrowserWindow.busyUI) {
types.push("status");
}
types.push("defaultStatus"); for (type of types) { if ((text = XULBrowserWindow[type])) { break;
}
}
// If it's a long data: URI that uses base64 encoding, truncate to // a reasonable length rather than trying to display the entire thing. // We can't shorten arbitrary URIs like this, as bidi etc might mean // we need the trailing characters for display. But a base64-encoded // data-URI is plain ASCII, so this is OK for status panel display. // (See bug 1484071.)
let textCropped = false; if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) {
text = text.substring(0, 500) + "\u2026";
textCropped = true;
}
if (this._labelElement.value != text || (text && !this.isVisible)) { this.panel.setAttribute("previoustype", this.panel.getAttribute("type")); this.panel.setAttribute("type", type);
get _labelElement() { deletethis._labelElement; return (this._labelElement = document.getElementById("statuspanel-label"));
},
set _label(val) { if (!this.isVisible) { this.panel.removeAttribute("mirror"); this.panel.removeAttribute("sizelimit");
}
if ( this.panel.getAttribute("type") == "status" && this.panel.getAttribute("previoustype") == "status"
) { // Before updating the label, set the panel's current width as its // min-width to let the panel grow but not shrink and prevent // unnecessary flicker while loading pages. We only care about the // panel's width once it has been painted, so we can do this // without flushing layout. this.panel.style.minWidth =
window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px";
} else { this.panel.style.minWidth = "";
}
if (val) { this._labelElement.value = val; if (this.panel.hidden) { this.panel.hidden = false; // This ensures that the "inactive" attribute removal triggers a // transition.
getComputedStyle(this.panel).display;
} this.panel.removeAttribute("inactive");
MousePosTracker.addListener(this);
} else { this.panel.setAttribute("inactive", "true");
MousePosTracker.removeListener(this);
}
},
_onTransitionEnd() { if (!this.isVisible) { this.panel.hidden = true;
}
},
getMouseTargetRect() {
let container = this.panel.parentNode;
let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);
_mirror() { if (this._frozen) { return;
} if (this.panel.hasAttribute("mirror")) { this.panel.removeAttribute("mirror");
} else { this.panel.setAttribute("mirror", "true");
}
if (!this.panel.hasAttribute("sizelimit")) { this.panel.setAttribute("sizelimit", "true");
}
},
};
var TabBarVisibility = {
_initialUpdateDone: false,
update(force = false) {
let isPopup = !window.toolbar.visible;
let isTaskbarTab = document.documentElement.hasAttribute("taskbartab");
let isSingleTabWindow = isPopup || isTaskbarTab;
let hasVerticalTabs =
!isSingleTabWindow &&
Services.prefs.getBoolPref("sidebar.verticalTabs", false);
// When `gBrowser` has not been initialized, we're opening a new window and // assume only a single tab is loading.
let hasSingleTab = !gBrowser || gBrowser.visibleTabs.length == 1;
// To prevent tabs being lost, hiding the tabs toolbar should only work // when only a single tab is visible or tabs are displayed elsewhere.
let hideTabsToolbar =
(isSingleTabWindow && hasSingleTab) || hasVerticalTabs;
// We only want a non-customized titlebar for popups. It should not be the // case, but if a popup window contains more than one tab we re-enable // titlebar customization and display tabs.
CustomTitlebar.allowedBy("non-popup", !(isPopup && hasSingleTab));
// Update the browser chrome.
let tabsToolbar = document.getElementById("TabsToolbar");
let navbar = document.getElementById("nav-bar");
gNavToolbox.toggleAttribute("tabs-hidden", hideTabsToolbar); // Should the nav-bar look and function like a titlebar?
navbar.classList.toggle( "browser-titlebar",
CustomTitlebar.enabled && hideTabsToolbar
);
if (
hideTabsToolbar == tabsToolbar.collapsed &&
!force && this._initialUpdateDone
) { // No further updates needed, `TabsToolbar` already matches the expected // visibilty. return;
} this._initialUpdateDone = true;
tabsToolbar.collapsed = hideTabsToolbar;
// Stylize close menu items based on tab visibility. When a window will only // ever have a single tab, only show the option to close the tab, and // simplify the text since we don't need to disambiguate from closing the window.
document.getElementById("menu_closeWindow").hidden = hideTabsToolbar;
document.l10n.setAttributes(
document.getElementById("menu_close"),
hideTabsToolbar
? "tabbrowser-menuitem-close"
: "tabbrowser-menuitem-close-tab"
);
},
};
let splitViews = new Set(); // bug1973996: This call is not guaranteed to complete // before the saved groups menu is populated for (let tab of this.contextTabs) {
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
// Add unique split views for count info below if (tab.splitview) {
splitViews.add(tab.splitview);
}
}
let disabled = gBrowser.tabs.length == 1;
let tabCountInfo = JSON.stringify({
tabCount: this.contextTabs.length,
});
let splitViewCountInfo = JSON.stringify({
splitViewCount: splitViews.size,
});
var menuItems = aPopupMenu.getElementsByAttribute( "tbattr", "tabbrowser-multiple"
); for (let menuItem of menuItems) {
menuItem.disabled = disabled;
}
let contextNewTabButton = document.getElementById("context_openANewTab"); // update context menu item strings for vertical tabs
document.l10n.setAttributes(
contextNewTabButton,
gBrowser.tabContainer?.verticalMode
? "tab-context-new-tab-open-vertical"
: "tab-context-new-tab-open"
);
// Session store
let closedCount = SessionStore.getLastClosedTabCount(window);
document
.getElementById("History:UndoCloseTab")
.toggleAttribute("disabled", closedCount == 0);
document.l10n.setArgs(document.getElementById("context_undoCloseTab"), {
tabCount: closedCount,
});
// Show/hide fullscreen context menu items and set the // autohide item's checked state to mirror the autohide pref.
showFullScreenViewContextMenuItems(aPopupMenu);
// #context_moveTabToNewGroup is a simplified context menu item that only // appears if there are no existing tab groups available to move the tab to.
let contextMoveTabToNewGroup = document.getElementById( "context_moveTabToNewGroup"
);
let contextMoveTabToGroup = document.getElementById( "context_moveTabToGroup"
);
let contextUngroupTab = document.getElementById("context_ungroupTab");
let contextMoveSplitViewToNewGroup = document.getElementById( "context_moveSplitViewToNewGroup"
);
let contextUngroupSplitView = document.getElementById( "context_ungroupSplitView"
);
let isAllSplitViewTabs = this.contextTabs.every(
contextTab => contextTab.splitview
);
if (gBrowser._tabGroupsEnabled) {
let selectedGroupCount = new Set( // The filter removes the "null" group for ungrouped tabs.
this.contextTabs.map(t => t.group).filter(g => g)
).size;
let openGroupsToMoveTo = gBrowser.getAllTabGroups({
sortByLastSeenActive: true,
});
// Determine whether or not the "current" tab group should appear in the // "move tab to group" context menu. if (selectedGroupCount == 1) {
let groupToFilter = this.contextTabs[0].group; if (groupToFilter && this.contextTabs.every(t => t.group)) {
openGroupsToMoveTo = openGroupsToMoveTo.filter(
group => group !== groupToFilter
);
}
}
// Populate the saved groups context menu // Only enable in non-private windows, or if at least one of the tabs is // considered saveable
let savedGroupsToMoveTo = []; if (
!PrivateBrowsingUtils.isWindowPrivate(window) &&
SessionStore.shouldSaveTabsToGroup(this.contextTabs)
) {
savedGroupsToMoveTo = SessionStore.getSavedTabGroups();
}
let contextAddNote = document.getElementById("context_addNote");
let contextEditNote = document.getElementById("context_editNote"); if (gBrowser._tabNotesEnabled) { // Tab notes behaviour is disabled if a user has a selection of tabs that // contains more than one canonical URL.
let multiselectingDiverseUrls =
this.multiselected &&
!this.contextTabs.every(
t => t.canonicalUrl === this.contextTabs[0].canonicalUrl
);
this.removeNewBadge(contextMoveTabToNewSplitView);
this.removeNewBadge(contextSeparateSplitView); if (!Services.prefs.getBoolPref("browser.tabs.splitview.hasUsed", true)) {
this.addNewBadge(contextMoveTabToNewSplitView);
this.addNewBadge(contextSeparateSplitView);
}
}
// Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
document.getElementById("context_reloadTab").hidden = this.multiselected;
document.getElementById("context_reloadSelectedTabs").hidden =
!this.multiselected;
let unloadTabItem = document.getElementById("context_unloadTab"); if (gBrowser._unloadTabInContextMenu) { // linkedPanel is false if the tab is already unloaded // Cannot unload about: pages, etc., so skip browsers that are not remote
let unloadableTabs = this.contextTabs.filter(
t => t.linkedPanel && t.linkedBrowser?.isRemoteBrowser
);
unloadTabItem.hidden = unloadableTabs.length === 0;
unloadTabItem.setAttribute( "data-l10n-args",
JSON.stringify({ tabCount: unloadableTabs.length })
);
} else {
unloadTabItem.hidden = true;
}
// Show Play Tab menu item if the tab has attribute activemedia-blocked
document.getElementById("context_playTab").hidden = !(
this.contextTab.activeMediaBlocked && !this.multiselected
);
document.getElementById("context_playSelectedTabs").hidden = !(
this.contextTab.activeMediaBlocked && this.multiselected
);
// Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
let hasAboutOpenTabsTab = this.contextTabs.some(
t => t.linkedBrowser.currentURI.spec === "about:opentabs"
);
let contextPinTab = document.getElementById("context_pinTab");
contextPinTab.hidden =
this.contextTab.pinned || this.multiselected || hasAboutOpenTabsTab;
let contextUnpinTab = document.getElementById("context_unpinTab");
contextUnpinTab.hidden = !this.contextTab.pinned || this.multiselected;
let contextPinSelectedTabs = document.getElementById( "context_pinSelectedTabs"
);
contextPinSelectedTabs.hidden =
this.contextTab.pinned || !this.multiselected || hasAboutOpenTabsTab;
let contextUnpinSelectedTabs = document.getElementById( "context_unpinSelectedTabs"
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
this
);
// Move Tab items
let contextMoveTabOptions = document.getElementById( "context_moveTabOptions"
); // gBrowser.visibleTabs excludes tabs in collapsed groups, // which we want to include in calculations for Move Tab items
let visibleOrCollapsedTabs = gBrowser.tabs.filter(
t => t.isOpen && !t.hidden
);
let allTabsSelected =
visibleOrCollapsedTabs.length == 1 ||
visibleOrCollapsedTabs.every(t => t.multiselected);
contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
contextMoveTabOptions.disabled = this.contextTab.hidden || allTabsSelected;
let selectedTabs = gBrowser.selectedTabs;
let contextMoveTabToEnd = document.getElementById("context_moveToEnd");
let allSelectedTabsAdjacent = selectedTabs.every(
(element, index, array) => { return array.length > index + 1
? element._tPos + 1 == array[index + 1]._tPos
: true;
}
);
let lastVisibleTab = visibleOrCollapsedTabs.at(-1);
let lastTabToMove = this.contextTabs.at(-1);
let isLastPinnedTab = false; if (lastTabToMove.pinned) {
let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
isLastPinnedTab = !sibling || !sibling.pinned;
}
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById("context_duplicateTab").hidden = this.multiselected;
document.getElementById("context_duplicateTabs").hidden =
!this.multiselected;
let closeTabsToTheStartItem = document.getElementById( "context_closeTabsToTheStart"
);
// update context menu item strings for vertical tabs
document.l10n.setAttributes(
closeTabsToTheStartItem,
gBrowser.tabContainer?.verticalMode
? "close-tabs-to-the-start-vertical"
: "close-tabs-to-the-start"
);
let closeTabsToTheEndItem = document.getElementById( "context_closeTabsToTheEnd"
);
// update context menu item strings for vertical tabs
document.l10n.setAttributes(
closeTabsToTheEndItem,
gBrowser.tabContainer?.verticalMode
? "close-tabs-to-the-end-vertical"
: "close-tabs-to-the-end"
);
// Disable "Close Tabs to the Left/Right" if there are no tabs // preceding/following it.
let noTabsToStart = !gBrowser._getTabsToTheStartFrom(this.contextTab)
.length;
closeTabsToTheStartItem.disabled = noTabsToStart;
let noTabsToEnd = !gBrowser._getTabsToTheEndFrom(this.contextTab).length;
closeTabsToTheEndItem.disabled = noTabsToEnd;
// Disable "Close other Tabs" if there are no unpinned tabs.
let unpinnedTabsToClose = this.multiselected
? gBrowser.openTabs.filter(
t => !t.multiselected && !t.pinned && !t.hidden
).length
: gBrowser.openTabs.filter(
t => t != this.contextTab && !t.pinned && !t.hidden
).length;
let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;
// Update the close item with how many tabs will close.
document
.getElementById("context_closeTab")
.setAttribute("data-l10n-args", tabCountInfo);
// Disable "Close Multiple Tabs" if all sub menuitems are disabled
document.getElementById("context_closeTabOptions").disabled =
closeTabsToTheStartItem.disabled &&
closeTabsToTheEndItem.disabled &&
closeOtherTabsItem.disabled;
// Hide "Bookmark Tab…" for multiselection. // Update its state if visible.
let bookmarkTab = document.getElementById("context_bookmarkTab");
bookmarkTab.hidden = this.multiselected;
// Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
let bookmarkMultiSelectedTabs = document.getElementById( "context_bookmarkSelectedTabs"
);
bookmarkMultiSelectedTabs.hidden = !this.multiselected;
let contentSharingShareTabs = document.getElementById( "context_shareSelectedTabs"
);
let toggleMute = document.getElementById("context_toggleMuteTab");
let toggleMultiSelectMute = document.getElementById( "context_toggleMuteSelectedTabs"
);
// Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
toggleMute.hidden = this.multiselected;
toggleMultiSelectMute.hidden = !this.multiselected;
_createTabGroupMenuItem(group, isSaved) {
let item = document.createXULElement("menuitem");
item.setAttribute("tab-group-id", group.id);
// Open groups have labels, and saved groups have names
let label = group.label ?? group.name; if (label) {
item.setAttribute("label", label);
} else {
document.l10n.setAttributes(item, "tab-context-unnamed-group");
}
item.classList.add("menuitem-iconic", "tab-group-icon"); if (isSaved) {
item.classList.add("tab-group-icon-closed");
}
createReopenInContainerMenu(event) {
createUserContextMenu(event, {
isContextMenu: true,
excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
});
},
duplicateSelectedTabs() {
let newIndex = this.contextTabs.at(-1)._tPos + 1; for (let tab of this.contextTabs) {
let newTab = SessionStore.duplicateTab(window, tab); if (tab.group) {
Glean.tabgroup.tabInteractions.duplicate.add();
}
gBrowser.moveTabTo(newTab, { tabIndex: newIndex++ });
}
},
reopenInContainer(event) {
let userContextId = parseInt(
event.target.getAttribute("data-usercontextid")
);
for (let tab of this.contextTabs) { if (tab.getAttribute("usercontextid") == userContextId) { continue;
}
/* Create a triggering principal that is able to load the new tab Forcontentprincipalsthatareabout:chrome:orresource:weneedsystemtoloadthem. AnythingotherthansystemprincipalneedstohavethenewuserContextId.
*/
let triggeringPrincipal;
if (tab.linkedPanel) {
triggeringPrincipal = tab.linkedBrowser.contentPrincipal;
} else { // For lazy tab browsers, get the original principal // from SessionStore
let tabState = JSON.parse(SessionStore.getTabState(tab));
try {
triggeringPrincipal = E10SUtils.deserializePrincipal(
tabState.triggeringPrincipal_base64
);
} catch (ex) { continue;
}
}
if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) { // Ensure that we have a null principal if we couldn't // deserialize it (for lazy tab browsers) ... // This won't always work however is safe to use.
triggeringPrincipal =
Services.scriptSecurityManager.createNullPrincipal({ userContextId });
} elseif (triggeringPrincipal.isContentPrincipal) {
triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
triggeringPrincipal,
{
userContextId,
}
);
}
// When using the tab context menu to create a group from the all tabs // panel, make sure we close that panel so that it doesn't obscure the tab // group creation panel.
gTabsPanel.hideAllTabsPanel();
},
moveSplitViewToNewGroup() {
let insertBefore = this.contextTab; if (insertBefore._tPos < gBrowser.pinnedTabCount) {
insertBefore = gBrowser.tabs[gBrowser.pinnedTabCount];
} elseif (this.contextTab.group) {
insertBefore = this.contextTab.group;
} elseif (this.contextTab.splitview) {
insertBefore = this.contextTab.splitview;
}
let tabsAndSplitViews = []; for (const contextTab of this.contextTabs) { if (contextTab.splitView) { if (!tabsAndSplitViews.includes(contextTab.splitView)) {
tabsAndSplitViews.push(contextTab.splitView);
}
} else {
tabsAndSplitViews.push(contextTab);
}
}
gBrowser.addTabGroup(tabsAndSplitViews, {
insertBefore,
isUserTriggered: true,
telemetryUserCreateSource: "tab_menu",
});
gBrowser.selectedTab = this.contextTabs[0];
// When using the tab context menu to create a group from the all tabs // panel, make sure we close that panel so that it doesn't obscure the tab // group creation panel.
gTabsPanel.hideAllTabsPanel();
},
/** *@param{MozTabbrowserTabGroup}group
*/
moveTabsToGroup(group) {
let elementsToMove = new Set(); for (let tab of this.contextTabs) {
elementsToMove.add(tab.splitview ?? tab);
}
group.addTabs(
Array.from(elementsToMove.values()),
gBrowser.TabMetrics.userTriggeredContext(
gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU
)
);
group.documentGlobal.focus();
},
addTabsToSavedGroup(groupId) {
let seen = new Set();
let tabs = []; for (let tab of this.contextTabs) { if (tab.splitview) { for (let splitTab of tab.splitview.tabs) { if (!seen.has(splitTab)) {
seen.add(splitTab);
tabs.push(splitTab);
}
}
} elseif (!seen.has(tab)) {
seen.add(tab);
tabs.push(tab);
}
}
SessionStore.addTabsToSavedGroup(
groupId,
tabs,
gBrowser.TabMetrics.userTriggeredContext(
gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU
)
);
gBrowser.removeTabs(tabs, {
animate: true,
...gBrowser.TabMetrics.userTriggeredContext(
gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP
),
});
},
ungroupTabsAndSplitViews() {
let splitViews = new Set(); for (const tab of this.contextTabs) { if (tab.splitview && !splitViews.has(tab.splitview)) {
splitViews.add(tab.splitview);
gBrowser.ungroupSplitView(tab.splitview);
} elseif (!tab.splitview) {
gBrowser.ungroupTab(tab);
}
}
},
moveTabsToSplitView() {
let insertBefore = this.contextTabs.includes(gBrowser.selectedTab)
? gBrowser.selectedTab
: this.contextTabs[0];
let tabsToAdd = this.contextTabs;
// Ensure selected tab is always first in split view const selectedTabIndex = tabsToAdd.indexOf(gBrowser.selectedTab); if (selectedTabIndex > -1 && selectedTabIndex != 0) { const [removed] = tabsToAdd.splice(selectedTabIndex, 1);
tabsToAdd.unshift(removed);
}
let newTab = null;
let trigger; if (this.contextTabs.length < 2) { // Open new tab to split with context tab
newTab = gBrowser.addTrustedTab("about:opentabs");
tabsToAdd = [this.contextTabs[0], newTab];
trigger = "menu_add";
} else {
trigger = "menu_open";
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.736Bemerkung:
(vorverarbeitet am 2026-08-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.