/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env mozilla/browser-window */
{ // start private scope for Tabbrowser /** * A set of known icons to use for internal pages. These are hardcoded so we can * start loading them faster than FaviconLoader would normally find them.
*/ const FAVICON_DEFAULTS = { "about:newtab": "chrome://branding/content/icon32.png", "about:home": "chrome://branding/content/icon32.png", "about:welcome": "chrome://branding/content/icon32.png", "about:privatebrowsing": "chrome://browser/skin/privatebrowsing/favicon.svg",
};
/** * Updates the User Context UI indicators if the browser is in a non-default context
*/ function updateUserContextUIIndicator() { function replaceContainerClass(classType, element, value) {
let prefix = "identity-" + classType + "-"; if (value && element.classList.contains(prefix + value)) { return;
} for (let className of element.classList) { if (className.startsWith(prefix)) {
element.classList.remove(className);
}
} if (value) {
element.classList.add(prefix + value);
}
}
let hbox = document.getElementById("userContext-icons");
let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); if (!userContextId) {
replaceContainerClass("color", hbox, "");
hbox.hidden = true; return;
}
let identity =
ContextualIdentityService.getPublicIdentityFromId(userContextId); if (!identity) {
replaceContainerClass("color", hbox, "");
hbox.hidden = true; return;
}
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;
}
// 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. After a language change, the // window title will update at the next tab or location change.
document.querySelector("title").removeAttribute("data-l10n-id");
/** * Binding from browser to tab
*/
_tabForBrowser = new WeakMap();
/** * `_createLazyBrowser` will define properties on the unbound lazy browser * which correspond to properties defined in MozBrowser which will be bound to * the browser when it is inserted into the document. If any of these * properties are accessed by consumers, `_insertBrowser` is called and * the browser is inserted to ensure that things don't break. This list * provides the names of properties that may be called while the browser * is in its unbound (lazy) state.
*/
_browserBindingProperties = [ "canGoBack", "canGoForward", "goBack", "goForward", "permitUnload", "reload", "reloadWithFlags", "stop", "loadURI", "fixupAndLoadURIString", "gotoIndex", "currentURI", "documentURI", "remoteType", "preferences", "imageDocument", "isRemoteBrowser", "messageManager", "getTabBrowser", "finder", "fastFind", "sessionHistory", "contentTitle", "characterSet", "fullZoom", "textZoom", "tabHasCustomZoom", "webProgress", "addProgressListener", "removeProgressListener", "audioPlaybackStarted", "audioPlaybackStopped", "resumeMedia", "mute", "unmute", "blockedPopups", "lastURI", "purgeSessionHistory", "stopScroll", "startScroll", "userTypedValue", "userTypedClear", "didStartLoadSinceLastUserTyping", "audioMuted",
];
_removingTabs = new Set();
_multiSelectedTabsSet = new WeakSet();
_lastMultiSelectedTabRef = null;
_clearMultiSelectionLocked = false;
_clearMultiSelectionLockedOnce = false;
_multiSelectChangeStarted = false;
_multiSelectChangeAdditions = new Set();
_multiSelectChangeRemovals = new Set();
_multiSelectChangeSelected = false;
/** * Tab close requests are ignored if the window is closing anyway, * e.g. when holding Ctrl+W.
*/
_windowIsClosing = false;
preloadedBrowser = null;
/** * This defines a proxy which allows us to access browsers by * index without actually creating a full array of browsers.
*/
browsers = new Proxy([], {
has: (target, name) => { if (typeof name == "string" && Number.isInteger(parseInt(name))) { return name in gBrowser.tabs;
} returnfalse;
},
get: (target, name) => { if (name == "length") { return gBrowser.tabs.length;
} if (typeof name == "string" && Number.isInteger(parseInt(name))) { if (!(name in gBrowser.tabs)) { return undefined;
} return gBrowser.tabs[name].linkedBrowser;
} return target[name];
},
});
/** * List of browsers whose docshells must be active in order for print preview * to work.
*/
_printPreviewBrowsers = new Set();
_switcher = null;
_soundPlayingAttrRemovalTimer = 0;
_hoverTabTimer = null;
get _tabGroupsEnabled() { return NimbusFeatures.tabGroups.getVariable("enabled");
}
get tabs() { returnthis.tabContainer.allTabs;
}
get tabGroups() { returnthis.tabContainer.allGroups;
}
/** * Returns all tabs in the current window, including hidden tabs and tabs * in collapsed groups, but excluding closing tabs and the Firefox View tab.
*/
get openTabs() { returnthis.tabContainer.openTabs;
}
/** * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups.
*/
get visibleTabs() { returnthis.tabContainer.visibleTabs;
}
get pinnedTabCount() { for (var i = 0; i < this.tabs.length; i++) { if (!this.tabs[i].pinned) { break;
}
} return i;
}
set selectedTab(val) { if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
document.documentElement.hasAttribute("window-modal-open") ||
(gNavToolbox.collapsed && !this._allowTabChange)
) { return;
} // Update the tab this.tabbox.selectedTab = val;
}
get selectedTab() { returnthis._selectedTab;
}
get selectedBrowser() { returnthis._selectedBrowser;
}
_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;
// 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 (tabArgument && tabArgument.linkedBrowser) {
remoteType = tabArgument.linkedBrowser.remoteType;
initialBrowsingContextGroupId =
tabArgument.linkedBrowser.browsingContext?.group.id;
} elseif (openWindowInfo) {
userContextId = openWindowInfo.originAttributes.userContextId; if (openWindowInfo.isRemote) {
remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
} 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") {
let oa = E10SUtils.predictOriginAttributes({
window,
userContextId,
});
remoteType = E10SUtils.getRemoteTypeForURI(
uriToLoad,
gMultiProcessBrowser,
gFissionBrowser,
E10SUtils.DEFAULT_REMOTE_TYPE, null,
oa
);
} 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.
// 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 (userContextId) {
tab.setAttribute("usercontextid", userContextId);
ContextualIdentityService.setTabStyle(tab);
}
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
);
}
/** * BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT * MAKE SURE TO ADD IT HERE AS WELL.
*/
get canGoBack() { returnthis.selectedBrowser.canGoBack;
}
get canGoBackIgnoringUserInteraction() { returnthis.selectedBrowser.canGoBackIgnoringUserInteraction;
}
get canGoForward() { returnthis.selectedBrowser.canGoForward;
}
/** * Get the already constructed findbar
*/
getCachedFindBar(aTab = this.selectedTab) { return aTab._findBar;
}
/** * Get the findbar, and create it if it doesn't exist. * @return the find bar (or null if the window or tab is closed/closing in the interim).
*/
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;
}
/** * Create a findbar instance. * @param aTab the tab to create the find bar for. * @return the created findbar, or null if the window or tab is closed/closing.
*/
async _createFindBar(aTab) {
let findBar = document.createXULElement("findbar");
let browser = this.getBrowserForTab(aTab);
_notifyPinnedStatus(aTab) { // browsingContext is expected to not be defined on discarded tabs. if (aTab.linkedBrowser.browsingContext) {
aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned;
}
if (this.tabContainer.verticalMode) { this._handleTabMove(aTab, () => { // we remove this attribute first, so that allTabs represents // the moving of a tab from the vertical pinned tabs container // and back into arrowscrollbox.
aTab.removeAttribute("pinned"); this.tabContainer.arrowScrollbox.prepend(aTab);
});
} else { this.moveTabTo(aTab, this.pinnedTabCount - 1, {
forceStandaloneTab: true,
});
aTab.removeAttribute("pinned");
}
aTab.style.marginInlineStart = "";
aTab._pinnedUnscrollable = false; this._updateTabBarForPinnedTabs(); this._notifyPinnedStatus(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); 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.mProgressListeners, aArguments);
}
if (aCallTabsListeners) {
aArguments.unshift(aBrowser);
/** * Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS.
*/
setDefaultIcon(aTab, aURI) { if (aURI && aURI.spec in FAVICON_DEFAULTS) { this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]);
}
}
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
if (
aIconURL &&
!aLoadingPrincipal &&
!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) { if (aLoadingPrincipal) {
aTab.setAttribute("iconloadingprincipal", aLoadingPrincipal);
} else {
aTab.removeAttribute("iconloadingprincipal");
}
aTab.setAttribute("image", aIconURL);
} else {
aTab.removeAttribute("image");
aTab.removeAttribute("iconloadingprincipal");
} this._tabAttrModified(aTab, ["image"]);
}
// The aOriginalURL argument is currently only used by tests. this._callProgressListeners(browser, "onLinkIconAvailable", [
aIconURL,
aOriginalURL,
]);
}
getWindowTitleForBrowser(aBrowser) {
let docElement = document.documentElement;
let title = "";
let dataSuffix =
docElement.getAttribute("privatebrowsingmode") == "temporary"
? "Private"
: "Default";
// 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(aBrowser.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(aBrowser); 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, "");
}
if (title) { // We're using a function rather than just using `title` as the // new substring to avoid `$$`, `$'` etc. having a special // meaning to `replace`. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter // and the documentation for functions for more info about this. return docElement.dataset["contentTitle" + dataSuffix]
.replace("CONTENTTITLE", () => title)
.replace( "PROFILENAME",
() =>
SelectableProfileService?.currentProfile?.name.replace(
/\0/g, ""
) ?? ""
);
}
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);
if (!aForceUpdate) {
TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
// 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.popupBlocker.getBlockedPopupCount();
let newBrowserPopupsBlocked =
newBrowser.popupBlocker.getBlockedPopupCount(); if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
newBrowser.popupBlocker.updateBlockedPopupsUI();
}
// 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.ownerGlobal == 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);
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) {
TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
}
}
_adjustFocusBeforeTabSwitch(oldTab, newTab) { if (this._previewMode) { return;
}
let oldBrowser = oldTab.linkedBrowser;
let newBrowser = newTab.linkedBrowser;
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.
tab._sharingState = tab._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 (tab._sharingState == null) {
tab._sharingState = {};
}
tab._sharingState = Object.assign(tab._sharingState, aState);
if ("webRTC" in aState) { if (tab._sharingState.webRTC?.sharing) { if (tab._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 state = Object.assign(
{},
aTab._sharingState && aTab._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, "");
}
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 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) {
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;
} try {
browser.fixupAndLoadURIString(aURIs[0], {
loadFlags,
postData: postDatas && postDatas[0],
triggeringPrincipal,
csp,
});
} catch (e) { // Ignore failure in case a URI is wrong, so we can continue // opening the next ones.
}
} else {
let params = {
allowInheritPrincipal,
ownerTab: owner,
skipAnimation: multiple,
allowThirdPartyFixup,
postData: postDatas && postDatas[0],
userContextId,
triggeringPrincipal,
bulkOrderedOpen: multiple,
csp,
fromExternal,
}; if (newIndex > -1) {
params.index = newIndex;
}
firstTabAdded = this.addTab(aURIs[0], params); 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,
csp,
fromExternal,
}; if (targetTabIndex > -1) {
params.index = ++tabNum;
} this.addTab(aURIs[i], params);
}
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);
aBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
// We'll be creating a new listener, so destroy the old one.
listener.destroy();
let oldDroppedLinkHandler = aBrowser.droppedLinkHandler;
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);
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;
}
let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });
aOptions.remoteType = E10SUtils.getRemoteTypeForURI(
aURL,
gMultiProcessBrowser,
gFissionBrowser,
oldRemoteType,
aBrowser.currentURI,
oa
);
// 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)();
if (gMultiProcessBrowser || remoteType) {
b.setAttribute("maychangeremoteness", "true");
}
if (userContextId) {
b.setAttribute("usercontextid", userContextId);
}
if (remoteType) {
b.setAttribute("remoteType", remoteType);
b.setAttribute("remote", "true");
}
if (!isPreloadBrowser) {
b.setAttribute("autocompletepopup", "PopupAutoComplete");
}
/* * This attribute is meant to describe if the browser is the * preloaded browser. When the preloaded browser is created, the * 'preloadedState' attribute for that browser is set to "preloaded", and * when a new tab is opened, and it is time to show that preloaded * browser, the 'preloadedState' attribute for that browser is removed. * * See more details on Bug 1420285.
*/ if (isPreloadBrowser) {
b.setAttribute("preloadedState", "preloaded");
}
// 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 (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.getAttribute("remote") == "true"; 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"; // Avoid recreating the same nsIURI object over and over again...
let uri; if (browser._cachedCurrentURI) {
uri = browser._cachedCurrentURI;
} else {
uri = browser._cachedCurrentURI = Services.io.newURI(url);
}
let oa = E10SUtils.predictOriginAttributes({
browser,
userContextId: aTab.getAttribute("usercontextid"),
}); return E10SUtils.getRemoteTypeForURI(
url,
gMultiProcessBrowser,
gFissionBrowser,
undefined,
uri,
oa
);
}; 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. 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";
discardBrowser(aTab, aForceDiscard) { "use strict";
let browser = aTab.linkedBrowser;
if (!this._mayDiscardBrowser(aTab, aForceDiscard)) { returnfalse;
}
// Reset sharing state. if (aTab._sharingState) { this.resetBrowserSharing(browser);
}
webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);
// 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);
// 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);
}
let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
aTab.dispatchEvent(evt); returntrue;
}
/** * Loads a tab with a default null principal unless specified
*/
addWebTab(aURI, params = {}) { if (!params.triggeringPrincipal) {
params.triggeringPrincipal =
Services.scriptSecurityManager.createNullPrincipal({
userContextId: params.userContextId,
});
} if (params.triggeringPrincipal.isSystemPrincipal) { thrownew Error( "System principal should never be passed into addWebTab()"
);
} returnthis.addTab(aURI, params);
}
/** * Must only be used sparingly for content that came from Chrome context * If in doubt use addWebTab * @param {string} aURI * @param {object} [options] * @see this.addTab options * @returns {MozTabbrowserTab|null}
*/
addTrustedTab(aURI, options = {}) {
options.triggeringPrincipal =
Services.scriptSecurityManager.getSystemPrincipal(); returnthis.addTab(aURI, options);
}
/** * @param {string} uriString * @param {object} options * @param {MozTabbrowserTabGroup} [options.tabGroup] * A related tab group where this tab should be added, when applicable. * When present, the tab is expected to reside in this tab group. When * absent, the tab is expected to be a standalone tab. * @returns {MozTabbrowserTab|null} * The new tab. The return value will be null if the tab couldn't be * created; this shouldn't normally happen, and an error will be logged * to the console if it does.
*/
addTab(
uriString,
{
allowInheritPrincipal,
allowThirdPartyFixup,
bulkOrderedOpen,
charset,
createLazyBrowser,
disableTRR,
eventDetail,
focusUrlBar,
forceNotRemote,
forceAllowDataURI,
fromExternal,
inBackground = true,
index,
lazyTabTitle,
name,
noInitialLabel,
openWindowInfo,
openerBrowser,
originPrincipal,
originStoragePrincipal,
ownerTab,
pinned,
postData,
preferredRemoteType,
referrerInfo,
relatedToCurrent,
initialBrowsingContextGroupId,
skipAnimation,
skipBackgroundNotify,
tabGroup,
triggeringPrincipal,
userContextId,
csp,
skipLoad = createLazyBrowser,
insertTab = true,
globalHistoryOptions,
triggeringRemoteType,
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
} = {}
) { // all callers of addTab that pass a params object need to pass // a valid triggeringPrincipal. if (!triggeringPrincipal) { thrownew Error( "Required argument triggeringPrincipal missing within addTab"
);
}
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, {
index,
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,
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 && !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 this._fireTabOpen(t, eventDetail);
// 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");
});
}
}
/** * Adds a new tab group. * * @param {object[]} tabs * The set of tabs to include in the group. * @param {object} [options] * @param {string} [options.id] * Optionally assign an ID to the tab group. Useful when rebuilding an * existing group e.g. when restoring. A pseudorandom string will be * generated if not set. * @param {string} [options.color] * Color for the group label. See tabgroup-menu.js for possible values. * If no color specified, will attempt to assign an unused group color. * @param {string} [options.label] * Label for the group. * @param {MozTabbrowserTab} [options.insertBefore] * An optional argument that accepts a single tab, which, if passed, will * cause the group to be inserted just before this tab in the tab strip. By * default, the group will be created at the end of the tab strip. * @param {boolean} [options.showCreateUI] * Set this to true to show the post-creation group edtior.
*/
addTabGroup(
tabs,
{
id = null,
color = null,
label = "",
insertBefore = null,
showCreateUI = false,
} = {}
) { if (!tabs?.length) { thrownew Error("Cannot create tab group with zero tabs");
}
if (!color) {
color = this.tabGroupMenu.nextUnusedColor;
}
if (!id) {
id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
}
let group = this._createTabGroup(id, color, false, label); this.tabContainer.insertBefore(
group,
insertBefore?.group ?? insertBefore
);
group.addTabs(tabs);
// 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;
}
/** * Removes the tab group. This has the effect of closing all the tabs * in the group. * * @param {MozTabbrowserTabGroup} [group] * The tab group to remove. * @param {object} [options] * Options to use when removing tabs. @see removeTabs for more info.
*/
removeTabGroup(group, options = {}) { if (this.tabGroupMenu.panel.state != "closed") { this.tabGroupMenu.panel.hidePopup(options.animate);
}
// 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 })
);
// Skip session store on a per-tab basis since these tabs will get // recorded as part of a group
options.skipSessionStore = true;
this.removeTabs(group.tabs, options);
}
/** * Removes a tab from a group. This has no effect on tabs that are not * already in a group. * * @param tab The tab to ungroup
*/
ungroupTab(tab) { if (!tab.group) { return;
}
getTabGroupById(id) { for (const win of BrowserWindowTracker.orderedWindows) { 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 */
}
let lazyBrowserURI; if (createLazyBrowser && uriString != "about:blank") {
lazyBrowserURI = aURIObject;
uriString = "about:blank";
}
/** * @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;
}
_createBrowserForTab(
tab,
{
uriString,
uri,
name,
preferredRemoteType,
openerBrowser,
uriIsAboutBlank,
referrerInfo,
forceNotRemote,
initialBrowsingContextGroupId,
openWindowInfo,
skipLoad,
}
) { // 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;
var oa = E10SUtils.predictOriginAttributes({ window, userContextId });
// 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 = E10SUtils.getRemoteTypeForURI(
referrerInfo.originalReferrer.spec,
gMultiProcessBrowser,
gFissionBrowser,
E10SUtils.DEFAULT_REMOTE_TYPE, null,
oa
);
}
let remoteType = forceNotRemote
? E10SUtils.NOT_REMOTE
: E10SUtils.getRemoteTypeForURI(
uriString,
gMultiProcessBrowser,
gFissionBrowser,
preferredRemoteType, null,
oa
);
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,
disableTRR,
forceAllowDataURI,
skipLoad,
referrerInfo,
charset,
postData,
csp,
globalHistoryOptions,
triggeringRemoteType,
schemelessInput,
hasValidUserGestureActivation,
textDirectiveUserActivation,
}
) { 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) {
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.
loadFlags |= LOAD_FLAGS_FIRST_LOAD;
} if (!allowInheritPrincipal) {
loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
} if (disableTRR) {
loadFlags |= LOAD_FLAGS_DISABLE_TRR;
} if (forceAllowDataURI) {
loadFlags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
} try {
browser.fixupAndLoadURIString(uriString, {
loadFlags,
triggeringPrincipal,
referrerInfo,
charset,
postData,
csp,
globalHistoryOptions,
triggeringRemoteType,
schemelessInput,
hasValidUserGestureActivation,
textDirectiveUserActivation,
});
} catch (ex) {
console.error(ex);
}
}
}
/** * @param {boolean} restoreTabsLazily * @param {number} selectTab see SessionStoreInternal.restoreTabs { aSelectTab } * @param {TabStateData[]} tabDataList * @param {TabGroupStateData[]} tabGroupDataList * @returns {MozTabbrowserTab[]}
*/
createTabsForSessionRestore(
restoreTabsLazily,
selectTab,
tabDataList,
tabGroupDataList
) {
let tabs = [];
let tabsFragment = document.createDocumentFragment();
let tabToSelect = null;
let hiddenTabs = new Map();
let shouldUpdateForPinnedTabs = false; /** @type {Map<TabGroupStateData['id'], TabGroupWorkingData>} */
let tabGroupWorkingData = new Map();
for (const tabGroupData of tabGroupDataList) {
tabGroupWorkingData.set(tabGroupData.id, {
stateData: tabGroupData,
node: undefined,
containingTabsFragment: document.createDocumentFragment(),
});
}
// 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;
}
// 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;
}
}
tabs.push(tab);
if (tabData.pinned) { // Calling `pinTab` calls `moveTabTo`, which assumes the tab is // inserted in the DOM. If the tab is not yet in the DOM, // just insert it in the right place from the start. if (!tab.parentNode) {
tab._tPos = this.pinnedTabCount; this.tabContainer.insertBefore(tab, this.tabs[this.pinnedTabCount]);
tab.toggleAttribute("pinned", true); this.tabContainer._invalidateCachedTabs(); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); this._notifyPinnedStatus(tab); // Once we're done adding all tabs, _updateTabBarForPinnedTabs // needs calling:
shouldUpdateForPinnedTabs = true;
}
} 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) {
tabGroup.containingTabsFragment.appendChild(tab); // 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);
}
tabsFragment.appendChild(tab); 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 DOM nodes for (const tabGroup of tabGroupWorkingData.values()) { if (tabGroup.node) {
tabGroup.node.appendChild(tabGroup.containingTabsFragment);
}
}
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(); if (shouldUpdateForPinnedTabs) { this._updateTabBarForPinnedTabs();
}
// 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--) {
let tab = tabs[i]; if (tab._tPos > 0) { this.moveTabTo(tab, 0);
}
}
}
moveTabsToEnd(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab]; for (let tab of tabs) { if (tab._tPos < this.tabs.length - 1) { this.moveTabTo(tab, this.tabs.length - 1);
}
}
}
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;
}
/** * This determines where the tab should be inserted within the tabContainer * * @param {MozTabbrowserTab} tab * @param {object} [options] * @param {number} [options.index] * @param {MozTabbrowserTabGroup} [options.tabGroup] * A related tab group where this tab should be added, when applicable.
*/
_insertTabAtIndex(
tab,
{ index, 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 index != "number") { // Move the new tab after another tab if needed, to the end otherwise.
index = 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 (
Services.prefs.getBoolPref( "browser.tabs.insertAfterCurrentExceptPinned"
) &&
previousTab.pinned
) {
index = Infinity;
} elseif (previousTab.visible) {
index = previousTab._tPos + 1;
} elseif (previousTab == FirefoxViewHandler.tab) {
index = 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);
}
}
}
// Prevent a flash of unstyled content by setting up the tab content // and inherited attributes before appending it (see Bug 1592054):
tab.initialize();
// 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, this.tabs.length);
}
/** @type {MozTabbrowserTab|undefined} */
let tabAfter = this.tabs.at(index); this.tabContainer._invalidateCachedTabs();
if (tabGroup) { if (tabAfter && tabAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, tabAfter);
} else { // Place tab at the end of the contextual tab group because one of: // 1) no `tabAfter` so `tab` should be the last tab in the tab strip // 2) `tabAfter` is in a different tab group this.moveTabToGroup(tab, tabGroup);
}
} else { // Place ungrouped tab before `tabAfter` or its group // 1) Ungrouped tab between standalone tabs // 2) Ungrouped tab at the end of the tab strip // 3) Ungrouped tab right before the next tab group, if the // next tab is in a group this.tabContainer.insertBefore(tab, tabAfter?.group ?? tabAfter);
}
this._updateTabsAfterInsert();
if (pinned) { this._updateTabBarForPinnedTabs();
}
TabBarVisibility.update();
}
/** * Dispatch a new tab event. This should be called when things are in a * consistent state, such that listeners of this event can again open * or close tabs.
*/
_fireTabOpen(tab, eventDetail) { delete tab.initializingTab;
let evt = new CustomEvent("TabOpen", {
bubbles: true,
detail: eventDetail || {},
});
tab.dispatchEvent(evt);
}
/** * @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 = [];
let keys = []; 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;
}
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
);
}
/** * In a multi-select context, the tabs (except pinned tabs) that are located to the * left of the leftmost selected tab will be removed.
*/
removeTabsToTheStartFrom(aTab) {
let tabs = this._getTabsToTheStartFrom(aTab); if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
) { return;
}
this.removeTabs(tabs);
}
/** * In a multi-select context, the tabs (except pinned tabs) that are located to the * right of the rightmost selected tab will be removed.
*/
removeTabsToTheEndFrom(aTab) {
let tabs = this._getTabsToTheEndFrom(aTab); if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
) { return;
}
this.removeTabs(tabs);
}
/** * Remove all tabs but `aTab`. By default, in a multi-select context, all * unpinned and unselected tabs are removed. Otherwise all unpinned tabs * except aTab are removed. This behavior can be changed using the the bool * flags below. * * @param {MozTabbrowserTab} aTab * The tab we will skip removing * @param {object} [aParams] * An optional set of parameters that will be passed to the * `removeTabs` function. * @param {boolean} [aParams.skipWarnAboutClosingTabs=false] * Skip showing the tab close warning prompt. * @param {boolean} [aParams.skipPinnedOrSelectedTabs=true] * Skip closing tabs that are selected or pinned.
*/
removeAllTabsBut(aTab, aParams = {}) {
let {
skipWarnAboutClosingTabs = false,
skipPinnedOrSelectedTabs = true,
} = aParams;
/** @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;
}
this.removeTabs(tabsToRemove, aParams);
}
removeMultiSelectedTabs() {
let selectedTabs = this.selectedTabs; if (
!this.warnAboutClosingTabs(
selectedTabs.length, this.closingTabsEnum.MULTI_SELECTED
)
) { return;
}
this.removeTabs(selectedTabs);
}
/** * @typedef {object} _startRemoveTabsReturnValue * @property {Promise<void>} beforeUnloadComplete * A promise that is resolved once all the beforeunload handlers have been * called. * @property {object[]} tabsWithBeforeUnloadPrompt * An array of tabs with unload prompts that need to be handled. * @property {object} [lastToClose] * The last tab to be closed, if appropriate.
*/
/** * Starts to remove tabs from the UI: checking for beforeunload handlers, * closing tabs where possible and triggering running of the unload handlers. * * @param {object[]} tabs * The set of tabs to remove. * @param {object} options * @param {boolean} options.animate * Whether or not to animate closing. * @param {boolean} options.suppressWarnAboutClosingWindow * This will supress the warning about closing a window with the last tab. * @param {boolean} options.skipPermitUnload * Skips the before unload checks for the tabs. Only set this to true when * using it in tandem with `runBeforeUnloadForTabs`. * @param {boolean} options.skipRemoves * Skips actually removing the tabs. The beforeunload handlers still run. * @param {boolean} options.skipSessionStore * If true, don't record the closed tabs in SessionStore. * @returns {_startRemoveTabsReturnValue}
*/
_startRemoveTabs(
tabs,
{
animate, // See bug 1883051 // eslint-disable-next-line no-unused-vars
suppressWarnAboutClosingWindow,
skipPermitUnload,
skipRemoves,
skipSessionStore,
}
) { // 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; /** * Map of tab group to surviving tabs in the group. * If any of the `tabs` to be removed belong to a tab group, keep track * of how many tabs in the tab group will be left after removing `tabs`. * For any tab group with 0 surviving tabs, we can know that that tab * group will be removed as a consequence of removing these `tabs`. * @type {Map<MozTabbrowserTabGroup, Set<MozTabbrowserTab>>}
*/
let tabGroupsSurvivingTabs = new Map();
for (let tab of tabs) { if (!skipRemoves) {
tab._closedInMultiselection = true;
} if (!skipRemoves && !skipSessionStore) { if (tab.group) { if (!tabGroupsSurvivingTabs.has(tab.group)) {
tabGroupsSurvivingTabs.set(tab.group, new Set(tab.group.tabs));
}
tabGroupsSurvivingTabs.get(tab.group).delete(tab);
}
} if (!skipRemoves && tab.selected) {
lastToClose = tab;
let toBlurTo = this._findTabToBlurTo(lastToClose, tabs); if (toBlurTo) { this._getSwitcher().warmupTab(toBlurTo);
}
} elseif (!skipPermitUnload && this._hasBeforeUnload(tab)) {
TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", tab); // 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;
TelemetryStopwatch.finish( "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
tab
); 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;
TelemetryStopwatch.finish( "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
tab
);
}
)
);
} else {
tabsWithoutBeforeUnload.push(tab);
}
}
if (!skipRemoves && !skipSessionStore) { for (let [
tabGroup,
survivingTabs,
] of tabGroupsSurvivingTabs.entries()) { // Before removing any tabs, save tab groups that won't survive // because all of their tabs are about to be removed. Then remove // the tab group directly to prevent the closing tabs from being // recorded by the session as individually closed tabs. if (!survivingTabs.size) {
tabGroup.save(); this.removeTabGroup(tabGroup);
}
}
}
// 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,
});
}
}
/** * Runs the before unload handler for the provided tabs, waiting for them * to complete. * * This can be used in tandem with removeTabs to allow any before unload * prompts to happen before any tab closures. This should only be used * in the case where any prompts need to happen before other items before * the actual tabs are closed. * * When using this function alongside removeTabs, specify the `skipUnload` * option to removeTabs. * * @param {object[]} tabs * An array of tabs to remove. * @returns {Promise<boolean>} * Returns true if the unload has been blocked by the user. False if tabs * may be subsequently closed.
*/
async runBeforeUnloadForTabs(tabs) { try {
let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt } = this._startRemoveTabs(tabs, {
animate: false,
suppressWarnAboutClosingWindow: false,
skipPermitUnload: false,
skipRemoves: true,
});
await beforeUnloadComplete;
// 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;
}
/** * Removes multiple tabs from the tab browser. * * @param {MozTabbrowserTab[]} tabs * The set of tabs to remove. * @param {object} [options] * @param {boolean} [options.animate] * Whether or not to animate closing, defaults to true. * @param {boolean} [options.suppressWarnAboutClosingWindow] * This will supress the warning about closing a window with the last tab. * @param {boolean} [options.skipPermitUnload] * Skips the before unload checks for the tabs. Only set this to true when * using it in tandem with `runBeforeUnloadForTabs`. * @param {boolean} [options.skipSessionStore] * If true, don't record the closed tabs in SessionStore.
*/
removeTabs(
tabs,
{
animate = true,
suppressWarnAboutClosingWindow = false,
skipPermitUnload = false,
skipSessionStore = false,
} = {}
) { // 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;
// 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;
}
let aParams = {
animate,
prewarmed: true,
skipPermitUnload,
skipSessionStore,
};
// 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 (
!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)
) { // Speculatevely start both stopwatches now. We'll cancel one of // the two later depending on whether we're animating.
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
}
// Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { this._endRemoveTab(aTab); return;
}
let isVisibleTab = aTab.visible;
let isLastTab = isVisibleTab && this.visibleTabs.length == 1; // 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;
// 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._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 && aTab._sharingState?.webRTC?.screen;
if (!screenShareInActiveTab) { this._blurTab(aTab);
}
var closeWindow = false; var newTab = false;
let anyRemainingTabsInCollapsedTabGroups =
!!this.tabsInCollapsedTabGroups.length; if (
aTab.visible && this.visibleTabs.length == 1 &&
!anyRemainingTabsInCollapsedTabGroups
) {
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);
}
// 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 },
});
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,
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();
}
var wasPinned = aTab.pinned;
// 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) { if (wasPinned) { this.tabContainer._positionPinnedTabs();
}
// 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.
TelemetryStopwatch.finish( "FX_TAB_CLOSE_TIME_ANIM_MS",
aTab, true/* aCanceledOkay */
);
TelemetryStopwatch.finish( "FX_TAB_CLOSE_TIME_NO_ANIM_MS",
aTab, true/* aCanceledOkay */
);
/** * Closes all tabs matching the list of nsURIs. * This does not close any tabs that have a beforeUnload prompt. * * @param {nsURI[]} urisToClose * The set of uris to remove. * @returns {number} The count of successfully closed tabs.
*/
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)) { // 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; const tabsToExclude = tabs.concat( this.tabContainer.allTabs.filter(tab => !tab.linkedPanel)
);
let newTab = this._findTabToBlurTo(this.selectedTab, tabsToExclude); if (newTab) { this.selectedTab = newTab;
} else { // all tabs are unloaded - show Firefox View
FirefoxViewHandler.openTab("opentabs");
allTabsUnloaded = true;
}
}
let memoryUsageBeforeUnload = await getTotalMemoryUsage();
let timeBeforeUnload = performance.now();
let numberOfTabsUnloaded = 0; for (let tab of tabs) {
numberOfTabsUnloaded += this.discardBrowser(tab, true) ? 1 : 0;
}
let timeElapsed = Math.floor(performance.now() - timeBeforeUnload);
Glean.browserEngagement.tabExplicitUnload.record({
unload_selected_tab: unloadSelectedTab,
all_tabs_unloaded: allTabsUnloaded,
tabs_unloaded: numberOfTabsUnloaded,
memory_before: memoryUsageBeforeUnload,
memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed,
});
}
/** * Handles opening a new tab with mouse middleclick. * @param node * @param event * The click event
*/
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.getAttribute("disabled") == "true") { 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();
}
}
/** * Finds the tab that we will blur to if we blur aTab. * @param {MozTabbrowserTab} aTab * The tab we would blur * @param {MozTabbrowserTab[]} [aExcludeTabs=[]] * Tabs to exclude from our search (i.e., because they are being * closed along with aTab)
*/
_findTabToBlurTo(aTab, aExcludeTabs = []) { if (!aTab.selected) { returnnull;
} if (FirefoxViewHandler.tab) {
aExcludeTabs.push(FirefoxViewHandler.tab);
}
let excludeTabs = new Set(aExcludeTabs);
// 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} * False if swapping isn't permitted, true otherwise.
*/
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.ownerGlobal)
) { returnfalse;
}
// Do not allow transfering a useRemoteSubframes tab to a // non-useRemoteSubframes window and vice versa. if (gFissionBrowser != aOtherTab.ownerGlobal.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.ownerGlobal.gBrowser; var isPending = aOtherTab.hasAttribute("pending");
let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
let stateFlags = 0; if (otherTabListener) {
stateFlags = otherTabListener.mStateFlags;
}
// 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.ownerGlobal;
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("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");
aOurTab._sharingState = aOtherTab._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);
}
if (otherBrowser.isDistinctProductPageVisit) {
ourBrowser.isDistinctProductPageVisit = true;
}
// 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,
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.ownerGlobal.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;
}
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._tabFilters.get(aOtherTab);
let tabListener = otherTabBrowser._tabListeners.get(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.ownerGlobal.TabProgressListener(
aOtherTab,
otherBrowser, false, false
);
otherTabBrowser._tabListeners.set(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.ownerGlobal.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.mProgressListeners.push(aListener);
}
removeProgressListener(aListener) { this.mProgressListeners = this.mProgressListeners.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._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();
}
}
/** * Moves a tab to a new browser window, unless it's already the only tab * in the current window, in which case this will do nothing.
*/
replaceTabWithWindow(aTab, aOptions) { if (this.tabs.length == 1) { returnnull;
}
var options = "chrome,dialog=no,all"; for (var name in aOptions) {
options += "," + name + "=" + aOptions[name];
}
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
options += ",private=1";
}
// Play the tab closing animation to give immediate feedback while // waiting for the new window to appear. if (!gReduceMotion) {
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
aTab.removeAttribute("fadein");
}
// tell a new window to take the "dropped" tab return window.openDialog(
AppConstants.BROWSER_CHROME_URL, "_blank",
options,
aTab
);
}
/** * Move contextTab (or selected tabs in a mutli-select context) * to a new browser window, unless it is (they are) already the only tab(s) * in the current window, in which case this will do nothing.
*/
replaceTabsWithWindow(contextTab, aOptions = {}) {
let tabs; if (contextTab.multiselected) {
tabs = this.selectedTabs;
} else {
tabs = [contextTab];
}
if (this.tabs.length == tabs.length) { returnnull;
}
if (tabs.length == 1) { returnthis.replaceTabWithWindow(tabs[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 tab of tabs) {
tab.style.maxWidth = ""; // ensure that fade-out transition happens
tab.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 (!tabs.includes(selectedTab)) {
selectedTab = tabs[0];
}
let win = this.replaceTabWithWindow(selectedTab, aOptions);
win.addEventListener( "before-initial-tab-adopted",
() => {
let index = 0; for (let tab of tabs) { if (tab !== selectedTab) { const newTab = win.gBrowser.adoptTab(tab, index); if (!newTab) { // The adoption failed. Restore "fadein" and don't increase the index.
tab.setAttribute("fadein", "true"); continue;
}
}
++index;
} // 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;
}
/** * Moves group to a new window. * * @param {MozTabbrowserTabGroup} group * The tab group to move.
*/
replaceGroupWithWindow(group) { // The first tab added to the new window will be selected. // If a tab in the group is selected, adopt it first.
let selectedIndex = group.tabs.indexOf(gBrowser.selectedTab); if (selectedIndex < 0) { // Otherwise, we'll first adopt the first tab in the group
selectedIndex = 0;
}
let firstTab = group.tabs[selectedIndex];
let newWindow = this.replaceTabWithWindow(firstTab);
newWindow.addEventListener( "before-initial-tab-adopted",
() => {
let tabsToGroup = group.tabs.map((tab, i) => { // addtabGroup will handle adopting the other tabs, but we already // started adopting the tab at selectedIndex so we need to swap // the old tab out for the new one. if (i == selectedIndex) { return newWindow.gBrowser.visibleTabs[0];
} return tab;
}); // The initial tab isn't fully adopted yet, but the tab object has been // instantiated, so we can make a group now.
newWindow.gBrowser.addTabGroup(tabsToGroup, {
color: group.color,
label: group.label,
id: group.id,
});
},
{ once: true }
); return newWindow;
}
_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;
}
/** * @param {MozTabbrowserTab} aTab * @param {number} aIndex * @param {object} [options] * @param {boolean} [options.forceStandaloneTab=false] * Force `aTab` to move into position as a standalone tab, overriding * any possibility of entering a tab group. For example, setting `true` * ensures that a pinned tab will not accidentally be placed inside of * a tab group, since pinned tabs are presently not allowed in tab groups. * @returns {void}
*/
moveTabTo(aTab, aIndex, options = { forceStandaloneTab: false }) { const { forceStandaloneTab } = options;
// 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();
}
this.tabContainer._handleTabSelect(true);
if (aTab.pinned) { this.tabContainer._positionPinnedTabs();
} // Pinning/unpinning vertical tabs, and moving tabs into tab groups, both bypass moveTabTo. // We still want to check whether its worth dispatching an event. if (oldPosition == aTab._tPos) { return;
} var evt = document.createEvent("UIEvents");
evt.initUIEvent("TabMove", true, false, window, oldPosition);
aTab.dispatchEvent(evt);
}
/** * Adopts a tab from another browser window, and inserts it at aIndex * * @returns {object} * The new tab in the current window, null if the tab couldn't be adopted.
*/
adoptTab(aTab, aIndex, aSelectTab) { // 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 params = {
eventDetail: { adoptedTab: aTab },
preferredRemoteType: linkedBrowser.remoteType,
initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
skipAnimation: true,
index: aIndex,
createLazyBrowser,
};
let numPinned = this.pinnedTabCount; if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
params.pinned = true;
}
if (aTab.hasAttribute("usercontextid")) { // new tab must have the same usercontextid as the old one
params.userContextId = aTab.getAttribute("usercontextid");
}
let newTab = this.addWebTab("about:blank", params);
let newBrowser = this.getBrowserForTab(newTab);
aTab.container._finishAnimateTabMove();
if (!createLazyBrowser) { // Stop the about:blank load.
newBrowser.stop();
}
/** * @param aTab * Can be from a different window as well * @param aRestoreTabImmediately * Can defer loading of the tab contents * @param aOptions * The new index of the tab
*/
duplicateTab(aTab, aRestoreTabImmediately, aOptions) { return SessionStore.duplicateTab(
window,
aTab,
0,
aRestoreTabImmediately,
aOptions
);
}
/** * Update accessible names of close buttons in the (multi) selected tabs * collection with how many tabs they will close
*/
_updateMultiselectedTabCloseButtonTooltip() { const tabCount = gBrowser.selectedTabs.length;
gBrowser.selectedTabs.forEach(selectedTab => {
document.l10n.setArgs(selectedTab.querySelector(".tab-close-button"), {
tabCount,
});
});
}
addToMultiSelectedTabs(aTab) { if (aTab.multiselected) { return;
}
/** * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
*/
addRangeToMultiSelectedTabs(aTab1, aTab2) { if (aTab1 == aTab2) { return;
}
removeFromMultiSelectedTabs(aTab) { if (!aTab.multiselected) { return;
}
aTab.removeAttribute("multiselected");
aTab.removeAttribute("aria-selected"); this._multiSelectedTabsSet.delete(aTab); this._startMultiSelectChange(); if (this._multiSelectChangeAdditions.has(aTab)) { this._multiSelectChangeAdditions.delete(aTab);
} else { this._multiSelectChangeRemovals.add(aTab);
} // Update labels for Close buttons of the remaining multiselected tabs: this._updateMultiselectedTabCloseButtonTooltip(); // Update the label for the Close button of the tab being removed // from the multiselection:
document.l10n.setArgs(aTab.querySelector(".tab-close-button"), {
tabCount: 1,
});
}
clearMultiSelectedTabs() { if (this._clearMultiSelectionLocked) { if (this._clearMultiSelectionLockedOnce) { this._clearMultiSelectionLockedOnce = false; this._clearMultiSelectionLocked = false;
} return;
}
if (this.multiSelectedTabsCount < 1) { return;
}
for (let tab of this.selectedTabs) { this.removeFromMultiSelectedTabs(tab);
} this._lastMultiSelectedTabRef = null;
}
/** * Remove a tab from the multiselection if it's the only one left there. * * In fact, some scenario may lead to only one single tab multi-selected, * this is something to avoid (Chrome does the same) * Consider 4 tabs A,B,C,D with A having the focus * 1. select C with Ctrl * 2. Right-click on B and "Close Tabs to The Right" * * Expected result * C and D closing * A being the only multi-selected tab, selection should be cleared * * * Single selected tab could even happen with a none-focused tab. * For exemple with the menu "Close other tabs", it could happen * with a multi-selected pinned tab. * For illustration, consider 4 tabs A,B,C,D with B active * 1. pin A and Ctrl-select it * 2. Ctrl-select C * 3. right-click on D and click "Close Other Tabs" * * Expected result * B and C closing * A[pinned] being the only multi-selected tab, selection should be cleared.
*/
_avoidSingleSelectedTab() { if (this.multiSelectedTabsCount == 1) { this.clearMultiSelectedTabs();
}
}
_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;
} 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);
}
}
warmupTab(aTab) { if (gMultiProcessBrowser) { this._getSwitcher().warmupTab(aTab);
}
}
/** * _maybeRequestReplyFromRemoteContent may call * aEvent.requestReplyFromRemoteContent if necessary. * * @param aEvent The handling event. * @return true if the handler should wait a reply event. * false if the handle can handle the immediately.
*/
_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) { returntrue; // Somebody called requestReplyFromRemoteContent already.
} if (
!aEvent.isReplyEventFromRemoteContent &&
aEvent.target?.isRemoteBrowser === true
) {
aEvent.requestReplyFromRemoteContent(); returntrue;
} returnfalse;
}
_handleKeyDownEvent(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;
}
// Don't check if the event was already consumed because tab // navigation should always work for better user experience.
switch (ShortcutUtils.getSystemActionForEvent(aEvent)) { 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();
}
}
_handleKeyPressEvent(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;
}
}
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());
}
getTabTooltip(tab, includeLabel = true) {
let labelArray = []; if (includeLabel) {
labelArray.push(tab._fullLabel || tab.getAttribute("label"));
} if (this.showPidAndActiveness) { const pids = this.getTabPids(tab); if (pids.length) {
let pidLabel = pids.length > 1 ? "pids" : "pid";
labelArray.push(`(${pidLabel} ${pids.join(", ")})`);
}
if (tab.linkedBrowser.docShellIsActive) {
labelArray.push("[A]");
}
}
labelArray = [label]; 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) { switch (aEvent.type) { case"keydown": this._handleKeyDownEvent(aEvent); break; case"keypress": this._handleKeyPressEvent(aEvent); break; case"framefocusrequested": {
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. break;
} this.selectedTab = tab;
window.focus();
aEvent.preventDefault(); break;
} case"visibilitychange": { const inactive = document.hidden; if (!this._switcher) { this.selectedBrowser.preserveLayers(inactive); this.selectedBrowser.docShellIsActive = !inactive;
} break;
} case"TabGroupCreate": if (aEvent.detail.showCreateUI) { this.tabGroupMenu.openCreateModal(aEvent.target);
} break; case"activate": // Intentional fallthrough case"deactivate": this.selectedTab.updateLastSeenActive(); break;
}
}
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;
}
}
}
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.mStateFlags;
let requestCount = oldListener.mRequestCount;
// We'll be creating a new listener, so destroy the old one.
oldListener.destroy();
let oldDroppedLinkHandler = browser.droppedLinkHandler;
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.ownerGlobal != window) { thrownew Error("Cannot set the successor of another window's tab");
} if (successorTab == aTab) {
successorTab = null;
} if (successorTab && successorTab.ownerGlobal != 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);
}
}
/** * For all tabs with aTab as a successor, set the successor to aOtherTab * instead.
*/
replaceInSuccession(aTab, aOtherTab) { if (aTab.predecessors) { for (const predecessor of Array.from(aTab.predecessors)) { this.setSuccessor(predecessor, aOtherTab);
}
}
}
/** * Get the triggering principal for the last navigation in the session history.
*/
_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();
}
};
/** * A web progress listener object definition for a given tab.
*/ class TabProgressListener {
constructor(
aTab,
aBrowser,
aStartsBlank,
aWasPreloadedBrowser,
aOrigStateFlags,
aOrigRequestCount
) {
let stateFlags = aOrigStateFlags || 0; // Initialize mStateFlags 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, mStateFlags 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.mBlank) { 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.mRequestCount == 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.mBlank)
) { this.mBlank = false;
}
if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) { this.mRequestCount++;
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.mBrowser.initialPageLoadedFromUserAction !=
originalLocation.spec && this.mBrowser.currentURI && this.mBrowser.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.mBrowser.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.mBrowser.browsingContext.sessionHistory?.count === 0 &&
BrowserUIUtils.checkEmptyPageOrigin( this.mBrowser,
originalLocation
)
) {
gBrowser.setInitialTabTitle(this.mTab, originalLocation.spec, {
isURL: true,
});
this.mBrowser.browsingContext.nonWebControlledBlankURI =
originalLocation; if (this.mTab.selected && !gBrowser.userTypedValue) {
gURLBar.setURI();
}
}
} deletethis.mBrowser.initialPageLoadedFromUserAction; // If the browser is loading it must not be crashed anymore this.mTab.removeAttribute("crashed");
}
if (this.mTab.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.mRequestCount = 0;
let modifiedAttrs = []; if (this.mTab.hasAttribute("busy")) { this.mTab.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.mTab._notselectedsinceload) { this.mTab.setAttribute("notselectedsinceload", "true");
} else { this.mTab.removeAttribute("notselectedsinceload");
}
this.mTab.setAttribute("bursting", "true");
}
}
if (this.mTab.hasAttribute("progress")) { this.mTab.removeAttribute("progress");
modifiedAttrs.push("progress");
}
if (modifiedAttrs.length) {
gBrowser._tabAttrModified(this.mTab, modifiedAttrs);
}
if (aWebProgress.isTopLevel) {
let isSuccessful = Components.isSuccessCode(aStatus); if (!isSuccessful && !this.mTab.isEmpty) { // Restore the current document's location in case the // request was stopped (possibly from a content script) // before the location changed.
this.mBrowser.userTypedValue = null; // When browser.tabs.documentchannel.parent-controlled pref and SHIP // are 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 the // parent-controlled pref and SHIP are enabled by default. // Bug 1725716 has been filed to consider removing isNavigating // field alltogether.
let isNavigating = this.mBrowser.isNavigating; if ( this.mTab.selected &&
aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD &&
!isNavigating
) {
gURLBar.setURI();
}
} elseif (isSuccessful) { this.mBrowser.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. if (
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS)
) { this.mTab.removeAttribute("image");
} else { // 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.mBrowser._documentURI is in // FAVICON_DEFAULTS.
gBrowser.setDefaultIcon(this.mTab, this.mBrowser._documentURI);
}
// For keyword URIs clear the user typed value since they will be changed into real URIs if (location.scheme == "keyword") { this.mBrowser.userTypedValue = null;
}
if (this.mTab.selected) {
gBrowser._isBusy = false;
}
}
if (aStateFlags & (STATE_START | STATE_STOP)) { // reset cached temporary values at beginning and end this.mMessage = ""; this.mTotalProgress = 0;
} this.mStateFlags = aStateFlags; this.mStatus = 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.mBrowser.didStartLoadSinceLastUserTyping() ||
(isErrorPage && aLocation.spec != "about:blank") ||
(isSameDocument && this.mBrowser.isNavigating) ||
(isSameDocument && !this.mBrowser.userTypedValue)
) { this.mBrowser.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.mTab.hasAttribute("busy")) { this.mTab.removeAttribute("busy");
gBrowser._tabAttrModified(this.mTab, ["busy"]);
}
if (!isSameDocument) { // If the browser was playing audio, we should remove the playing state. if (this.mTab.hasAttribute("soundplaying")) {
clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); this.mTab._soundPlayingAttrRemovalTimer = 0; this.mTab.removeAttribute("soundplaying");
gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
}
// If the browser was previously muted, we should restore the muted state. if (this.mTab.hasAttribute("muted")) { this.mTab.linkedBrowser.mute();
}
if (gBrowser.isFindBarInitialized(this.mTab)) {
let findBar = gBrowser.getCachedFindBar(this.mTab);
// 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.mTab.hasAttribute("pending") &&
!this.mTab.hasAttribute("customizemode") &&
aWebProgress.isLoadingDocument
) { // Removing the tab's image here causes flickering, wait until the // load is complete. this.mBrowser.mIconURL = null;
}
if (!isReload && aWebProgress.isLoadingDocument) {
let triggerer = gBrowser._getTriggeringPrincipalFromHistory( this.mBrowser
); // 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.mBrowser.getAttribute("usercontextid") || 0; if (this.mBrowser.registeredOpenURI) {
let uri = this.mBrowser.registeredOpenURI;
gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
uri.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
); deletethis.mBrowser.registeredOpenURI;
} if (!isBlankPageURL(aLocation.spec)) {
gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
aLocation.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
); this.mBrowser.registeredOpenURI = aLocation;
}
if (this.mTab != gBrowser.selectedTab) {
let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab); if (tabCacheIndex != -1) {
gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
}
}
}
if (!this.mBlank || this.mBrowser.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;
},
/** * Handles URIs when we want to deal with them in chrome code rather than pass * them down to a content browser. This can avoid unnecessary process switching * for the browser. * @param aBrowser the browser that is attempting to load the URI * @param aUri the nsIURI that is being loaded * @returns true if the URI is handled, otherwise false
*/
_handleUriInChrome(aBrowser, aUri) { if (aUri.scheme == "file") { try {
let mimeType = Cc["@mozilla.org/mime;1"]
.getService(Ci.nsIMIMEService)
.getTypeFromURI(aUri); if (mimeType == "application/x-xpinstall") {
let systemPrincipal =
Services.scriptSecurityManager.getSystemPrincipal();
AddonManager.getInstallForURL(aUri.spec, {
telemetryInfo: { source: "file-url" },
}).then(install => {
AddonManager.installAddonFromWebpage(
mimeType,
aBrowser,
systemPrincipal,
install
);
}); returntrue;
}
} catch (e) { returnfalse;
}
}
returnfalse;
},
_updateTriggerMetadataForLoad(
browser,
uriString,
{ loadFlags, globalHistoryOptions }
) { if (globalHistoryOptions?.triggeringSponsoredURL) { 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);
} catch (e) {}
}
// 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 (uri && this._handleUriInChrome(browser, uri)) { // If we've handled the URI in chrome, then just return here. return;
}
// 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 toolbar = document.getElementById("TabsToolbar");
let navbar = document.getElementById("nav-bar");
let hideTabstrip = false;
let isPopup = !window.toolbar.visible;
let isVerticalTabs = Services.prefs.getBoolPref( "sidebar.verticalTabs", false
);
let nonPopupWithVerticalTabs = !isPopup && isVerticalTabs; if (
!gBrowser /* gBrowser isn't initialized yet */ ||
gBrowser.visibleTabs.length == 1
) {
hideTabstrip = isPopup;
}
if (nonPopupWithVerticalTabs) { // CustomTitlebar decides if we can draw within the titlebar area. // In vertical tabs mode, the toolbar with the horizontal tabstrip gets hidden // and the navbar becomes a titlebar.
hideTabstrip = true;
CustomTitlebar.allowedBy("tabs-visible", true);
} else {
CustomTitlebar.allowedBy("tabs-visible", !hideTabstrip);
}
gNavToolbox.toggleAttribute("tabs-hidden", hideTabstrip); // Should the nav-bar look and function like a titlebar?
navbar.classList.toggle( "browser-titlebar",
CustomTitlebar.enabled && hideTabstrip
);
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")
.setAttribute("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");
if (gBrowser._tabGroupsEnabled) {
let groupableTabs = contextTabs.filter(t => !t.pinned);
let selectedGroupCount = new Set( // the filter is necessary to remove the "null" group
groupableTabs.map(t => t.group).filter(g => g)
).size;
// Determine whether or not the "current" tab group should appear in the move context menu
let availableGroupsToMoveTo = gBrowser
.getAllTabGroups()
.sort((a, b) => a.createdDate - b.createdDate);
let groupToFilter; if (selectedGroupCount == 1) {
groupToFilter = groupableTabs[0].group;
} if (groupToFilter) {
availableGroupsToMoveTo = availableGroupsToMoveTo.filter(
group => group !== groupToFilter
);
}
// Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
document.getElementById("context_reloadTab").hidden = multiselectionContext;
document.getElementById("context_reloadSelectedTabs").hidden =
!multiselectionContext;
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 = 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 && !multiselectionContext
);
document.getElementById("context_playSelectedTabs").hidden = !( this.contextTab.activeMediaBlocked && multiselectionContext
);
// Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
let contextPinTab = document.getElementById("context_pinTab");
contextPinTab.hidden = this.contextTab.pinned || multiselectionContext;
let contextUnpinTab = document.getElementById("context_unpinTab");
contextUnpinTab.hidden = !this.contextTab.pinned || multiselectionContext;
let contextPinSelectedTabs = document.getElementById( "context_pinSelectedTabs"
);
contextPinSelectedTabs.hidden = this.contextTab.pinned || !multiselectionContext;
let contextUnpinSelectedTabs = document.getElementById( "context_unpinSelectedTabs"
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !multiselectionContext;
// Move Tab items
let contextMoveTabOptions = document.getElementById( "context_moveTabOptions"
);
contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
contextMoveTabOptions.disabled = this.contextTab.hidden || gBrowser.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 visibleTabs = gBrowser.visibleTabs;
let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
let tabsToMove = contextTabs;
let lastTabToMove = tabsToMove[tabsToMove.length - 1];
let isLastPinnedTab = false; if (lastTabToMove.pinned) {
let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
isLastPinnedTab = !sibling || !sibling.pinned;
}
contextMoveTabToEnd.disabled =
(lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
allSelectedTabsAdjacent;
let contextMoveTabToStart = document.getElementById("context_moveToStart");
let isFirstTab =
tabsToMove[0] == visibleTabs[0] ||
tabsToMove[0] == visibleTabs[gBrowser.pinnedTabCount];
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById("context_duplicateTab").hidden =
multiselectionContext;
document.getElementById("context_duplicateTabs").hidden =
!multiselectionContext;
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 = multiselectionContext
? 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);
let closeDuplicateEnabled = Services.prefs.getBoolPref( "browser.tabs.context.close-duplicate.enabled"
);
let closeDuplicateTabsItem = document.getElementById( "context_closeDuplicateTabs"
);
closeDuplicateTabsItem.hidden = !closeDuplicateEnabled;
closeDuplicateTabsItem.disabled =
!closeDuplicateEnabled ||
!gBrowser.getDuplicateTabsToClose(this.contextTab).length;
// 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 = multiselectionContext;
// Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
let bookmarkMultiSelectedTabs = document.getElementById( "context_bookmarkSelectedTabs"
);
bookmarkMultiSelectedTabs.hidden = !multiselectionContext;
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 = multiselectionContext;
toggleMultiSelectMute.hidden = !multiselectionContext;
createReopenInContainerMenu(event) {
createUserContextMenu(event, {
isContextMenu: true,
excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
});
},
duplicateSelectedTabs() {
let tabsToDuplicate = gBrowser.selectedTabs;
let newIndex = tabsToDuplicate[tabsToDuplicate.length - 1]._tPos + 1; for (let tab of tabsToDuplicate) {
let newTab = SessionStore.duplicateTab(window, tab);
gBrowser.moveTabTo(newTab, newIndex++);
}
},
reopenInContainer(event) {
let userContextId = parseInt(
event.target.getAttribute("data-usercontextid")
);
let reopenedTabs = this.contextTab.multiselected
? gBrowser.selectedTabs
: [this.contextTab];
for (let tab of reopenedTabs) { if (tab.getAttribute("usercontextid") == userContextId) { continue;
}
/* Create a triggering principal that is able to load the new tab For content principals that are about: chrome: or resource: we need system to load them. Anything other than system principal needs to have the new userContextId.
*/
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();
},
ungroupTabs() { if (this.contextTab.multiselected) { for (let i = gBrowser.selectedTabs.length - 1; i >= 0; i--) {
gBrowser.ungroupTab(gBrowser.selectedTabs[i]);
}
} else {
gBrowser.ungroupTab(this.contextTab);
}
},
};
Messung V0.5 in Prozent
¤ 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.285Bemerkung:
(vorverarbeitet am 2026-04-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.