summaryrefslogtreecommitdiffstats
path: root/browser/base/content/tabbrowser.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/tabbrowser.js')
-rw-r--r--browser/base/content/tabbrowser.js7798
1 files changed, 7798 insertions, 0 deletions
diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js
new file mode 100644
index 0000000000..a9637ad262
--- /dev/null
+++ b/browser/base/content/tabbrowser.js
@@ -0,0 +1,7798 @@
+/* -*- 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/. */
+
+{
+ // start private scope for gBrowser
+ /**
+ * 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",
+ };
+
+ const {
+ LOAD_FLAGS_NONE,
+ LOAD_FLAGS_FROM_EXTERNAL,
+ LOAD_FLAGS_FIRST_LOAD,
+ LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL,
+ LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
+ LOAD_FLAGS_FIXUP_SCHEME_TYPOS,
+ LOAD_FLAGS_FORCE_ALLOW_DATA_URI,
+ LOAD_FLAGS_DISABLE_TRR,
+ } = Ci.nsIWebNavigation;
+
+ /**
+ * 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;
+ }
+
+ replaceContainerClass("color", hbox, identity.color);
+
+ let label = ContextualIdentityService.getUserContextLabel(userContextId);
+ document.getElementById("userContext-label").setAttribute("value", 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;
+ }
+
+ window._gBrowser = {
+ init() {
+ ChromeUtils.defineESModuleGetters(this, {
+ AsyncTabSwitcher: "resource:///modules/AsyncTabSwitcher.sys.mjs",
+ PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
+ UrlbarProviderOpenTabs:
+ "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
+ });
+ XPCOMUtils.defineLazyServiceGetters(this, {
+ MacSharingService: [
+ "@mozilla.org/widget/macsharingservice;1",
+ "nsIMacSharingService",
+ ],
+ });
+ ChromeUtils.defineLazyGetter(this, "tabLocalization", () => {
+ return new Localization(
+ ["browser/tabbrowser.ftl", "branding/brand.ftl"],
+ true
+ );
+ });
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_shouldExposeContentTitle",
+ "privacy.exposeContentTitleInWindow",
+ true
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_shouldExposeContentTitlePbm",
+ "privacy.exposeContentTitleInWindow.pbm",
+ true
+ );
+
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ ChromeUtils.defineESModuleGetters(this, {
+ TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
+ });
+ }
+
+ Services.obs.addObserver(this, "contextual-identity-updated");
+
+ Services.els.addSystemEventListener(document, "keydown", this, false);
+ Services.els.addSystemEventListener(document, "keypress", this, false);
+ document.addEventListener("visibilitychange", this);
+ window.addEventListener("framefocusrequested", this);
+ window.addEventListener("activate", this);
+ window.addEventListener("deactivate", this);
+
+ this.tabContainer.init();
+ this._setupInitialBrowserAndTab();
+
+ if (
+ Services.prefs.getIntPref("browser.display.document_color_use") == 2
+ ) {
+ this.tabpanels.style.backgroundColor = Services.prefs.getBoolPref(
+ "browser.display.use_system_colors"
+ )
+ ? "canvas"
+ : Services.prefs.getCharPref("browser.display.background_color");
+ }
+
+ this._setFindbarData();
+
+ // 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");
+
+ this._setupEventListeners();
+ this._initialized = true;
+ },
+
+ ownerGlobal: window,
+
+ ownerDocument: document,
+
+ closingTabsEnum: {
+ ALL: 0,
+ OTHER: 1,
+ TO_START: 2,
+ TO_END: 3,
+ MULTI_SELECTED: 4,
+ },
+
+ _lastRelatedTabMap: new WeakMap(),
+
+ mProgressListeners: [],
+
+ mTabsProgressListeners: [],
+
+ _tabListeners: new Map(),
+
+ _tabFilters: new Map(),
+
+ _isBusy: false,
+
+ _awaitingToggleCaretBrowsingPrompt: false,
+
+ arrowKeysShouldWrap: AppConstants == "macosx",
+
+ _previewMode: false,
+
+ _lastFindValue: "",
+
+ _contentWaitingCount: 0,
+
+ _tabLayerCache: [],
+
+ tabAnimationsInProgress: 0,
+
+ /**
+ * 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;
+ }
+ return false;
+ },
+ 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 tabContainer() {
+ delete this.tabContainer;
+ return (this.tabContainer = document.getElementById("tabbrowser-tabs"));
+ },
+
+ get tabs() {
+ return this.tabContainer.allTabs;
+ },
+
+ get tabbox() {
+ delete this.tabbox;
+ return (this.tabbox = document.getElementById("tabbrowser-tabbox"));
+ },
+
+ get tabpanels() {
+ delete this.tabpanels;
+ return (this.tabpanels = document.getElementById("tabbrowser-tabpanels"));
+ },
+
+ addEventListener(...args) {
+ this.tabpanels.addEventListener(...args);
+ },
+
+ removeEventListener(...args) {
+ this.tabpanels.removeEventListener(...args);
+ },
+
+ dispatchEvent(...args) {
+ return this.tabpanels.dispatchEvent(...args);
+ },
+
+ get visibleTabs() {
+ return this.tabContainer._getVisibleTabs();
+ },
+
+ get _numPinnedTabs() {
+ 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() {
+ return this._selectedTab;
+ },
+
+ get selectedBrowser() {
+ return this._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 (!openWindowInfo && window.arguments && window.arguments[11]) {
+ openWindowInfo = window.arguments[11];
+ }
+
+ let tabArgument = gBrowserInit.getTabToAdopt();
+
+ // If we have a tab argument with browser, we use its remoteType. Otherwise,
+ // if e10s is disabled or there's a parent process opener (e.g. parent
+ // process about: page) for the content tab, we use a parent
+ // process remoteType. Otherwise, we check the URI to determine
+ // what to do - if there isn't one, we default to the default remote type.
+ //
+ // When adopting a tab, we'll also use that tab's browsingContextGroupId,
+ // if available, to ensure we don't spawn a new process.
+ let remoteType;
+ let initialBrowsingContextGroupId;
+
+ if (tabArgument && tabArgument.hasAttribute("usercontextid")) {
+ // The window's first argument is a tab if and only if we are swapping tabs.
+ // We must set the browser's usercontextid so that the newly created remote
+ // tab child has the correct usercontextid.
+ userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
+ }
+
+ if (tabArgument && tabArgument.linkedBrowser) {
+ remoteType = tabArgument.linkedBrowser.remoteType;
+ initialBrowsingContextGroupId =
+ tabArgument.linkedBrowser.browsingContext?.group.id;
+ } else if (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;
+ }
+ }
+
+ let createOptions = {
+ uriIsAboutBlank: false,
+ userContextId,
+ initialBrowsingContextGroupId,
+ remoteType,
+ openWindowInfo,
+ };
+ let browser = this.createBrowser(createOptions);
+ browser.setAttribute("primary", "true");
+ if (gBrowserAllowScriptsToCloseInitialTabs) {
+ browser.setAttribute("allowscriptstoclose", "true");
+ }
+ browser.droppedLinkHandler = handleDroppedLink;
+ browser.loadURI = URILoadingWrapper.loadURI.bind(
+ URILoadingWrapper,
+ browser
+ );
+ browser.fixupAndLoadURIString =
+ URILoadingWrapper.fixupAndLoadURIString.bind(
+ URILoadingWrapper,
+ browser
+ );
+
+ let uniqueId = this._generateUniquePanelID();
+ let panel = this.getPanel(browser);
+ panel.id = uniqueId;
+ this.tabpanels.appendChild(panel);
+
+ let tab = this.tabs[0];
+ tab.linkedPanel = uniqueId;
+ this._selectedTab = tab;
+ this._selectedBrowser = browser;
+ tab.permanentKey = browser.permanentKey;
+ tab._tPos = 0;
+ tab._fullyOpen = true;
+ tab.linkedBrowser = browser;
+
+ 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() {
+ return this.selectedBrowser.canGoBack;
+ },
+
+ get canGoForward() {
+ return this.selectedBrowser.canGoForward;
+ },
+
+ goBack(requireUserInteraction) {
+ return this.selectedBrowser.goBack(requireUserInteraction);
+ },
+
+ goForward(requireUserInteraction) {
+ return this.selectedBrowser.goForward(requireUserInteraction);
+ },
+
+ reload() {
+ return this.selectedBrowser.reload();
+ },
+
+ reloadWithFlags(aFlags) {
+ return this.selectedBrowser.reloadWithFlags(aFlags);
+ },
+
+ stop() {
+ return this.selectedBrowser.stop();
+ },
+
+ /**
+ * throws exception for unknown schemes
+ */
+ loadURI(uri, params) {
+ return this.selectedBrowser.loadURI(uri, params);
+ },
+ /**
+ * throws exception for unknown schemes
+ */
+ fixupAndLoadURIString(uriString, params) {
+ return this.selectedBrowser.fixupAndLoadURIString(uriString, params);
+ },
+
+ gotoIndex(aIndex) {
+ return this.selectedBrowser.gotoIndex(aIndex);
+ },
+
+ get currentURI() {
+ return this.selectedBrowser.currentURI;
+ },
+
+ get finder() {
+ return this.selectedBrowser.finder;
+ },
+
+ get docShell() {
+ return this.selectedBrowser.docShell;
+ },
+
+ get webNavigation() {
+ return this.selectedBrowser.webNavigation;
+ },
+
+ get webProgress() {
+ return this.selectedBrowser.webProgress;
+ },
+
+ get contentWindow() {
+ return this.selectedBrowser.contentWindow;
+ },
+
+ get sessionHistory() {
+ return this.selectedBrowser.sessionHistory;
+ },
+
+ get contentDocument() {
+ return this.selectedBrowser.contentDocument;
+ },
+
+ get contentTitle() {
+ return this.selectedBrowser.contentTitle;
+ },
+
+ get contentPrincipal() {
+ return this.selectedBrowser.contentPrincipal;
+ },
+
+ get securityUI() {
+ return this.selectedBrowser.securityUI;
+ },
+
+ set fullZoom(val) {
+ this.selectedBrowser.fullZoom = val;
+ },
+
+ get fullZoom() {
+ return this.selectedBrowser.fullZoom;
+ },
+
+ set textZoom(val) {
+ this.selectedBrowser.textZoom = val;
+ },
+
+ get textZoom() {
+ return this.selectedBrowser.textZoom;
+ },
+
+ get isSyntheticDocument() {
+ return this.selectedBrowser.isSyntheticDocument;
+ },
+
+ set userTypedValue(val) {
+ this.selectedBrowser.userTypedValue = val;
+ },
+
+ get userTypedValue() {
+ return this.selectedBrowser.userTypedValue;
+ },
+
+ _setFindbarData() {
+ // Ensure we know what the find bar key is in the content process:
+ let { sharedData } = Services.ppmm;
+ if (!sharedData.has("Findbar:Shortcut")) {
+ let keyEl = document.getElementById("key_find");
+ let mods = keyEl
+ .getAttribute("modifiers")
+ .replace(
+ /accel/i,
+ AppConstants.platform == "macosx" ? "meta" : "control"
+ );
+ sharedData.set("Findbar:Shortcut", {
+ key: keyEl.getAttribute("key"),
+ shiftKey: mods.includes("shift"),
+ ctrlKey: mods.includes("control"),
+ altKey: mods.includes("alt"),
+ metaKey: mods.includes("meta"),
+ });
+ }
+ },
+
+ isFindBarInitialized(aTab) {
+ return (aTab || this.selectedTab)._findBar != undefined;
+ },
+
+ /**
+ * 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);
+
+ browser.parentNode.insertAdjacentElement("afterend", findBar);
+
+ await new Promise(r => requestAnimationFrame(r));
+ delete aTab._pendingFindBar;
+ if (window.closed || aTab.closing) {
+ return null;
+ }
+
+ findBar.browser = browser;
+ findBar._findField.value = this._lastFindValue;
+
+ aTab._findBar = findBar;
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabFindInitialized", true, false);
+ aTab.dispatchEvent(event);
+
+ return findBar;
+ },
+
+ _appendStatusPanel() {
+ this.selectedBrowser.insertAdjacentElement("afterend", StatusPanel.panel);
+ },
+
+ _updateTabBarForPinnedTabs() {
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer._updateCloseButtons();
+ },
+
+ _notifyPinnedStatus(aTab) {
+ // browsingContext is expected to not be defined on discarded tabs.
+ if (aTab.linkedBrowser.browsingContext) {
+ aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned;
+ }
+
+ let event = document.createEvent("Events");
+ event.initEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", true, false);
+ aTab.dispatchEvent(event);
+ },
+
+ pinTab(aTab) {
+ if (aTab.pinned) {
+ return;
+ }
+
+ this.showTab(aTab);
+ this.moveTabTo(aTab, this._numPinnedTabs);
+ aTab.setAttribute("pinned", "true");
+ this._updateTabBarForPinnedTabs();
+ this._notifyPinnedStatus(aTab);
+ },
+
+ unpinTab(aTab) {
+ if (!aTab.pinned) {
+ return;
+ }
+
+ this.moveTabTo(aTab, this._numPinnedTabs - 1);
+ aTab.removeAttribute("pinned");
+ aTab.style.marginInlineStart = "";
+ aTab._pinnedUnscrollable = false;
+ this._updateTabBarForPinnedTabs();
+ this._notifyPinnedStatus(aTab);
+ },
+
+ previewTab(aTab, aCallback) {
+ let currentTab = this.selectedTab;
+ try {
+ // Suppress focus, ownership and selected tab changes
+ this._previewMode = true;
+ this.selectedTab = aTab;
+ aCallback();
+ } finally {
+ this.selectedTab = currentTab;
+ this._previewMode = false;
+ }
+ },
+
+ syncThrobberAnimations(aTab) {
+ aTab.ownerGlobal.promiseDocumentFlushed(() => {
+ if (!aTab.container) {
+ return;
+ }
+
+ const animations = Array.from(
+ aTab.container.getElementsByTagName("tab")
+ )
+ .filter(tab => tab.hasAttribute("busy"))
+ .flatMap(tab => tab.throbber?.getAnimations({ subtree: true }) ?? [])
+ .filter(
+ anim =>
+ CSSAnimation.isInstance(anim) &&
+ (anim.animationName === "tab-throbber-animation" ||
+ anim.animationName === "tab-throbber-animation-rtl") &&
+ anim.playState === "running"
+ );
+
+ // Synchronize with the oldest running animation, if any.
+ const firstStartTime = Math.min(
+ ...animations.map(anim =>
+ anim.startTime === null ? Infinity : anim.startTime
+ )
+ );
+ if (firstStartTime === Infinity) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ for (let animation of animations) {
+ // If |animation| has been cancelled since this rAF callback
+ // was scheduled we don't want to set its startTime since
+ // that would restart it. We check for a cancelled animation
+ // by looking for a null currentTime rather than checking
+ // the playState, since reading the playState of
+ // a CSSAnimation object will flush style.
+ if (animation.currentTime !== null) {
+ animation.startTime = firstStartTime;
+ }
+ }
+ });
+ });
+ },
+
+ getBrowserAtIndex(aIndex) {
+ return this.browsers[aIndex];
+ },
+
+ getBrowserForOuterWindowID(aID) {
+ for (let b of this.browsers) {
+ if (b.outerWindowID == aID) {
+ return b;
+ }
+ }
+
+ return null;
+ },
+
+ getTabForBrowser(aBrowser) {
+ return this._tabForBrowser.get(aBrowser);
+ },
+
+ getPanel(aBrowser) {
+ return this.getBrowserContainer(aBrowser).parentNode;
+ },
+
+ getBrowserContainer(aBrowser) {
+ return (aBrowser || this.selectedBrowser).parentNode.parentNode;
+ },
+
+ getTabNotificationDeck() {
+ if (!this._tabNotificationDeck) {
+ let template = document.getElementById(
+ "tab-notification-deck-template"
+ );
+ template.replaceWith(template.content);
+ this._tabNotificationDeck = document.getElementById(
+ "tab-notification-deck"
+ );
+ }
+ return this._tabNotificationDeck;
+ },
+
+ _nextNotificationBoxId: 0,
+ getNotificationBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ if (!browser._notificationBox) {
+ browser._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "top");
+ element.setAttribute(
+ "name",
+ `tab-notification-box-${this._nextNotificationBoxId++}`
+ );
+ this.getTabNotificationDeck().append(element);
+ if (browser == this.selectedBrowser) {
+ this._updateVisibleNotificationBox(browser);
+ }
+ });
+ }
+ return browser._notificationBox;
+ },
+
+ readNotificationBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ return browser._notificationBox || null;
+ },
+
+ _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")
+ : "";
+ },
+
+ getTabModalPromptBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ if (!browser.tabModalPromptBox) {
+ browser.tabModalPromptBox = new TabModalPromptBox(browser);
+ }
+ return browser.tabModalPromptBox;
+ },
+
+ getTabDialogBox(aBrowser) {
+ if (!aBrowser) {
+ throw new Error("aBrowser is required");
+ }
+ if (!aBrowser.tabDialogBox) {
+ aBrowser.tabDialogBox = new TabDialogBox(aBrowser);
+ }
+ return aBrowser.tabDialogBox;
+ },
+
+ getTabFromAudioEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ return null;
+ }
+
+ var browser = aEvent.originalTarget;
+ var tab = this.getTabForBrowser(browser);
+ return tab;
+ },
+
+ _callProgressListeners(
+ aBrowser,
+ aMethod,
+ aArguments,
+ aCallGlobalListeners = true,
+ aCallTabsListeners = true
+ ) {
+ var rv = true;
+
+ 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);
+
+ callListeners(this.mTabsProgressListeners, aArguments);
+ }
+
+ return rv;
+ },
+
+ /**
+ * 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]);
+ }
+ },
+
+ setIcon(
+ aTab,
+ aIconURL = "",
+ aOriginalURL = aIconURL,
+ aLoadingPrincipal = null,
+ aClearImageFirst = false
+ ) {
+ let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url);
+
+ aIconURL = makeString(aIconURL);
+ aOriginalURL = makeString(aOriginalURL);
+
+ 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,
+ ]);
+ },
+
+ getIcon(aTab) {
+ let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
+ return browser.mIconURL;
+ },
+
+ setPageInfo(aURL, aDescription, aPreviewImage) {
+ if (aURL) {
+ let pageInfo = {
+ url: aURL,
+ description: aDescription,
+ previewImageURL: aPreviewImage,
+ };
+ PlacesUtils.history.update(pageInfo).catch(console.error);
+ }
+ },
+
+ getWindowTitleForBrowser(aBrowser) {
+ let docElement = document.documentElement;
+ let title = "";
+ let dataSuffix =
+ docElement.getAttribute("privatebrowsingmode") == "temporary"
+ ? "Private"
+ : "Default";
+ let defaultTitle = docElement.dataset["title" + dataSuffix];
+
+ if (
+ !this._shouldExposeContentTitle ||
+ (PrivateBrowsingUtils.isWindowPrivate(window) &&
+ !this._shouldExposeContentTitlePbm)
+ ) {
+ return defaultTitle;
+ }
+
+ // 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;
+ } else if (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
+ );
+ }
+
+ return defaultTitle;
+ },
+
+ updateTitlebar() {
+ document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
+ },
+
+ updateCurrentBrowser(aForceUpdate) {
+ let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
+ if (this.selectedBrowser == newBrowser && !aForceUpdate) {
+ return;
+ }
+
+ let newTab = this.getTabForBrowser(newBrowser);
+
+ if (!aForceUpdate) {
+ TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
+
+ if (gMultiProcessBrowser) {
+ this._asyncTabSwitching = true;
+ this._getSwitcher().requestTab(newTab);
+ this._asyncTabSwitching = false;
+ }
+
+ document.commandDispatcher.lock();
+ }
+
+ let oldTab = this.selectedTab;
+
+ // 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 oldBrowser = this.selectedBrowser;
+
+ if (!gMultiProcessBrowser) {
+ oldBrowser.removeAttribute("primary");
+ oldBrowser.docShellIsActive = false;
+ newBrowser.setAttribute("primary", "true");
+ newBrowser.docShellIsActive = !document.hidden;
+ }
+
+ if (gURLBar) {
+ oldBrowser._urlbarSelectionStart = gURLBar.selectionStart;
+ oldBrowser._urlbarSelectionEnd = gURLBar.selectionEnd;
+ }
+
+ this._selectedBrowser = newBrowser;
+ this._selectedTab = newTab;
+ this.showTab(newTab);
+
+ this._appendStatusPanel();
+
+ this._updateVisibleNotificationBox(newBrowser);
+
+ 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
+ );
+ }
+
+ let listener = this._tabListeners.get(newTab);
+ if (listener && listener.mStateFlags) {
+ this._callProgressListeners(
+ null,
+ "onUpdateCurrentBrowser",
+ [
+ listener.mStateFlags,
+ listener.mStatus,
+ listener.mMessage,
+ listener.mTotalProgress,
+ ],
+ 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;
+ }
+
+ this.updateTitlebar();
+
+ newTab.removeAttribute("titlechanged");
+ newTab.attention = false;
+
+ // The tab has been selected, it's not unselected anymore.
+ // (1) Call the current tab's finishUnselectedTabHoverTimer()
+ // to save a telemetry record.
+ // (2) Call the current browser's unselectedTabHover() with false
+ // to dispatch an event.
+ newTab.finishUnselectedTabHoverTimer();
+ 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._tabAttrModified(oldTab, ["selected"]);
+ this._tabAttrModified(newTab, ["selected"]);
+
+ this.readNotificationBox(newBrowser)?.shown();
+
+ 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 (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) {
+ oldBrowser.getInPermitUnload(inPermitUnload => {
+ if (!inPermitUnload) {
+ return;
+ }
+ // Since the user is switching away from a tab that has
+ // a beforeunload prompt active, we remove the prompt.
+ // This prevents confusing user flows like the following:
+ // 1. User attempts to close Firefox
+ // 2. User switches tabs (ingoring a beforeunload prompt)
+ // 3. User returns to tab, presses "Leave page"
+ let promptBox = this.getTabModalPromptBox(oldBrowser);
+ let prompts = promptBox.listPrompts();
+ // There might not be any prompts here if the tab was closed
+ // while in an onbeforeunload prompt, which will have
+ // destroyed aforementioned prompt already, so check there's
+ // something to remove, first:
+ if (prompts.length) {
+ // NB: This code assumes that the beforeunload prompt
+ // is the top-most prompt on the tab.
+ prompts[prompts.length - 1].abortPrompt();
+ }
+ });
+ }
+
+ 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();
+ }
+ }
+
+ updateUserContextUIIndicator();
+ gPermissionPanel.updateSharingIndicator();
+
+ // 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;
+
+ oldBrowser._urlbarFocused = gURLBar && gURLBar.focused;
+
+ 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();
+ } else if (
+ 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 && newBrowser._urlbarFocused && gURLBar && 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;
+ }
+ if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
+ // If there's a tabmodal prompt showing, focus it.
+ let prompts = newBrowser.tabModalPromptBox.listPrompts();
+ let prompt = prompts[prompts.length - 1];
+ // @tabmodalPromptShowing is also set for other tab modal prompts
+ // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
+ // Bug 1492814 will implement this for the Payment Request dialog.
+ if (prompt) {
+ prompt.Dialog.setDefaultFocus();
+ 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 (newBrowser._urlbarFocused && gURLBar) {
+ 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;
+ }
+
+ if (currentActiveElement != document.activeElement) {
+ return;
+ }
+
+ gURLBar.setSelectionRange(
+ newBrowser._urlbarSelectionStart,
+ newBrowser._urlbarSelectionEnd
+ );
+ },
+ { once: true }
+ );
+ } else {
+ gURLBar.setSelectionRange(
+ newBrowser._urlbarSelectionStart,
+ newBrowser._urlbarSelectionEnd
+ );
+ }
+ };
+
+ // 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,
+
+ 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) {
+ return false;
+ }
+ delete aTab._labelIsInitialTitle;
+ }
+
+ let isURL = false;
+ 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;
+ }
+ }
+
+ return this._setTabLabel(aTab, title, { isContentTitle, isURL });
+ },
+
+ // 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) {
+ return this._setTabLabel(aTab, aLabel);
+ },
+
+ _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
+ if (!aLabel || aLabel.includes("about:reader?")) {
+ return false;
+ }
+
+ // 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) {
+ return false;
+ }
+
+ let dwu = window.windowUtils;
+ let isRTL =
+ dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
+
+ aTab.setAttribute("label", aLabel);
+ aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
+ aTab.toggleAttribute("labelendaligned", isRTL != (document.dir == "rtl"));
+
+ // Dispatch TabAttrModified event unless we're setting the label
+ // before the TabOpen event was dispatched.
+ if (!beforeTabOpen) {
+ this._tabAttrModified(aTab, ["label"]);
+ }
+
+ if (aTab.selected) {
+ this.updateTitlebar();
+ }
+
+ return true;
+ },
+
+ loadTabs(
+ aURIs,
+ {
+ allowInheritPrincipal,
+ allowThirdPartyFixup,
+ inBackground,
+ newIndex,
+ postDatas,
+ replace,
+ targetTab,
+ triggeringPrincipal,
+ csp,
+ userContextId,
+ fromExternal,
+ } = {}
+ ) {
+ if (!aURIs.length) {
+ return;
+ }
+
+ // 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 flags = LOAD_FLAGS_NONE;
+ if (allowThirdPartyFixup) {
+ flags |=
+ LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (!allowInheritPrincipal) {
+ flags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+ if (fromExternal) {
+ flags |= LOAD_FLAGS_FROM_EXTERNAL;
+ }
+ try {
+ browser.fixupAndLoadURIString(aURIs[0], {
+ flags,
+ 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;
+ }
+ },
+
+ updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) {
+ let isRemote = aBrowser.getAttribute("remote") == "true";
+
+ // 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) {
+ throw new Error("Remote type must be set!");
+ }
+
+ let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
+
+ if (!gMultiProcessBrowser && shouldBeRemote) {
+ throw new 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)
+ ) {
+ return false;
+ }
+
+ 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);
+
+ let wasActive = document.activeElement == aBrowser;
+
+ // 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();
+
+ if (shouldBeRemote) {
+ aBrowser.setAttribute("remote", "true");
+ aBrowser.setAttribute("remoteType", remoteType);
+ } else {
+ aBrowser.setAttribute("remote", "false");
+ aBrowser.removeAttribute("remoteType");
+ }
+
+ // 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();
+ }
+
+ aBrowser.droppedLinkHandler = oldDroppedLinkHandler;
+
+ // 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");
+ // we call updatetabIndicatorAttr here, rather than _tabAttrModified, so as
+ // to be consistent with how "crashed" attribute changes are handled elsewhere
+ this.tabContainer.updateTabIndicatorAttr(tab);
+ }
+
+ if (wasActive) {
+ aBrowser.focus();
+ }
+
+ // If the findbar has been initialised, reset its browser reference.
+ if (this.isFindBarInitialized(tab)) {
+ this.getCachedFindBar(tab).browser = aBrowser;
+ }
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ return true;
+ },
+
+ updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
+ if (!gMultiProcessBrowser) {
+ return this.updateBrowserRemoteness(aBrowser, {
+ remoteType: E10SUtils.NOT_REMOTE,
+ });
+ }
+
+ let oldRemoteType = aBrowser.remoteType;
+
+ 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) {
+ return this.updateBrowserRemoteness(aBrowser, aOptions);
+ }
+
+ return false;
+ },
+
+ createBrowser({
+ isPreloadBrowser,
+ name,
+ openWindowInfo,
+ remoteType,
+ initialBrowsingContextGroupId,
+ uriIsAboutBlank,
+ userContextId,
+ skipLoad,
+ } = {}) {
+ let b = document.createXULElement("browser");
+ // Use the JSM global to create the permanentKey, so that if the
+ // permanentKey is held by something after this window closes, it
+ // doesn't keep the window alive.
+ b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
+
+ // Ensure that SessionStore has flushed any session history state from the
+ // content process before we this browser's remoteness.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ b.prepareToChangeRemoteness = () =>
+ SessionStore.prepareToChangeRemoteness(b);
+ b.afterChangeRemoteness = switchId => {
+ let tab = this.getTabForBrowser(b);
+ SessionStore.finishTabRemotenessChange(tab, switchId);
+ return true;
+ };
+ }
+
+ const defaultBrowserAttributes = {
+ contextmenu: "contentAreaContextMenu",
+ message: "true",
+ messagemanagergroup: "browsers",
+ tooltip: "aHTMLTooltip",
+ type: "content",
+ manualactiveness: "true",
+ };
+ for (let attribute in defaultBrowserAttributes) {
+ b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
+ }
+
+ 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);
+ }
+
+ let notificationbox = document.createXULElement("notificationbox");
+ notificationbox.setAttribute("notificationside", "top");
+
+ let stack = document.createXULElement("stack");
+ stack.className = "browserStack";
+ stack.appendChild(b);
+
+ let browserContainer = document.createXULElement("vbox");
+ browserContainer.className = "browserContainer";
+ browserContainer.appendChild(notificationbox);
+ 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,
+ });
+ }
+ },
+
+ _insertBrowser(aTab, aInsertedOnTabCreation) {
+ "use strict";
+
+ // 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 { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams;
+ delete aTab._browserParams;
+ delete browser._cachedCurrentURI;
+
+ 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);
+
+ browser.droppedLinkHandler = handleDroppedLink;
+ browser.loadURI = URILoadingWrapper.loadURI.bind(
+ URILoadingWrapper,
+ browser
+ );
+ browser.fixupAndLoadURIString =
+ URILoadingWrapper.fixupAndLoadURIString.bind(
+ URILoadingWrapper,
+ browser
+ );
+
+ // 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";
+
+ if (
+ !aTab ||
+ aTab.selected ||
+ aTab.closing ||
+ this._windowIsClosing ||
+ !browser.isConnected ||
+ !browser.isRemoteBrowser ||
+ !browser.permitUnload(action).permitUnload
+ ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ discardBrowser(aTab, aForceDiscard) {
+ "use strict";
+ let browser = aTab.linkedBrowser;
+
+ if (!this._mayDiscardBrowser(aTab, aForceDiscard)) {
+ return false;
+ }
+
+ // 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();
+
+ this._tabListeners.delete(aTab);
+ this._tabFilters.delete(aTab);
+
+ // 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);
+ }
+
+ browser.destroy();
+ this.getPanel(browser).remove();
+ aTab.removeAttribute("linkedpanel");
+
+ this._createLazyBrowser(aTab);
+
+ let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
+ aTab.dispatchEvent(evt);
+ return true;
+ },
+
+ /**
+ * 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) {
+ throw new Error(
+ "System principal should never be passed into addWebTab()"
+ );
+ }
+ return this.addTab(aURI, params);
+ },
+
+ addAdjacentNewTab(tab) {
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: new Promise(resolve => {
+ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
+ index: tab._tPos + 1,
+ userContextId: tab.userContextId,
+ });
+ resolve(this.selectedBrowser);
+ }),
+ },
+ "browser-open-newtab-start"
+ );
+ },
+
+ /**
+ * Must only be used sparingly for content that came from Chrome context
+ * If in doubt use addWebTab
+ */
+ addTrustedTab(aURI, params = {}) {
+ params.triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ return this.addTab(aURI, params);
+ },
+
+ /**
+ * @returns {object}
+ * 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,
+ triggeringPrincipal,
+ userContextId,
+ csp,
+ skipLoad = createLazyBrowser,
+ insertTab = true,
+ globalHistoryOptions,
+ triggeringRemoteType,
+ wasSchemelessInput,
+ } = {}
+ ) {
+ // all callers of addTab that pass a params object need to pass
+ // a valid triggeringPrincipal.
+ if (!triggeringPrincipal) {
+ throw new 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;
+
+ // Don't use document.l10n.setAttributes because the FTL file is loaded
+ // lazily and we won't be able to resolve the string.
+ document
+ .getElementById("History:UndoCloseTab")
+ .setAttribute("data-l10n-args", JSON.stringify({ tabCount: 1 }));
+
+ // 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);
+
+ // 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.getAttribute("overflow") != "true" &&
+ !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,
+ createLazyBrowser,
+ skipAnimation,
+ pinned,
+ noInitialLabel,
+ skipBackgroundNotify,
+ });
+ if (insertTab) {
+ // insert the tab into the tab container in the correct position
+ this._insertTabAtIndex(t, {
+ index,
+ ownerTab,
+ openerTab,
+ pinned,
+ bulkOrderedOpen,
+ });
+ }
+
+ ({ browser: b, usingPreloadedContent } = this._createBrowserForTab(t, {
+ uriString,
+ uri,
+ preferredRemoteType,
+ openerBrowser,
+ uriIsAboutBlank,
+ referrerInfo,
+ forceNotRemote,
+ name,
+ initialBrowsingContextGroupId,
+ openWindowInfo,
+ skipLoad,
+ }));
+
+ if (focusUrlBar) {
+ 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.setCrossGroupOpener(
+ 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();
+ }
+ return null;
+ }
+
+ if (insertTab) {
+ // Fire a TabOpen event
+ this._fireTabOpen(t, eventDetail);
+
+ this._kickOffBrowserLoad(b, {
+ uri,
+ uriString,
+ usingPreloadedContent,
+ triggeringPrincipal,
+ originPrincipal,
+ originStoragePrincipal,
+ uriIsAboutBlank,
+ allowInheritPrincipal,
+ allowThirdPartyFixup,
+ fromExternal,
+ disableTRR,
+ forceAllowDataURI,
+ skipLoad,
+ referrerInfo,
+ charset,
+ postData,
+ csp,
+ globalHistoryOptions,
+ triggeringRemoteType,
+ wasSchemelessInput,
+ });
+ }
+
+ // 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) {
+ requestAnimationFrame(function () {
+ // kick the animation off
+ t.setAttribute("fadein", "true");
+ });
+ }
+
+ // Additionally send pinned tab events
+ if (pinned) {
+ this._notifyPinnedStatus(t);
+ }
+
+ gSharedTabWarning.tabAdded(t);
+
+ if (!inBackground) {
+ this.selectedTab = t;
+ }
+ return t;
+ },
+
+ _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";
+ }
+
+ let uriIsAboutBlank = uriString == "about:blank";
+ return { uri: aURIObject, uriIsAboutBlank, lazyBrowserURI, uriString };
+ },
+
+ _createTab({
+ uriString,
+ userContextId,
+ openerTab,
+ createLazyBrowser,
+ skipAnimation,
+ 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,
+ });
+ }
+
+ tab.linkedBrowser = b;
+
+ this._tabForBrowser.set(b, tab);
+ tab.permanentKey = b.permanentKey;
+ tab._browserParams = {
+ uriIsAboutBlank,
+ remoteType,
+ usingPreloadedContent,
+ };
+
+ if (BookmarkingUI.isOnNewTabPage(uri)) {
+ this.getPanel(b).classList.add("newTabBrowserPanel");
+ }
+
+ // 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,
+ wasSchemelessInput,
+ }
+ ) {
+ 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 flags = LOAD_FLAGS_NONE;
+ if (allowThirdPartyFixup) {
+ flags |=
+ LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (fromExternal) {
+ flags |= LOAD_FLAGS_FROM_EXTERNAL;
+ } else if (!triggeringPrincipal.isSystemPrincipal) {
+ // XXX this code must be reviewed and changed when bug 1616353
+ // lands.
+ flags |= LOAD_FLAGS_FIRST_LOAD;
+ }
+ if (!allowInheritPrincipal) {
+ flags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+ if (disableTRR) {
+ flags |= LOAD_FLAGS_DISABLE_TRR;
+ }
+ if (forceAllowDataURI) {
+ flags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
+ }
+ try {
+ browser.fixupAndLoadURIString(uriString, {
+ flags,
+ triggeringPrincipal,
+ referrerInfo,
+ charset,
+ postData,
+ csp,
+ globalHistoryOptions,
+ triggeringRemoteType,
+ wasSchemelessInput,
+ });
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ createTabsForSessionRestore(restoreTabsLazily, selectTab, tabDataList) {
+ let tabs = [];
+ let tabsFragment = document.createDocumentFragment();
+ let tabToSelect = null;
+ let hiddenTabs = new Map();
+ let shouldUpdateForPinnedTabs = false;
+
+ // 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.
+ if (
+ select &&
+ this.selectedTab.userContextId == userContextId &&
+ !SessionStore.isTabRestoring(this.selectedTab)
+ ) {
+ tabWasReused = true;
+ tab = this.selectedTab;
+ if (!tabData.pinned) {
+ this.unpinTab(tab);
+ } else {
+ this.pinTab(tab);
+ }
+ }
+
+ // Add a new tab if needed.
+ if (!tab) {
+ let createLazyBrowser =
+ restoreTabsLazily && !select && !tabData.pinned;
+
+ let url = "about:blank";
+ if (tabData.entries?.length) {
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ // Ensure the index is in bounds.
+ activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
+ activeIndex = Math.max(activeIndex, 0);
+ url = tabData.entries[activeIndex].url;
+ }
+
+ let preferredRemoteType = E10SUtils.getRemoteTypeForURI(
+ url,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ E10SUtils.DEFAULT_REMOTE_TYPE,
+ null,
+ E10SUtils.predictOriginAttributes({ window, userContextId })
+ );
+
+ // If we're creating a lazy browser, let tabbrowser know the future
+ // URI because progress listeners won't get onLocationChange
+ // notification before the browser is inserted.
+ //
+ // Setting noInitialLabel is a perf optimization. Rendering tab labels
+ // would make resizing the tabs more expensive as we're adding them.
+ // Each tab will get its initial label set in restoreTab.
+ tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", {
+ createLazyBrowser,
+ skipAnimation: true,
+ noInitialLabel: true,
+ userContextId,
+ skipBackgroundNotify: true,
+ bulkOrderedOpen: true,
+ insertTab: false,
+ skipLoad: true,
+ preferredRemoteType,
+ });
+
+ if (select) {
+ tabToSelect = tab;
+ }
+ }
+
+ 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._numPinnedTabs;
+ this.tabContainer.insertBefore(tab, this.tabs[this._numPinnedTabs]);
+ 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;
+ }
+ } 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 new DOM nodes
+ this.tabContainer.appendChild(tabsFragment);
+
+ 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, aSource) {
+ if (tabsToClose <= 1) {
+ return true;
+ }
+
+ const pref =
+ aCloseTabs == this.closingTabsEnum.ALL
+ ? "browser.tabs.warnOnClose"
+ : "browser.tabs.warnOnCloseOtherTabs";
+ var shouldPrompt = Services.prefs.getBoolPref(pref);
+ if (!shouldPrompt) {
+ return true;
+ }
+
+ const maxTabsUndo = Services.prefs.getIntPref(
+ "browser.sessionstore.max_tabs_undo"
+ );
+ if (
+ aCloseTabs != this.closingTabsEnum.ALL &&
+ tabsToClose <= maxTabsUndo
+ ) {
+ return true;
+ }
+
+ // Our prompt to close this window is most important, so replace others.
+ gDialogBox.replaceDialogIfOpen();
+
+ var ps = Services.prompt;
+
+ // 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-confirm-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
+ );
+
+ Services.telemetry.setEventRecordingEnabled("close_tab_warning", true);
+ let closeTabEnumKey =
+ Object.entries(this.closingTabsEnum)
+ .find(([k, v]) => v == aCloseTabs)?.[0]
+ ?.toLowerCase() || "some";
+
+ let warnCheckbox = warnOnClose.value ? "checked" : "unchecked";
+ if (!checkboxLabel) {
+ warnCheckbox = "not-present";
+ }
+ let sessionWillBeRestored =
+ Services.prefs.getIntPref("browser.startup.page") == 3 ||
+ Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+ let closesWindow = aCloseTabs == this.closingTabsEnum.ALL;
+ Services.telemetry.recordEvent(
+ "close_tab_warning",
+ "shown",
+ closesWindow ? "window" : "tabs",
+ null,
+ {
+ source: aSource || `close-${closeTabEnumKey}-tabs`,
+ button: buttonPressed == 0 ? "close" : "cancel",
+ warn_checkbox: warnCheckbox,
+ closing_tabs: "" + tabsToClose,
+ closing_wins: "" + +closesWindow, // ("1" or "0", depending on the value)
+ // This value doesn't really apply to whether this warning
+ // gets shown, but having pings be consistent (and perhaps
+ // being able to see trends for users with/without sessionrestore)
+ // seems useful:
+ will_restore: sessionWillBeRestored ? "yes" : "no",
+ }
+ );
+
+ 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
+ */
+ _insertTabAtIndex(
+ tab,
+ { index, ownerTab, openerTab, pinned, bulkOrderedOpen } = {}
+ ) {
+ // 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 (!previousTab.hidden) {
+ index = previousTab._tPos + 1;
+ } else if (previousTab == FirefoxViewHandler.tab) {
+ index = 0;
+ }
+
+ if (lastRelatedTab) {
+ lastRelatedTab.owner = null;
+ } else if (openerTab) {
+ tab.owner = openerTab;
+ }
+ // Always set related map if opener exists.
+ if (openerTab) {
+ this._lastRelatedTabMap.set(openerTab, tab);
+ }
+ }
+ }
+ // Ensure index is within bounds.
+ if (pinned) {
+ index = Math.max(index, 0);
+ index = Math.min(index, this._numPinnedTabs);
+ } else {
+ index = Math.max(index, this._numPinnedTabs);
+ index = Math.min(index, this.tabs.length);
+ }
+
+ let tabAfter = this.tabs[index] || null;
+ this.tabContainer._invalidateCachedTabs();
+ // Prevent a flash of unstyled content by setting up the tab content
+ // and inherited attributes before appending it (see Bug 1592054):
+ tab.initialize();
+ this.tabContainer.insertBefore(tab, tabAfter);
+ if (tabAfter) {
+ this._updateTabsAfterInsert();
+ } else {
+ tab._tPos = index;
+ }
+
+ 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);
+ },
+
+ getTabsToTheStartFrom(aTab) {
+ let tabsToStart = [];
+ if (aTab.hidden) {
+ return tabsToStart;
+ }
+ let tabs = this.visibleTabs;
+ for (let i = 0; i < tabs.length; ++i) {
+ if (tabs[i] == aTab) {
+ break;
+ }
+ // Ignore pinned tabs.
+ if (tabs[i].pinned) {
+ 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;
+ },
+
+ getTabsToTheEndFrom(aTab) {
+ let tabsToEnd = [];
+ if (aTab.hidden) {
+ return tabsToEnd;
+ }
+ let tabs = this.visibleTabs;
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ if (tabs[i] == aTab) {
+ break;
+ }
+ // Ignore pinned tabs.
+ if (tabs[i].pinned) {
+ 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;
+ },
+
+ /**
+ * 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 aTab The tab we will skip removing
+ * @param 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;
+
+ let filterFn;
+
+ // If enabled also filter by selected or pinned state.
+ if (skipPinnedOrSelectedTabs) {
+ if (aTab?.multiselected) {
+ filterFn = tab => !tab.multiselected && !tab.pinned;
+ } else {
+ filterFn = tab => tab != aTab && !tab.pinned;
+ }
+ } else {
+ // Exclude just aTab from being removed.
+ filterFn = tab => tab != aTab;
+ }
+
+ let tabsToRemove = this.visibleTabs.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} 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,
+ suppressWarnAboutClosingWindow,
+ skipPermitUnload,
+ skipRemoves,
+ skipSessionStore,
+ }
+ ) {
+ // Note: if you change any of the unload algorithm, consider also
+ // changing `runBeforeUnloadForTabs` above.
+ let tabsWithBeforeUnloadPrompt = [];
+ let tabsWithoutBeforeUnload = [];
+ let beforeUnloadPromises = [];
+ let lastToClose;
+
+ for (let tab of tabs) {
+ if (!skipRemoves) {
+ tab._closedInGroup = true;
+ }
+ if (!skipRemoves && tab.selected) {
+ lastToClose = tab;
+ let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
+ if (toBlurTo) {
+ this._getSwitcher().warmupTab(toBlurTo);
+ }
+ } else if (!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.
+ } else if (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);
+ }
+ }
+
+ // 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,
+ });
+ }
+ }
+
+ return {
+ beforeUnloadComplete: Promise.all(beforeUnloadPromises),
+ tabsWithBeforeUnloadPrompt,
+ lastToClose,
+ };
+ },
+
+ /**
+ * 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) {
+ return true;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return false;
+ },
+
+ /**
+ * Removes multiple tabs from the tab browser.
+ *
+ * @param {object[]} 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;
+ }
+
+ SessionStore.resetLastClosedTabCount(window);
+ this._clearMultiSelectionLocked = true;
+
+ // Guarantee that _clearMultiSelectionLocked lock gets released.
+ try {
+ let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt, lastToClose } =
+ this._startRemoveTabs(tabs, {
+ animate,
+ suppressWarnAboutClosingWindow,
+ skipPermitUnload,
+ skipRemoves: false,
+ skipSessionStore,
+ });
+
+ // 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._closedInGroup = 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);
+ }
+
+ this._clearMultiSelectionLocked = false;
+ this._avoidSingleSelectedTab();
+ // Don't use document.l10n.setAttributes because the FTL file is loaded
+ // lazily and we won't be able to resolve the string.
+ document.getElementById("History:UndoCloseTab").setAttribute(
+ "data-l10n-args",
+ JSON.stringify({
+ tabCount: SessionStore.getLastClosedTabCount(window),
+ })
+ );
+ },
+
+ removeCurrentTab(aParams) {
+ this.removeTab(this.selectedTab, aParams);
+ },
+
+ removeTab(
+ aTab,
+ {
+ animate,
+ triggeringEvent,
+ skipPermitUnload,
+ closeWindowWithLastTab,
+ prewarmed,
+ skipSessionStore,
+ } = {}
+ ) {
+ if (UserInteraction.running("browser.tabs.opening", window)) {
+ UserInteraction.finish("browser.tabs.opening", window);
+ }
+
+ // 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 isLastTab = !aTab.hidden && 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;
+
+ if (
+ !this._beginRemoveTab(aTab, {
+ closeWindowFastpath: true,
+ skipPermitUnload,
+ closeWindowWithLastTab,
+ prewarmed,
+ skipSessionStore,
+ })
+ ) {
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
+ return;
+ }
+
+ let lockTabSizing =
+ !aTab.pinned &&
+ !aTab.hidden &&
+ aTab._fullyOpen &&
+ triggeringEvent?.inputSource == MouseEvent.MOZ_SOURCE_MOUSE &&
+ triggeringEvent?.target.closest(".tabbrowser-tab");
+ if (lockTabSizing) {
+ this.tabContainer._lockTabSizing(aTab, tabWidth);
+ } else {
+ this.tabContainer._unlockTabSizing();
+ }
+
+ if (
+ !animate /* the caller didn't opt in */ ||
+ gReduceMotion ||
+ isLastTab ||
+ aTab.pinned ||
+ aTab.hidden ||
+ this._removingTabs.size >
+ 3 /* don't want lots of concurrent animations */ ||
+ !aTab.hasAttribute(
+ "fadein"
+ ) /* fade-in transition hasn't been triggered yet */ ||
+ tabWidth == 0 /* fade-in transition hasn't moved yet */
+ ) {
+ // We're not animating, so we can cancel the animation stopwatch.
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ // We're animating, so we can cancel the non-animation stopwatch.
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
+
+ aTab.style.maxWidth = ""; // ensure that fade-out transition happens
+ aTab.removeAttribute("fadein");
+ aTab.removeAttribute("bursting");
+
+ setTimeout(
+ function (tab, tabbrowser) {
+ if (
+ tab.container &&
+ window.getComputedStyle(tab).maxWidth == "0.1px"
+ ) {
+ console.assert(
+ false,
+ "Giving up waiting for the tab closing animation to finish (bug 608589)"
+ );
+ tabbrowser._endRemoveTab(tab);
+ }
+ },
+ 3000,
+ aTab,
+ this
+ );
+ },
+
+ _hasBeforeUnload(aTab) {
+ let browser = aTab.linkedBrowser;
+ if (browser.isRemoteBrowser && browser.frameLoader) {
+ return browser.hasBeforeUnload;
+ }
+ return false;
+ },
+
+ _beginRemoveTab(
+ aTab,
+ {
+ adoptedByTab,
+ closeWindowWithLastTab,
+ closeWindowFastpath,
+ skipPermitUnload,
+ prewarmed,
+ skipSessionStore = false,
+ } = {}
+ ) {
+ if (aTab.closing || this._windowIsClosing) {
+ return false;
+ }
+
+ var browser = this.getBrowserForTab(aTab);
+ if (
+ !skipPermitUnload &&
+ !adoptedByTab &&
+ aTab.linkedPanel &&
+ !aTab._pendingPermitUnload &&
+ (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
+ ) {
+ if (!prewarmed) {
+ let blurTab = this._findTabToBlurTo(aTab);
+ if (blurTab) {
+ this.warmupTab(blurTab);
+ }
+ }
+
+ TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
+
+ // 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;
+
+ TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
+
+ // 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) {
+ return false;
+ }
+ }
+
+ // 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;
+ if (!aTab.hidden && this.visibleTabs.length == 1) {
+ 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"
+ );
+ return false;
+ }
+
+ newTab = true;
+ }
+ aTab._endRemoveArgs = [closeWindow, newTab];
+
+ // 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);
+ }
+ return true;
+ }
+
+ 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);
+ }
+
+ aTab.closing = true;
+ this._removingTabs.add(aTab);
+ this.tabContainer._invalidateCachedTabs();
+
+ // Invalidate hovered tab state tracking for this closing tab.
+ aTab._mouseleave();
+
+ if (newTab) {
+ this.addTrustedTab(BROWSER_NEW_TAB_URL, {
+ skipAnimation: true,
+ });
+ } else {
+ TabBarVisibility.update();
+ }
+
+ // Splice this tab out of any lines of succession before any events are
+ // dispatched.
+ this.replaceInSuccession(aTab, aTab.successor);
+ this.setSuccessor(aTab, null);
+
+ // We're committed to closing the tab now.
+ // Dispatch a notification.
+ // We dispatch it before any teardown so that event listeners can
+ // inspect the tab that's about to close.
+ let evt = new CustomEvent("TabClose", {
+ bubbles: true,
+ detail: { adoptedBy: adoptedByTab, skipSessionStore },
+ });
+ 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);
+
+ browser.webProgress.removeProgressListener(filter);
+
+ const listener = this._tabListeners.get(aTab);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+ }
+
+ 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");
+
+ return true;
+ },
+
+ _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);
+ }
+ } else if (!this._windowIsClosing) {
+ if (aNewTab) {
+ gURLBar.select();
+ }
+
+ // workaround for bug 345399
+ this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
+ }
+
+ // 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();
+
+ // Update hashiddentabs if this tab was hidden.
+ if (aTab.hidden) {
+ this.tabContainer._updateHiddenTabsStatus();
+ }
+
+ // ... 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();
+
+ setTimeout(
+ function (tabs) {
+ tabs._lastTabClosedByMouse = false;
+ },
+ 0,
+ this.tabContainer
+ );
+ }
+
+ // 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 */
+ );
+
+ if (aCloseWindow) {
+ this._windowIsClosing = closeWindow(
+ true,
+ window.warnAboutClosingWindow,
+ "close-last-tab"
+ );
+ }
+ },
+
+ /**
+ * 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) {
+ BrowserOpenTab({ 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 aTab
+ * The tab we would blur
+ * @param aExcludeTabs
+ * Tabs to exclude from our search (i.e., because they are being
+ * closed along with aTab)
+ */
+ _findTabToBlurTo(aTab, aExcludeTabs = []) {
+ if (!aTab.selected) {
+ return null;
+ }
+ 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;
+ }
+
+ if (
+ aTab.owner &&
+ !aTab.owner.hidden &&
+ !aTab.owner.closing &&
+ !excludeTabs.has(aTab.owner) &&
+ Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
+ ) {
+ return aTab.owner;
+ }
+
+ // Switch to a visible tab unless there aren't any others remaining
+ let remainingTabs = this.visibleTabs;
+ let numTabs = remainingTabs.length;
+ if (numTabs == 0 || (numTabs == 1 && remainingTabs[0] == aTab)) {
+ remainingTabs = Array.prototype.filter.call(
+ this.tabs,
+ tab => !tab.closing && !excludeTabs.has(tab)
+ );
+ }
+
+ // Try to find a remaining tab that comes after the given tab
+ let tab = this.tabContainer.findNextTab(aTab, {
+ direction: 1,
+ filter: _tab => remainingTabs.includes(_tab),
+ });
+
+ if (!tab) {
+ tab = this.tabContainer.findNextTab(aTab, {
+ direction: -1,
+ filter: _tab => remainingTabs.includes(_tab),
+ });
+ }
+
+ return tab;
+ },
+
+ _blurTab(aTab) {
+ this.selectedTab = this._findTabToBlurTo(aTab);
+ },
+
+ /**
+ * @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)
+ ) {
+ return false;
+ }
+
+ // Do not allow transfering a useRemoteSubframes tab to a
+ // non-useRemoteSubframes window and vice versa.
+ if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) {
+ return false;
+ }
+
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ let otherBrowser = aOtherTab.linkedBrowser;
+
+ // Can't swap between chrome and content processes.
+ if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
+ return false;
+ }
+
+ // 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,
+ })
+ ) {
+ return false;
+ }
+
+ // 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;
+ }
+
+ SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
+
+ // 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;
+ }
+ }
+
+ this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
+ }
+
+ // 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);
+ }
+
+ return true;
+ },
+
+ 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);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ otherBrowser.webProgress.addProgressListener(filter, notifyAll);
+ },
+
+ _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);
+
+ ourBrowserContainer.hidden = ourBrowserContainerWasHidden;
+ otherBrowserContainer.hidden = otherBrowserContainerWasHidden;
+
+ // Swap permanentKey properties.
+ let ourPermanentKey = ourBrowser.permanentKey;
+ ourBrowser.permanentKey = aOtherBrowser.permanentKey;
+ aOtherBrowser.permanentKey = ourPermanentKey;
+ aOurTab.permanentKey = ourBrowser.permanentKey;
+ if (remoteBrowser) {
+ let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
+ if (otherTab) {
+ otherTab.permanentKey = aOtherBrowser.permanentKey;
+ }
+ }
+
+ // Restore the progress listener
+ tabListener = new TabProgressListener(
+ aOurTab,
+ ourBrowser,
+ false,
+ false,
+ aStateFlags
+ );
+ this._tabListeners.set(aOurTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ ourBrowser.webProgress.addProgressListener(filter, notifyAll);
+ },
+
+ _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;
+ }
+ },
+
+ reloadMultiSelectedTabs() {
+ this.reloadTabs(this.selectedTabs);
+ },
+
+ 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
+ );
+ },
+
+ addTabsProgressListener(aListener) {
+ this.mTabsProgressListeners.push(aListener);
+ },
+
+ removeTabsProgressListener(aListener) {
+ this.mTabsProgressListeners = this.mTabsProgressListeners.filter(
+ l => l != aListener
+ );
+ },
+
+ getBrowserForTab(aTab) {
+ return aTab.linkedBrowser;
+ },
+
+ showOnlyTheseTabs(aTabs) {
+ for (let tab of this.tabs) {
+ if (!aTabs.includes(tab)) {
+ this.hideTab(tab);
+ } else {
+ this.showTab(tab);
+ }
+ }
+
+ this.tabContainer._updateHiddenTabsStatus();
+ this.tabContainer._handleTabSelect(true);
+ },
+
+ showTab(aTab) {
+ if (!aTab.hidden || aTab == FirefoxViewHandler.tab) {
+ return;
+ }
+ aTab.removeAttribute("hidden");
+ this.tabContainer._invalidateCachedVisibleTabs();
+
+ this.tabContainer._updateCloseButtons();
+ this.tabContainer._updateHiddenTabsStatus();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabShow", true, false);
+ aTab.dispatchEvent(event);
+ SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
+ },
+
+ 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();
+ this.tabContainer._updateHiddenTabsStatus();
+
+ // 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;
+ }
+ } else if (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) {
+ return null;
+ }
+
+ 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) {
+ return null;
+ }
+
+ if (tabs.length == 1) {
+ return this.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;
+ },
+
+ _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;
+ },
+
+ moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
+ var oldPosition = aTab._tPos;
+ if (oldPosition == aIndex) {
+ return;
+ }
+
+ // Don't allow mixing pinned and unpinned tabs.
+ if (aTab.pinned) {
+ aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
+ } else {
+ aIndex = Math.max(aIndex, this._numPinnedTabs);
+ }
+ if (oldPosition == aIndex) {
+ return;
+ }
+
+ if (!aKeepRelatedTabs) {
+ this._lastRelatedTabMap = new WeakMap();
+ }
+
+ let wasFocused = document.activeElement == this.selectedTab;
+
+ aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
+
+ let neighbor = this.tabs[aIndex] || null;
+ this.tabContainer._invalidateCachedTabs();
+ this.tabContainer.insertBefore(aTab, neighbor);
+ this._updateTabsAfterInsert();
+
+ if (wasFocused) {
+ this.selectedTab.focus();
+ }
+
+ this.tabContainer._handleTabSelect(true);
+
+ if (aTab.pinned) {
+ this.tabContainer._positionPinnedTabs();
+ }
+
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabMove", true, false, window, oldPosition);
+ aTab.dispatchEvent(evt);
+ },
+
+ moveTabForward() {
+ let nextTab = this.tabContainer.findNextTab(this.selectedTab, {
+ direction: 1,
+ filter: tab => !tab.hidden,
+ });
+
+ if (nextTab) {
+ this.moveTabTo(this.selectedTab, nextTab._tPos);
+ } else if (this.arrowKeysShouldWrap) {
+ this.moveTabToStart();
+ }
+ },
+
+ /**
+ * 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._numPinnedTabs;
+ 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();
+ }
+
+ if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
+ // Swapping wasn't permitted. Bail out.
+ this.removeTab(newTab);
+ return null;
+ }
+
+ if (aSelectTab) {
+ this.selectedTab = newTab;
+ }
+
+ return newTab;
+ },
+
+ moveTabBackward() {
+ let previousTab = this.tabContainer.findNextTab(this.selectedTab, {
+ direction: -1,
+ filter: tab => !tab.hidden,
+ });
+
+ if (previousTab) {
+ this.moveTabTo(this.selectedTab, previousTab._tPos);
+ } else if (this.arrowKeysShouldWrap) {
+ this.moveTabToEnd();
+ }
+ },
+
+ moveTabToStart() {
+ let tabPos = this.selectedTab._tPos;
+ if (tabPos > 0) {
+ this.moveTabTo(this.selectedTab, 0);
+ }
+ },
+
+ moveTabToEnd() {
+ let tabPos = this.selectedTab._tPos;
+ if (tabPos < this.browsers.length - 1) {
+ this.moveTabTo(this.selectedTab, this.browsers.length - 1);
+ }
+ },
+
+ moveTabOver(aEvent) {
+ if (
+ (!RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+ (RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)
+ ) {
+ this.moveTabForward();
+ } else {
+ this.moveTabBackward();
+ }
+ },
+
+ /**
+ * @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
+ );
+ },
+
+ addToMultiSelectedTabs(aTab) {
+ if (aTab.multiselected) {
+ return;
+ }
+
+ aTab.setAttribute("multiselected", "true");
+ aTab.setAttribute("aria-selected", "true");
+ this._multiSelectedTabsSet.add(aTab);
+ this._startMultiSelectChange();
+ if (this._multiSelectChangeRemovals.has(aTab)) {
+ this._multiSelectChangeRemovals.delete(aTab);
+ } else {
+ this._multiSelectChangeAdditions.add(aTab);
+ }
+ },
+
+ /**
+ * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
+ */
+ addRangeToMultiSelectedTabs(aTab1, aTab2) {
+ if (aTab1 == aTab2) {
+ return;
+ }
+
+ const tabs = this.visibleTabs;
+ const indexOfTab1 = tabs.indexOf(aTab1);
+ const indexOfTab2 = tabs.indexOf(aTab2);
+
+ const [lowerIndex, higherIndex] =
+ indexOfTab1 < indexOfTab2
+ ? [Math.max(0, indexOfTab1), indexOfTab2]
+ : [Math.max(0, indexOfTab2), indexOfTab1];
+
+ for (let i = lowerIndex; i <= higherIndex; i++) {
+ this.addToMultiSelectedTabs(tabs[i]);
+ }
+ },
+
+ 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);
+ }
+ },
+
+ 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;
+ },
+
+ selectAllTabs() {
+ let visibleTabs = this.visibleTabs;
+ gBrowser.addRangeToMultiSelectedTabs(
+ visibleTabs[0],
+ visibleTabs[visibleTabs.length - 1]
+ );
+ },
+
+ allTabsSelected() {
+ return (
+ this.visibleTabs.length == 1 ||
+ this.visibleTabs.every(t => t.multiselected)
+ );
+ },
+
+ lockClearMultiSelectionOnce() {
+ this._clearMultiSelectionLockedOnce = true;
+ this._clearMultiSelectionLocked = true;
+ },
+
+ unlockClearMultiSelection() {
+ this._clearMultiSelectionLockedOnce = false;
+ this._clearMultiSelectionLocked = false;
+ },
+
+ /**
+ * 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();
+ }
+ },
+
+ _switchToNextMultiSelectedTab() {
+ this._clearMultiSelectionLocked = true;
+
+ // Guarantee that _clearMultiSelectionLocked lock gets released.
+ try {
+ let lastMultiSelectedTab = this.lastMultiSelectedTab;
+ if (!lastMultiSelectedTab.selected) {
+ this.selectedTab = lastMultiSelectedTab;
+ } else {
+ let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(
+ this._multiSelectedTabsSet
+ ).filter(this._mayTabBeMultiselected);
+ this.selectedTab = selectedTabs.at(-1);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ this._clearMultiSelectionLocked = false;
+ },
+
+ set selectedTabs(tabs) {
+ this.clearMultiSelectedTabs();
+ this.selectedTab = tabs[0];
+ if (tabs.length > 1) {
+ for (let tab of tabs) {
+ this.addToMultiSelectedTabs(tab);
+ }
+ }
+ },
+
+ get selectedTabs() {
+ let { selectedTab, _multiSelectedTabsSet } = this;
+ let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(
+ _multiSelectedTabsSet
+ ).filter(this._mayTabBeMultiselected);
+ if (
+ (!_multiSelectedTabsSet.has(selectedTab) &&
+ this._mayTabBeMultiselected(selectedTab)) ||
+ !tabs.length
+ ) {
+ tabs.push(selectedTab);
+ }
+ return tabs.sort((a, b) => a._tPos > b._tPos);
+ },
+
+ get multiSelectedTabsCount() {
+ return ChromeUtils.nondeterministicGetWeakSetKeys(
+ this._multiSelectedTabsSet
+ ).filter(this._mayTabBeMultiselected).length;
+ },
+
+ get lastMultiSelectedTab() {
+ let tab = this._lastMultiSelectedTabRef
+ ? this._lastMultiSelectedTabRef.get()
+ : null;
+ if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) {
+ return tab;
+ }
+ let selectedTab = this.selectedTab;
+ this.lastMultiSelectedTab = selectedTab;
+ return selectedTab;
+ },
+
+ set lastMultiSelectedTab(aTab) {
+ this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
+ },
+
+ _mayTabBeMultiselected(aTab) {
+ return aTab.isConnected && !aTab.closing && !aTab.hidden;
+ },
+
+ _startMultiSelectChange() {
+ if (!this._multiSelectChangeStarted) {
+ this._multiSelectChangeStarted = true;
+ Promise.resolve().then(() => this._endMultiSelectChange());
+ }
+ },
+
+ _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);
+ }
+ },
+
+ activateBrowserForPrintPreview(aBrowser) {
+ this._printPreviewBrowsers.add(aBrowser);
+ if (this._switcher) {
+ this._switcher.activateBrowserForPrintPreview(aBrowser);
+ }
+ aBrowser.docShellIsActive = true;
+ },
+
+ deactivatePrintPreviewBrowsers() {
+ let browsers = this._printPreviewBrowsers;
+ this._printPreviewBrowsers = new Set();
+ for (let browser of browsers) {
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+ }
+ },
+
+ /**
+ * Returns true if a given browser's docshell should be active.
+ */
+ shouldActivateDocShell(aBrowser) {
+ if (this._switcher) {
+ return this._switcher.shouldActivateDocShell(aBrowser);
+ }
+ return (
+ (aBrowser == this.selectedBrowser && !document.hidden) ||
+ this._printPreviewBrowsers.has(aBrowser) ||
+ this.PictureInPicture.isOriginatingBrowser(aBrowser)
+ );
+ },
+
+ _getSwitcher() {
+ if (!this._switcher) {
+ this._switcher = new this.AsyncTabSwitcher(this);
+ }
+ return this._switcher;
+ },
+
+ 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) {
+ return false;
+ }
+ // If the event target is a remote browser, and the event has not been
+ // handled by the remote content yet, we should wait a reply event
+ // from the content.
+ if (aEvent.isWaitingReplyFromRemoteContent) {
+ return true; // Somebody called requestReplyFromRemoteContent already.
+ }
+ if (
+ !aEvent.isReplyEventFromRemoteContent &&
+ aEvent.target?.isRemoteBrowser === true
+ ) {
+ aEvent.requestReplyFromRemoteContent();
+ return true;
+ }
+ return false;
+ },
+
+ _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();
+ } else if (!this.selectedTab.pinned) {
+ this.removeCurrentTab({ animate: true });
+ }
+ aEvent.preventDefault();
+ }
+ },
+
+ toggleCaretBrowsing() {
+ const kPrefShortcutEnabled =
+ "accessibility.browsewithcaret_shortcut.enabled";
+ const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
+ const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+ var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled);
+ if (!isEnabled || this._awaitingToggleCaretBrowsingPrompt) {
+ return;
+ }
+
+ // Toggle browse with caret mode
+ var browseWithCaretOn = Services.prefs.getBoolPref(
+ kPrefCaretBrowsingOn,
+ false
+ );
+ var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true);
+ if (warn && !browseWithCaretOn) {
+ var checkValue = { value: false };
+ var promptService = Services.prompt;
+
+ try {
+ this._awaitingToggleCaretBrowsingPrompt = true;
+ const [title, message, checkbox] =
+ this.tabLocalization.formatValuesSync([
+ "tabbrowser-confirm-caretbrowsing-title",
+ "tabbrowser-confirm-caretbrowsing-message",
+ "tabbrowser-confirm-caretbrowsing-checkbox",
+ ]);
+ var buttonPressed = promptService.confirmEx(
+ window,
+ title,
+ message,
+ // Make "No" the default:
+ promptService.STD_YES_NO_BUTTONS |
+ promptService.BUTTON_POS_1_DEFAULT,
+ null,
+ null,
+ null,
+ checkbox,
+ checkValue
+ );
+ } catch (ex) {
+ return;
+ } finally {
+ this._awaitingToggleCaretBrowsingPrompt = false;
+ }
+ if (buttonPressed != 0) {
+ if (checkValue.value) {
+ try {
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, false);
+ } catch (ex) {}
+ }
+ return;
+ }
+ if (checkValue.value) {
+ try {
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, false);
+ } catch (ex) {}
+ }
+ }
+
+ // Toggle the pref
+ try {
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
+ } catch (ex) {}
+ },
+
+ _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(1, true);
+ aEvent.preventDefault();
+ }
+ break;
+ case ShortcutUtils.PREVIOUS_TAB:
+ if (AppConstants.platform == "macosx") {
+ this.tabContainer.advanceSelectedTab(-1, true);
+ aEvent.preventDefault();
+ }
+ break;
+ }
+ },
+
+ getTabTooltip(tab, includeLabel = true) {
+ let labelArray = [];
+ if (includeLabel) {
+ labelArray.push(tab._fullLabel || tab.getAttribute("label"));
+ }
+ if (
+ Services.prefs.getBoolPref(
+ "browser.tabs.tooltipsShowPidAndActiveness",
+ false
+ )
+ ) {
+ if (tab.linkedBrowser) {
+ // Show the PIDs of the content process and remote subframe processes.
+ let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
+ tab.linkedBrowser,
+ gFissionBrowser
+ );
+ if (contentPid) {
+ if (framePids && framePids.length) {
+ labelArray.push(
+ `(pids ${contentPid}, ${framePids.sort().join(", ")})`
+ );
+ } else {
+ labelArray.push(`(pid ${contentPid})`);
+ }
+ }
+ if (tab.linkedBrowser.docShellIsActive) {
+ labelArray.push("[A]");
+ }
+ }
+ }
+
+ let label = labelArray.join(" ");
+ if (tab.userContextId) {
+ const containerName = ContextualIdentityService.getUserContextLabel(
+ tab.userContextId
+ );
+ label = this.tabLocalization.formatValueSync(
+ "tabbrowser-container-tab-title",
+ { title: label, containerName }
+ );
+ }
+
+ 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) {
+ event.preventDefault();
+ return;
+ }
+
+ const tooltip = event.target;
+ tooltip.removeAttribute("data-l10n-id");
+
+ const tabCount = this.selectedTabs.includes(tab)
+ ? this.selectedTabs.length
+ : 1;
+ if (tab.mOverCloseButton) {
+ tooltip.label = "";
+ document.l10n.setAttributes(tooltip, "tabbrowser-close-tabs-tooltip", {
+ tabCount,
+ });
+ } else if (tab._overPlayingIcon) {
+ 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);
+ } else if (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 {
+ 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 "activate":
+ // Intentional fallthrough
+ case "deactivate":
+ this.selectedTab.updateLastSeenActive();
+ break;
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ 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");
+
+ let l10nId = data.sameURI
+ ? "refresh-blocked-refresh-label"
+ : "refresh-blocked-redirect-label";
+ if (notification) {
+ notification.label = { "l10n-id": l10nId };
+ } else {
+ const buttons = [
+ {
+ "l10n-id": "refresh-blocked-allow",
+ callback() {
+ actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
+ },
+ },
+ ];
+
+ notificationBox.appendNotification(
+ "refresh-blocked",
+ {
+ label: { "l10n-id": l10nId },
+ image: "chrome://browser/skin/notification-icons/popup.svg",
+ priority: notificationBox.PRIORITY_INFO_MEDIUM,
+ },
+ buttons
+ );
+ }
+ },
+
+ _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;
+ },
+
+ destroy() {
+ this.tabContainer.destroy();
+ Services.obs.removeObserver(this, "contextual-identity-updated");
+
+ for (let tab of this.tabs) {
+ let browser = tab.linkedBrowser;
+ if (browser.registeredOpenURI) {
+ let userContextId = browser.getAttribute("usercontextid") || 0;
+ this.UrlbarProviderOpenTabs.unregisterOpenTab(
+ browser.registeredOpenURI.spec,
+ userContextId,
+ PrivateBrowsingUtils.isWindowPrivate(window)
+ );
+ delete browser.registeredOpenURI;
+ }
+
+ let filter = this._tabFilters.get(tab);
+ if (filter) {
+ browser.webProgress.removeProgressListener(filter);
+
+ let listener = this._tabListeners.get(tab);
+ if (listener) {
+ filter.removeProgressListener(listener);
+ listener.destroy();
+ }
+
+ this._tabFilters.delete(tab);
+ this._tabListeners.delete(tab);
+ }
+ }
+
+ Services.els.removeSystemEventListener(document, "keydown", this, false);
+ if (AppConstants.platform == "macosx") {
+ Services.els.removeSystemEventListener(
+ document,
+ "keypress",
+ this,
+ false
+ );
+ }
+ document.removeEventListener("visibilitychange", this);
+ window.removeEventListener("framefocusrequested", this);
+ window.removeEventListener("activate", this);
+ window.removeEventListener("deactivate", this);
+
+ if (gMultiProcessBrowser) {
+ if (this._switcher) {
+ this._switcher.destroy();
+ }
+ }
+ },
+
+ _setupEventListeners() {
+ this.tabpanels.addEventListener("select", event => {
+ if (event.target == this.tabpanels) {
+ this.updateCurrentBrowser();
+ }
+ });
+
+ 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 = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog"
+ )
+ ? this.getTabDialogBox(tabForEvent.linkedBrowser)
+ : this.getTabModalPromptBox(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?.wasPermitUnload ||
+ 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);
+
+ if (this.selectedBrowser == browser) {
+ TabCrashHandler.onSelectedBrowserCrash(
+ browser,
+ isRestartRequiredCrash
+ );
+ } else {
+ TabCrashHandler.onBackgroundBrowserCrash(
+ browser,
+ isRestartRequiredCrash
+ );
+ }
+
+ tab.removeAttribute("soundplaying");
+ this.setIcon(tab, icon);
+ };
+
+ this.addEventListener("oop-browser-crashed", onTabCrashed);
+ this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed);
+
+ this.addEventListener("DOMAudioPlaybackStarted", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ clearTimeout(tab._soundPlayingAttrRemovalTimer);
+ tab._soundPlayingAttrRemovalTimer = 0;
+
+ 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"
+ );
+
+ tab.style.setProperty(
+ "--soundplaying-removal-delay",
+ `${removalDelay - 300}ms`
+ );
+ tab.toggleAttribute("soundplaying-scheduledremoval", true);
+ this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
+
+ tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ tab.removeAttribute("soundplaying");
+ this._tabAttrModified(tab, [
+ "soundplaying",
+ "soundplaying-scheduledremoval",
+ ]);
+ }, removalDelay);
+ }
+ });
+
+ this.addEventListener("DOMAudioPlaybackBlockStarted", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ if (!tab.hasAttribute("activemedia-blocked")) {
+ tab.setAttribute("activemedia-blocked", true);
+ this._tabAttrModified(tab, ["activemedia-blocked"]);
+ }
+ });
+
+ this.addEventListener("DOMAudioPlaybackBlockStopped", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("activemedia-blocked")) {
+ tab.removeAttribute("activemedia-blocked");
+ this._tabAttrModified(tab, ["activemedia-blocked"]);
+ let hist = Services.telemetry.getHistogramById(
+ "TAB_AUDIO_INDICATOR_USED"
+ );
+ hist.add(2 /* unblockByVisitingTab */);
+ }
+ });
+
+ this.addEventListener("GloballyAutoplayBlocked", event => {
+ let browser = event.originalTarget;
+ let tab = this.getTabForBrowser(browser);
+ if (!tab) {
+ return;
+ }
+
+ SitePermissions.setForPrincipal(
+ browser.contentPrincipal,
+ "autoplay-media",
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_GLOBAL,
+ browser
+ );
+ });
+
+ let tabContextFTLInserter = () => {
+ this.translateTabContextMenu();
+ this.tabContainer.removeEventListener(
+ "contextmenu",
+ tabContextFTLInserter,
+ true
+ );
+ this.tabContainer.removeEventListener(
+ "mouseover",
+ tabContextFTLInserter
+ );
+ this.tabContainer.removeEventListener(
+ "focus",
+ tabContextFTLInserter,
+ true
+ );
+ };
+ this.tabContainer.addEventListener(
+ "contextmenu",
+ tabContextFTLInserter,
+ true
+ );
+ this.tabContainer.addEventListener("mouseover", tabContextFTLInserter);
+ this.tabContainer.addEventListener("focus", tabContextFTLInserter, true);
+
+ // 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);
+
+ let wasActive = document.activeElement == browser;
+
+ // 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 = didChangeEvent => {
+ browser.userTypedValue = oldUserTypedValue;
+ if (hadStartedLoad) {
+ browser.urlbarChangeTracker.startedLoad();
+ }
+
+ browser.droppedLinkHandler = oldDroppedLinkHandler;
+
+ // 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");
+ gBrowser.tabContainer.updateTabIndicatorAttr(tab);
+ }
+
+ if (wasActive) {
+ browser.focus();
+ }
+
+ if (this.isFindBarInitialized(tab)) {
+ this.getCachedFindBar(tab).browser = browser;
+ }
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+ };
+ browser.addEventListener("DidChangeBrowserRemoteness", didChange, {
+ once: true,
+ });
+ });
+
+ this.addEventListener("pageinfo", event => {
+ let browser = event.originalTarget;
+ let tab = this.getTabForBrowser(browser);
+ if (!tab) {
+ return;
+ }
+
+ const { url, description, previewImageURL } = event.detail;
+ this.setPageInfo(url, description, previewImageURL);
+ });
+ },
+
+ 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) {
+ throw new Error("Cannot set the successor of another window's tab");
+ }
+ if (successorTab == aTab) {
+ successorTab = null;
+ }
+ if (successorTab && successorTab.ownerGlobal != window) {
+ throw new 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;
+ }
+
+ this.mTab = aTab;
+ this.mBrowser = aBrowser;
+ this.mBlank = aStartsBlank;
+
+ // cache flags for correct status UI update after tab switching
+ this.mStateFlags = stateFlags;
+ this.mStatus = 0;
+ this.mMessage = "";
+ this.mTotalProgress = 0;
+
+ // count of open requests (should always be 0 or 1)
+ this.mRequestCount = aOrigRequestCount || 0;
+ }
+
+ destroy() {
+ delete this.mTab;
+ delete this.mBrowser;
+ }
+
+ _callProgressListeners(...args) {
+ args.unshift(this.mBrowser);
+ return gBrowser._callProgressListeners.apply(gBrowser, args);
+ }
+
+ _shouldShowProgress(aRequest) {
+ if (this.mBlank) {
+ return false;
+ }
+
+ // Don't show progress indicators in tabs for about: URIs
+ // pointing to local resources.
+ if (
+ aRequest instanceof Ci.nsIChannel &&
+ aRequest.originalURI.schemeIs("about")
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
+ if (!this.mBlank || !aWebProgress.isTopLevel) {
+ return false;
+ }
+
+ // 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
+ ) {
+ return true;
+ }
+
+ let location = aLocation ? aLocation.spec : "";
+ return location == "about:blank";
+ }
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ this.mTotalProgress = aMaxTotalProgress
+ ? aCurTotalProgress / aMaxTotalProgress
+ : 0;
+
+ if (!this._shouldShowProgress(aRequest)) {
+ return;
+ }
+
+ if (this.mTotalProgress && this.mTab.hasAttribute("busy")) {
+ this.mTab.setAttribute("progress", "true");
+ gBrowser._tabAttrModified(this.mTab, ["progress"]);
+ }
+
+ this._callProgressListeners("onProgressChange", [
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress,
+ ]);
+ }
+
+ onProgressChange64(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ return this.onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ );
+ }
+
+ /* eslint-disable complexity */
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (!aRequest) {
+ return;
+ }
+
+ let location, originalLocation;
+ try {
+ aRequest.QueryInterface(Ci.nsIChannel);
+ location = aRequest.URI;
+ originalLocation = aRequest.originalURI;
+ } catch (ex) {}
+
+ let ignoreBlank = this._isForInitialAboutBlank(
+ aWebProgress,
+ aStateFlags,
+ location
+ );
+
+ const { STATE_START, STATE_STOP, STATE_IS_NETWORK } =
+ Ci.nsIWebProgressListener;
+
+ // 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();
+ }
+ }
+ }
+ delete this.mBrowser.initialPageLoadedFromUserAction;
+ // If the browser is loading it must not be crashed anymore
+ this.mTab.removeAttribute("crashed");
+ gBrowser.tabContainer.updateTabIndicatorAttr(this.mTab);
+ }
+
+ if (this._shouldShowProgress(aRequest)) {
+ if (
+ !(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) &&
+ aWebProgress &&
+ aWebProgress.isTopLevel
+ ) {
+ this.mTab.setAttribute("busy", "true");
+ gBrowser._tabAttrModified(this.mTab, ["busy"]);
+ this.mTab._notselectedsinceload = !this.mTab.selected;
+ gBrowser.syncThrobberAnimations(this.mTab);
+ }
+
+ if (this.mTab.selected) {
+ gBrowser._isBusy = true;
+ }
+ }
+ } else if (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();
+ }
+ } else if (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");
+ }
+
+ // 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 (ignoreBlank) {
+ this._callProgressListeners(
+ "onUpdateCurrentBrowser",
+ [aStateFlags, aStatus, "", 0],
+ true,
+ false
+ );
+ } else {
+ this._callProgressListeners(
+ "onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ true,
+ false
+ );
+ }
+
+ this._callProgressListeners(
+ "onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ 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();
+ }
+
+ gBrowser
+ .getPanel(this.mBrowser)
+ .classList.toggle(
+ "newTabBrowserPanel",
+ BookmarkingUI.isOnNewTabPage(aLocation)
+ );
+
+ 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();
+ }
+ }
+
+ // Note that we're not updating for same-document loads, despite
+ // the `title` argument to `history.pushState/replaceState`. For
+ // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653
+ // and https://github.com/whatwg/html/issues/2174
+ if (!isReload) {
+ gBrowser.setTabTitle(this.mTab);
+ }
+
+ // 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();
+ }
+ }
+
+ if (
+ aRequest instanceof Ci.nsIChannel &&
+ !isBlankPageURL(aRequest.originalURI.spec)
+ ) {
+ this.mBrowser.originalURI = aRequest.originalURI;
+ }
+ }
+
+ 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)
+ );
+ delete this.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,
+ ]);
+ }
+ }
+
+ if (topLevel) {
+ this.mBrowser.lastURI = aLocation;
+ this.mBrowser.lastLocationChange = Date.now();
+ }
+ }
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (this.mBlank) {
+ return;
+ }
+
+ this._callProgressListeners("onStatusChange", [
+ aWebProgress,
+ aRequest,
+ aStatus,
+ aMessage,
+ ]);
+
+ this.mMessage = aMessage;
+ }
+
+ onSecurityChange(aWebProgress, aRequest, aState) {
+ this._callProgressListeners("onSecurityChange", [
+ aWebProgress,
+ aRequest,
+ aState,
+ ]);
+ }
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ this._callProgressListeners("onContentBlockingEvent", [
+ aWebProgress,
+ aRequest,
+ aEvent,
+ ]);
+ }
+
+ onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+ return this._callProgressListeners("onRefreshAttempted", [
+ aWebProgress,
+ aURI,
+ aDelay,
+ aSameURI,
+ ]);
+ }
+ }
+ TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsISupportsWeakReference",
+ ]);
+
+ let URILoadingWrapper = {
+ _normalizeLoadURIOptions(browser, loadURIOptions) {
+ if (!loadURIOptions.triggeringPrincipal) {
+ throw new Error("Must load with a triggering Principal");
+ }
+
+ if (
+ loadURIOptions.userContextId &&
+ loadURIOptions.userContextId != browser.getAttribute("usercontextid")
+ ) {
+ throw new Error("Cannot load with mismatched userContextId");
+ }
+
+ loadURIOptions.loadFlags |= loadURIOptions.flags | LOAD_FLAGS_NONE;
+ delete loadURIOptions.flags;
+ loadURIOptions.hasValidUserGestureActivation ??=
+ document.hasValidTransientUserGestureActivation;
+ },
+
+ _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;
+ },
+
+ _fixupURIString(browser, uriString, loadURIOptions) {
+ let fixupFlags = this._loadFlagsToFixupFlags(
+ browser,
+ loadURIOptions.loadFlags
+ );
+
+ // 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.
+ }
+ return null;
+ },
+
+ /**
+ * 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
+ );
+ });
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ _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) {}
+ }
+
+ if (globalHistoryOptions?.triggeringSearchEngine) {
+ browser.setAttribute(
+ "triggeringSearchEngine",
+ globalHistoryOptions.triggeringSearchEngine
+ );
+ browser.setAttribute("triggeringSearchEngineURL", uriString);
+ } else {
+ browser.removeAttribute("triggeringSearchEngine");
+ browser.removeAttribute("triggeringSearchEngineURL");
+ }
+ },
+
+ // 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;
+ }
+
+ this._updateTriggerMetadataForLoad(
+ browser,
+ uriString || uri.spec,
+ loadURIOptions
+ );
+
+ // 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 panel() {
+ delete this.panel;
+ this.panel = document.getElementById("statuspanel");
+ this.panel.addEventListener(
+ "transitionend",
+ this._onTransitionEnd.bind(this)
+ );
+ this.panel.addEventListener(
+ "transitioncancel",
+ this._onTransitionEnd.bind(this)
+ );
+ return this.panel;
+ },
+
+ 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);
+
+ this._label = text;
+ this._labelElement.setAttribute(
+ "crop",
+ type == "overLink" && !textCropped ? "center" : "end"
+ );
+ }
+ },
+
+ get _labelElement() {
+ delete this._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);
+
+ return {
+ top: panelRect.top,
+ bottom: panelRect.bottom,
+ left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left,
+ right: RTL_UI
+ ? containerRect.right
+ : containerRect.left + panelRect.width,
+ };
+ },
+
+ onMouseEnter() {
+ this._mirror();
+ },
+
+ onMouseLeave() {
+ this._mirror();
+ },
+
+ _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() {
+ let toolbar = document.getElementById("TabsToolbar");
+ let collapse = false;
+ if (
+ !gBrowser /* gBrowser isn't initialized yet */ ||
+ gBrowser.visibleTabs.length == 1
+ ) {
+ collapse = !window.toolbar.visible;
+ }
+
+ if (collapse == toolbar.collapsed && this._initialUpdateDone) {
+ return;
+ }
+ this._initialUpdateDone = true;
+
+ toolbar.collapsed = collapse;
+ let navbar = document.getElementById("nav-bar");
+ navbar.setAttribute("tabs-hidden", collapse);
+
+ document.getElementById("menu_closeWindow").hidden = collapse;
+ document.l10n.setAttributes(
+ document.getElementById("menu_close"),
+ collapse ? "tabbrowser-menuitem-close" : "tabbrowser-menuitem-close-tab"
+ );
+
+ TabsInTitlebar.allowedBy("tabs-visible", !collapse);
+ },
+};
+
+var TabContextMenu = {
+ contextTab: null,
+ _updateToggleMuteMenuItems(aTab, aConditionFn) {
+ ["muted", "soundplaying"].forEach(attr => {
+ if (!aConditionFn || aConditionFn(attr)) {
+ if (aTab.hasAttribute(attr)) {
+ aTab.toggleMuteMenuItem.setAttribute(attr, "true");
+ aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
+ } else {
+ aTab.toggleMuteMenuItem.removeAttribute(attr);
+ aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
+ }
+ }
+ });
+ },
+ updateContextMenu(aPopupMenu) {
+ let tab =
+ aPopupMenu.triggerNode &&
+ (aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab"));
+
+ this.contextTab = tab || gBrowser.selectedTab;
+ this.contextTab.addEventListener("TabAttrModified", this);
+ aPopupMenu.addEventListener("popuphiding", this);
+
+ let disabled = gBrowser.tabs.length == 1;
+ let multiselectionContext = this.contextTab.multiselected;
+ let tabCountInfo = JSON.stringify({
+ tabCount: (multiselectionContext && gBrowser.multiSelectedTabsCount) || 1,
+ });
+
+ var menuItems = aPopupMenu.getElementsByAttribute(
+ "tbattr",
+ "tabbrowser-multiple"
+ );
+ for (let menuItem of menuItems) {
+ menuItem.disabled = disabled;
+ }
+
+ disabled = gBrowser.visibleTabs.length == 1;
+ menuItems = aPopupMenu.getElementsByAttribute(
+ "tbattr",
+ "tabbrowser-multiple-visible"
+ );
+ for (let menuItem of menuItems) {
+ menuItem.disabled = disabled;
+ }
+
+ // Session store
+ document.getElementById("context_undoCloseTab").disabled =
+ SessionStore.getClosedTabCount() == 0;
+
+ // Show/hide fullscreen context menu items and set the
+ // autohide item's checked state to mirror the autohide pref.
+ showFullScreenViewContextMenuItems(aPopupMenu);
+
+ // Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
+ document.getElementById("context_reloadTab").hidden = multiselectionContext;
+ document.getElementById("context_reloadSelectedTabs").hidden =
+ !multiselectionContext;
+
+ // 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 contextTabIsSelected = this.contextTab.multiselected;
+ let visibleTabs = gBrowser.visibleTabs;
+ let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
+ let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
+ 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._numPinnedTabs];
+ contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
+
+ if (this.contextTab.hasAttribute("customizemode")) {
+ document.getElementById("context_openTabInWindow").disabled = true;
+ }
+
+ // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
+ document.getElementById("context_duplicateTab").hidden =
+ multiselectionContext;
+ document.getElementById("context_duplicateTabs").hidden =
+ !multiselectionContext;
+
+ // Disable "Close Tabs to the Left/Right" if there are no tabs
+ // preceding/following it.
+ let closeTabsToTheStartItem = document.getElementById(
+ "context_closeTabsToTheStart"
+ );
+ let noTabsToStart = !gBrowser.getTabsToTheStartFrom(this.contextTab).length;
+ closeTabsToTheStartItem.disabled = noTabsToStart;
+ let closeTabsToTheEndItem = document.getElementById(
+ "context_closeTabsToTheEnd"
+ );
+ let noTabsToEnd = !gBrowser.getTabsToTheEndFrom(this.contextTab).length;
+ closeTabsToTheEndItem.disabled = noTabsToEnd;
+
+ // Disable "Close other Tabs" if there are no unpinned tabs.
+ let unpinnedTabsToClose = multiselectionContext
+ ? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length
+ : gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned)
+ .length;
+ let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
+ closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;
+
+ // Update the close item with how many tabs will close.
+ document
+ .getElementById("context_closeTab")
+ .setAttribute("data-l10n-args", tabCountInfo);
+
+ // Disable "Close Multiple Tabs" if all sub menuitems are disabled
+ document.getElementById("context_closeTabOptions").disabled =
+ closeTabsToTheStartItem.disabled &&
+ closeTabsToTheEndItem.disabled &&
+ closeOtherTabsItem.disabled;
+
+ // Hide "Bookmark Tab…" for multiselection.
+ // Update its state if visible.
+ let bookmarkTab = document.getElementById("context_bookmarkTab");
+ bookmarkTab.hidden = 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;
+
+ const isMuted = this.contextTab.hasAttribute("muted");
+ document.l10n.setAttributes(
+ toggleMute,
+ isMuted ? "tabbrowser-context-unmute-tab" : "tabbrowser-context-mute-tab"
+ );
+ document.l10n.setAttributes(
+ toggleMultiSelectMute,
+ isMuted
+ ? "tabbrowser-context-unmute-selected-tabs"
+ : "tabbrowser-context-mute-selected-tabs"
+ );
+
+ this.contextTab.toggleMuteMenuItem = toggleMute;
+ this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
+ this._updateToggleMuteMenuItems(this.contextTab);
+
+ let selectAllTabs = document.getElementById("context_selectAllTabs");
+ selectAllTabs.disabled = gBrowser.allTabsSelected();
+
+ gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
+
+ let reopenInContainer = document.getElementById(
+ "context_reopenInContainer"
+ );
+ reopenInContainer.hidden =
+ !Services.prefs.getBoolPref("privacy.userContext.enabled", false) ||
+ PrivateBrowsingUtils.isWindowPrivate(window);
+ reopenInContainer.disabled = this.contextTab.hidden;
+
+ gShareUtils.updateShareURLMenuItem(
+ this.contextTab.linkedBrowser,
+ document.getElementById("context_sendTabToDevice")
+ );
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "popuphiding":
+ if (aEvent.target.id == "tabContextMenu") {
+ this.contextTab.removeEventListener("TabAttrModified", this);
+ }
+ break;
+ case "TabAttrModified":
+ let tab = aEvent.target;
+ this._updateToggleMuteMenuItems(tab, attr =>
+ aEvent.detail.changed.includes(attr)
+ );
+ break;
+ }
+ },
+
+ 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 });
+ } else if (triggeringPrincipal.isContentPrincipal) {
+ triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
+ triggeringPrincipal,
+ {
+ userContextId,
+ }
+ );
+ }
+
+ let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, {
+ userContextId,
+ pinned: tab.pinned,
+ index: tab._tPos + 1,
+ triggeringPrincipal,
+ });
+
+ if (gBrowser.selectedTab == tab) {
+ gBrowser.selectedTab = newTab;
+ }
+ if (tab.muted && !newTab.muted) {
+ newTab.toggleMuteAudio(tab.muteReason);
+ }
+ }
+ },
+
+ closeContextTabs(event) {
+ if (this.contextTab.multiselected) {
+ gBrowser.removeMultiSelectedTabs();
+ } else {
+ gBrowser.removeTab(this.contextTab, { animate: true });
+ }
+ },
+};