tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

tabbrowser.js (341534B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 {
      7  // start private scope for Tabbrowser
      8  /**
      9   * A set of known icons to use for internal pages. These are hardcoded so we can
     10   * start loading them faster than FaviconLoader would normally find them.
     11   */
     12  const FAVICON_DEFAULTS = {
     13    "about:newtab": "chrome://branding/content/icon32.png",
     14    "about:home": "chrome://branding/content/icon32.png",
     15    "about:welcome": "chrome://branding/content/icon32.png",
     16    "about:privatebrowsing":
     17      "chrome://browser/skin/privatebrowsing/favicon.svg",
     18  };
     19 
     20  const {
     21    LOAD_FLAGS_NONE,
     22    LOAD_FLAGS_FROM_EXTERNAL,
     23    LOAD_FLAGS_FIRST_LOAD,
     24    LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL,
     25    LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
     26    LOAD_FLAGS_FIXUP_SCHEME_TYPOS,
     27    LOAD_FLAGS_FORCE_ALLOW_DATA_URI,
     28    LOAD_FLAGS_DISABLE_TRR,
     29  } = Ci.nsIWebNavigation;
     30 
     31  const DIRECTION_FORWARD = 1;
     32  const DIRECTION_BACKWARD = -1;
     33 
     34  /**
     35   * Updates the User Context UI indicators if the browser is in a non-default context
     36   */
     37  function updateUserContextUIIndicator() {
     38    function replaceContainerClass(classType, element, value) {
     39      let prefix = "identity-" + classType + "-";
     40      if (value && element.classList.contains(prefix + value)) {
     41        return;
     42      }
     43      for (let className of element.classList) {
     44        if (className.startsWith(prefix)) {
     45          element.classList.remove(className);
     46        }
     47      }
     48      if (value) {
     49        element.classList.add(prefix + value);
     50      }
     51    }
     52 
     53    let hbox = document.getElementById("userContext-icons");
     54 
     55    let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
     56    if (!userContextId) {
     57      replaceContainerClass("color", hbox, "");
     58      hbox.hidden = true;
     59      return;
     60    }
     61 
     62    let identity =
     63      ContextualIdentityService.getPublicIdentityFromId(userContextId);
     64    if (!identity) {
     65      replaceContainerClass("color", hbox, "");
     66      hbox.hidden = true;
     67      return;
     68    }
     69 
     70    replaceContainerClass("color", hbox, identity.color);
     71 
     72    let label = ContextualIdentityService.getUserContextLabel(userContextId);
     73    document.getElementById("userContext-label").textContent = label;
     74    // Also set the container label as the tooltip so we can only show the icon
     75    // in small windows.
     76    hbox.setAttribute("tooltiptext", label);
     77 
     78    let indicator = document.getElementById("userContext-indicator");
     79    replaceContainerClass("icon", indicator, identity.icon);
     80 
     81    hbox.hidden = false;
     82  }
     83 
     84  async function getTotalMemoryUsage() {
     85    const procInfo = await ChromeUtils.requestProcInfo();
     86    let totalMemoryUsage = procInfo.memory;
     87    for (const child of procInfo.children) {
     88      totalMemoryUsage += child.memory;
     89    }
     90    return totalMemoryUsage;
     91  }
     92 
     93  window.Tabbrowser = class {
     94    init() {
     95      this.tabContainer = document.getElementById("tabbrowser-tabs");
     96      this.tabGroupMenu = document.getElementById("tab-group-editor");
     97      this.tabNoteMenu = document.getElementById("tab-note-menu");
     98      this.tabbox = document.getElementById("tabbrowser-tabbox");
     99      this.tabpanels = document.getElementById("tabbrowser-tabpanels");
    100      this.pinnedTabsContainer = document.getElementById(
    101        "pinned-tabs-container"
    102      );
    103      this.splitViewCommandSet = document.getElementById("splitViewCommands");
    104 
    105      ChromeUtils.defineESModuleGetters(this, {
    106        AsyncTabSwitcher:
    107          "moz-src:///browser/components/tabbrowser/AsyncTabSwitcher.sys.mjs",
    108        PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
    109        // SmartTabGrouping.sys.mjs is missing. tor-browser#44045.
    110        // Unused in this context. See mozilla bug 1981785.
    111        TabMetrics:
    112          "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
    113        TabStateFlusher:
    114          "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
    115        TaskbarTabsUtils:
    116          "resource:///modules/taskbartabs/TaskbarTabsUtils.sys.mjs",
    117        TaskbarTabs: "resource:///modules/taskbartabs/TaskbarTabs.sys.mjs",
    118        UrlbarProviderOpenTabs:
    119          "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
    120        FaviconUtils: "moz-src:///browser/modules/FaviconUtils.sys.mjs",
    121      });
    122      ChromeUtils.defineLazyGetter(this, "tabLocalization", () => {
    123        return new Localization(
    124          [
    125            "browser/tabbrowser.ftl",
    126            "browser/taskbartabs.ftl",
    127            "branding/brand.ftl",
    128          ],
    129          true
    130        );
    131      });
    132      XPCOMUtils.defineLazyPreferenceGetter(
    133        this,
    134        "_shouldExposeContentTitle",
    135        "privacy.exposeContentTitleInWindow",
    136        true
    137      );
    138      XPCOMUtils.defineLazyPreferenceGetter(
    139        this,
    140        "_shouldExposeContentTitlePbm",
    141        "privacy.exposeContentTitleInWindow.pbm",
    142        true
    143      );
    144      XPCOMUtils.defineLazyPreferenceGetter(
    145        this,
    146        "_showTabCardPreview",
    147        "browser.tabs.hoverPreview.enabled",
    148        true
    149      );
    150      XPCOMUtils.defineLazyPreferenceGetter(
    151        this,
    152        "_allowTransparentBrowser",
    153        "browser.tabs.allow_transparent_browser",
    154        false
    155      );
    156      XPCOMUtils.defineLazyPreferenceGetter(
    157        this,
    158        "_tabGroupsEnabled",
    159        "browser.tabs.groups.enabled",
    160        false
    161      );
    162      XPCOMUtils.defineLazyPreferenceGetter(
    163        this,
    164        "_tabNotesEnabled",
    165        "browser.tabs.notes.enabled",
    166        false
    167      );
    168      XPCOMUtils.defineLazyPreferenceGetter(
    169        this,
    170        "showPidAndActiveness",
    171        "browser.tabs.tooltipsShowPidAndActiveness",
    172        false
    173      );
    174      XPCOMUtils.defineLazyPreferenceGetter(
    175        this,
    176        "_unloadTabInContextMenu",
    177        "browser.tabs.unloadTabInContextMenu",
    178        false
    179      );
    180      XPCOMUtils.defineLazyPreferenceGetter(
    181        this,
    182        "_notificationEnableDelay",
    183        "security.notification_enable_delay",
    184        500
    185      );
    186      XPCOMUtils.defineLazyPreferenceGetter(
    187        this,
    188        "_remoteSVGIconDecoding",
    189        "browser.tabs.remoteSVGIconDecoding",
    190        false
    191      );
    192 
    193      if (AppConstants.MOZ_CRASHREPORTER) {
    194        ChromeUtils.defineESModuleGetters(this, {
    195          TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
    196        });
    197      }
    198 
    199      Services.obs.addObserver(this, "contextual-identity-updated");
    200      Services.obs.addObserver(this, "intl:app-locales-changed");
    201 
    202      document.addEventListener("keydown", this, { mozSystemGroup: true });
    203      document.addEventListener("keypress", this, { mozSystemGroup: true });
    204      document.addEventListener("visibilitychange", this);
    205      window.addEventListener("framefocusrequested", this);
    206      window.addEventListener("activate", this);
    207      window.addEventListener("deactivate", this);
    208      window.addEventListener("TabGroupCollapse", this);
    209      window.addEventListener("TabGroupCreateByUser", this);
    210      window.addEventListener("TabGrouped", this);
    211      window.addEventListener("TabUngrouped", this);
    212      window.addEventListener("TabSplitViewActivate", this);
    213      window.addEventListener("TabSplitViewDeactivate", this);
    214 
    215      this.tabContainer.init();
    216      this._setupInitialBrowserAndTab();
    217 
    218      if (
    219        Services.prefs.getIntPref("browser.display.document_color_use") == 2
    220      ) {
    221        this.tabpanels.style.backgroundColor = Services.prefs.getCharPref(
    222          "browser.display.background_color"
    223        );
    224      }
    225 
    226      this._setFindbarData();
    227 
    228      // We take over setting the document title, so remove the l10n id to
    229      // avoid it being re-translated and overwriting document content if
    230      // we ever switch languages at runtime.
    231      document.querySelector("title").removeAttribute("data-l10n-id");
    232 
    233      this._setupEventListeners();
    234      this._initialized = true;
    235    }
    236 
    237    ownerGlobal = window;
    238 
    239    ownerDocument = document;
    240 
    241    closingTabsEnum = {
    242      ALL: 0,
    243      OTHER: 1,
    244      TO_START: 2,
    245      TO_END: 3,
    246      MULTI_SELECTED: 4,
    247      DUPLICATES: 6,
    248      ALL_DUPLICATES: 7,
    249    };
    250 
    251    _lastRelatedTabMap = new WeakMap();
    252 
    253    mProgressListeners = [];
    254 
    255    mTabsProgressListeners = [];
    256 
    257    _tabListeners = new Map();
    258 
    259    _tabFilters = new Map();
    260 
    261    _isBusy = false;
    262 
    263    _awaitingToggleCaretBrowsingPrompt = false;
    264 
    265    _previewMode = false;
    266 
    267    _lastFindValue = "";
    268 
    269    _contentWaitingCount = 0;
    270 
    271    _tabLayerCache = [];
    272 
    273    tabAnimationsInProgress = 0;
    274 
    275    /**
    276     * Binding from browser to tab
    277     */
    278    _tabForBrowser = new WeakMap();
    279 
    280    /**
    281     * `_createLazyBrowser` will define properties on the unbound lazy browser
    282     * which correspond to properties defined in MozBrowser which will be bound to
    283     * the browser when it is inserted into the document.  If any of these
    284     * properties are accessed by consumers, `_insertBrowser` is called and
    285     * the browser is inserted to ensure that things don't break.  This list
    286     * provides the names of properties that may be called while the browser
    287     * is in its unbound (lazy) state.
    288     */
    289    _browserBindingProperties = [
    290      "canGoBack",
    291      "canGoForward",
    292      "goBack",
    293      "goForward",
    294      "permitUnload",
    295      "reload",
    296      "reloadWithFlags",
    297      "stop",
    298      "loadURI",
    299      "fixupAndLoadURIString",
    300      "gotoIndex",
    301      "currentURI",
    302      "documentURI",
    303      "remoteType",
    304      "preferences",
    305      "imageDocument",
    306      "isRemoteBrowser",
    307      "messageManager",
    308      "getTabBrowser",
    309      "finder",
    310      "fastFind",
    311      "sessionHistory",
    312      "contentTitle",
    313      "characterSet",
    314      "fullZoom",
    315      "textZoom",
    316      "tabHasCustomZoom",
    317      "webProgress",
    318      "addProgressListener",
    319      "removeProgressListener",
    320      "audioPlaybackStarted",
    321      "audioPlaybackStopped",
    322      "resumeMedia",
    323      "mute",
    324      "unmute",
    325      "blockedPopups",
    326      "lastURI",
    327      "purgeSessionHistory",
    328      "stopScroll",
    329      "startScroll",
    330      "userTypedValue",
    331      "userTypedClear",
    332      "didStartLoadSinceLastUserTyping",
    333      "audioMuted",
    334    ];
    335 
    336    _removingTabs = new Set();
    337 
    338    _multiSelectedTabsSet = new WeakSet();
    339 
    340    _lastMultiSelectedTabRef = null;
    341 
    342    _clearMultiSelectionLocked = false;
    343 
    344    _clearMultiSelectionLockedOnce = false;
    345 
    346    _multiSelectChangeStarted = false;
    347 
    348    _multiSelectChangeAdditions = new Set();
    349 
    350    _multiSelectChangeRemovals = new Set();
    351 
    352    _multiSelectChangeSelected = false;
    353 
    354    /**
    355     * Tab close requests are ignored if the window is closing anyway,
    356     * e.g. when holding Ctrl+W.
    357     */
    358    _windowIsClosing = false;
    359 
    360    preloadedBrowser = null;
    361 
    362    /**
    363     * This defines a proxy which allows us to access browsers by
    364     * index without actually creating a full array of browsers.
    365     */
    366    browsers = new Proxy([], {
    367      has: (target, name) => {
    368        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
    369          return name in gBrowser.tabs;
    370        }
    371        return false;
    372      },
    373      get: (target, name) => {
    374        if (name == "length") {
    375          return gBrowser.tabs.length;
    376        }
    377        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
    378          if (!(name in gBrowser.tabs)) {
    379            return undefined;
    380          }
    381          return gBrowser.tabs[name].linkedBrowser;
    382        }
    383        return target[name];
    384      },
    385    });
    386 
    387    /**
    388     * List of browsers whose docshells must be active in order for print preview
    389     * to work.
    390     */
    391    _printPreviewBrowsers = new Set();
    392 
    393    /** @type {MozTabSplitViewWrapper} */
    394    #activeSplitView = null;
    395 
    396    get activeSplitView() {
    397      return this.#activeSplitView;
    398    }
    399 
    400    /**
    401     * List of browsers which are currently in an active Split View.
    402     *
    403     * @type {MozBrowser[]}
    404     */
    405    get splitViewBrowsers() {
    406      const browsers = [];
    407      if (this.#activeSplitView) {
    408        for (const tab of this.#activeSplitView.tabs) {
    409          browsers.push(tab.linkedBrowser);
    410        }
    411      }
    412      return browsers;
    413    }
    414 
    415    _switcher = null;
    416 
    417    _soundPlayingAttrRemovalTimer = 0;
    418 
    419    _hoverTabTimer = null;
    420 
    421    get tabs() {
    422      return this.tabContainer.allTabs;
    423    }
    424 
    425    get tabGroups() {
    426      return this.tabContainer.allGroups;
    427    }
    428 
    429    get tabsInCollapsedTabGroups() {
    430      return this.tabGroups
    431        .filter(tabGroup => tabGroup.collapsed)
    432        .flatMap(tabGroup => tabGroup.tabs)
    433        .filter(tab => !tab.hidden && !tab.closing);
    434    }
    435 
    436    addEventListener(...args) {
    437      this.tabpanels.addEventListener(...args);
    438    }
    439 
    440    removeEventListener(...args) {
    441      this.tabpanels.removeEventListener(...args);
    442    }
    443 
    444    dispatchEvent(...args) {
    445      return this.tabpanels.dispatchEvent(...args);
    446    }
    447 
    448    /**
    449     * Returns all tabs in the current window, including hidden tabs and tabs
    450     * in collapsed groups, but excluding closing tabs and the Firefox View tab.
    451     */
    452    get openTabs() {
    453      return this.tabContainer.openTabs;
    454    }
    455 
    456    /**
    457     * Same as `openTabs` but excluding hidden tabs.
    458     */
    459    get nonHiddenTabs() {
    460      return this.tabContainer.nonHiddenTabs;
    461    }
    462 
    463    /**
    464     * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups.
    465     */
    466    get visibleTabs() {
    467      return this.tabContainer.visibleTabs;
    468    }
    469 
    470    get pinnedTabCount() {
    471      for (var i = 0; i < this.tabs.length; i++) {
    472        if (!this.tabs[i].pinned) {
    473          break;
    474        }
    475      }
    476      return i;
    477    }
    478 
    479    set selectedTab(val) {
    480      if (
    481        gSharedTabWarning.willShowSharedTabWarning(val) ||
    482        document.documentElement.hasAttribute("window-modal-open") ||
    483        (gNavToolbox.collapsed && !this._allowTabChange)
    484      ) {
    485        return;
    486      }
    487      // Update the tab
    488      this.tabbox.selectedTab = val;
    489    }
    490 
    491    get selectedTab() {
    492      return this._selectedTab;
    493    }
    494 
    495    get selectedBrowser() {
    496      return this._selectedBrowser;
    497    }
    498 
    499    get selectedBrowsers() {
    500      const splitViewBrowsers = this.splitViewBrowsers;
    501      return splitViewBrowsers.length
    502        ? splitViewBrowsers
    503        : [this._selectedBrowser];
    504    }
    505 
    506    _setupInitialBrowserAndTab() {
    507      // See browser.js for the meaning of window.arguments.
    508      // Bug 1485961 covers making this more sane.
    509      let userContextId = window.arguments && window.arguments[5];
    510 
    511      let openWindowInfo = window.docShell.treeOwner
    512        .QueryInterface(Ci.nsIInterfaceRequestor)
    513        .getInterface(Ci.nsIAppWindow).initialOpenWindowInfo;
    514 
    515      if (!openWindowInfo && window.arguments && window.arguments[11]) {
    516        openWindowInfo = window.arguments[11];
    517      }
    518 
    519      let extraOptions;
    520      if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) {
    521        extraOptions = window.arguments[1];
    522      }
    523 
    524      // If our opener provided a remoteType which was responsible for creating
    525      // this pop-up window, we'll fall back to using that remote type when no
    526      // other remote type is available.
    527      let triggeringRemoteType;
    528      if (extraOptions?.hasKey("triggeringRemoteType")) {
    529        triggeringRemoteType = extraOptions.getPropertyAsACString(
    530          "triggeringRemoteType"
    531        );
    532      }
    533 
    534      let tabArgument = gBrowserInit.getTabToAdopt();
    535 
    536      // If we have a tab argument with browser, we use its remoteType. Otherwise,
    537      // if e10s is disabled or there's a parent process opener (e.g. parent
    538      // process about: page) for the content tab, we use a parent
    539      // process remoteType. Otherwise, we check the URI to determine
    540      // what to do - if there isn't one, we default to the default remote type.
    541      //
    542      // When adopting a tab, we'll also use that tab's browsingContextGroupId,
    543      // if available, to ensure we don't spawn a new process.
    544      let remoteType;
    545      let initialBrowsingContextGroupId;
    546 
    547      if (tabArgument && tabArgument.hasAttribute("usercontextid")) {
    548        // The window's first argument is a tab if and only if we are swapping tabs.
    549        // We must set the browser's usercontextid so that the newly created remote
    550        // tab child has the correct usercontextid.
    551        userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
    552      }
    553 
    554      if (tabArgument && tabArgument.linkedBrowser) {
    555        remoteType = tabArgument.linkedBrowser.remoteType;
    556        initialBrowsingContextGroupId =
    557          tabArgument.linkedBrowser.browsingContext?.group.id;
    558      } else if (openWindowInfo) {
    559        userContextId = openWindowInfo.originAttributes.userContextId;
    560        if (openWindowInfo.isRemote) {
    561          remoteType = triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE;
    562        } else {
    563          remoteType = E10SUtils.NOT_REMOTE;
    564        }
    565      } else {
    566        let uriToLoad = gBrowserInit.uriToLoadPromise;
    567        if (uriToLoad && Array.isArray(uriToLoad)) {
    568          uriToLoad = uriToLoad[0]; // we only care about the first item
    569        }
    570 
    571        if (uriToLoad && typeof uriToLoad == "string") {
    572          let oa = E10SUtils.predictOriginAttributes({
    573            window,
    574            userContextId,
    575          });
    576          remoteType = E10SUtils.getRemoteTypeForURI(
    577            uriToLoad,
    578            gMultiProcessBrowser,
    579            gFissionBrowser,
    580            triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE,
    581            null,
    582            oa
    583          );
    584        } else {
    585          // If we reach here, we don't have the url to load. This means that
    586          // `uriToLoad` is most likely a promise which is waiting on SessionStore
    587          // initialization. We can't delay setting up the browser here, as that
    588          // would mean that `gBrowser.selectedBrowser` might not always exist,
    589          // which is the current assumption.
    590 
    591          if (Cu.isInAutomation) {
    592            ChromeUtils.releaseAssert(
    593              !triggeringRemoteType,
    594              "Unexpected triggeringRemoteType with no uriToLoad"
    595            );
    596          }
    597 
    598          // In this case we default to the privileged about process as that's
    599          // the best guess we can make, and we'll likely need it eventually.
    600          remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
    601        }
    602      }
    603 
    604      let createOptions = {
    605        uriIsAboutBlank: false,
    606        userContextId,
    607        initialBrowsingContextGroupId,
    608        remoteType,
    609        openWindowInfo,
    610      };
    611      let browser = this.createBrowser(createOptions);
    612      browser.setAttribute("primary", "true");
    613      if (gBrowserAllowScriptsToCloseInitialTabs) {
    614        browser.setAttribute("allowscriptstoclose", "true");
    615      }
    616      browser.droppedLinkHandler = handleDroppedLink;
    617      browser.loadURI = URILoadingWrapper.loadURI.bind(
    618        URILoadingWrapper,
    619        browser
    620      );
    621      browser.fixupAndLoadURIString =
    622        URILoadingWrapper.fixupAndLoadURIString.bind(
    623          URILoadingWrapper,
    624          browser
    625        );
    626 
    627      if (AIWindow.isAIWindowActive(window)) {
    628        let uriToLoad = gBrowserInit.uriToLoadPromise;
    629        let firstURI = Array.isArray(uriToLoad) ? uriToLoad[0] : uriToLoad;
    630 
    631        if (!this._allowTransparentBrowser) {
    632          browser.toggleAttribute(
    633            "transparent",
    634            !firstURI ||
    635              AIWindow.isAIWindowContentPage(Services.io.newURI(firstURI))
    636          );
    637        }
    638      }
    639 
    640      let uniqueId = this._generateUniquePanelID();
    641      let panel = this.getPanel(browser);
    642      panel.id = uniqueId;
    643      this.tabpanels.appendChild(panel);
    644 
    645      let tab = this.tabs[0];
    646      tab.linkedPanel = uniqueId;
    647      this._selectedTab = tab;
    648      this._selectedBrowser = browser;
    649      tab.permanentKey = browser.permanentKey;
    650      tab._tPos = 0;
    651      tab._fullyOpen = true;
    652      tab.linkedBrowser = browser;
    653 
    654      if (userContextId) {
    655        tab.setAttribute("usercontextid", userContextId);
    656        ContextualIdentityService.setTabStyle(tab);
    657      }
    658 
    659      this._tabForBrowser.set(browser, tab);
    660 
    661      this.appendStatusPanel();
    662 
    663      // This is the initial browser, so it's usually active; the default is false
    664      // so we have to update it:
    665      browser.docShellIsActive = this.shouldActivateDocShell(browser);
    666 
    667      // Hook the browser up with a progress listener.
    668      let tabListener = new TabProgressListener(tab, browser, true, false);
    669      let filter = Cc[
    670        "@mozilla.org/appshell/component/browser-status-filter;1"
    671      ].createInstance(Ci.nsIWebProgress);
    672      filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
    673      this._tabListeners.set(tab, tabListener);
    674      this._tabFilters.set(tab, filter);
    675      browser.webProgress.addProgressListener(
    676        filter,
    677        Ci.nsIWebProgress.NOTIFY_ALL
    678      );
    679    }
    680 
    681    /**
    682     * BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
    683     * MAKE SURE TO ADD IT HERE AS WELL.
    684     */
    685    get canGoBack() {
    686      return this.selectedBrowser.canGoBack;
    687    }
    688 
    689    get canGoBackIgnoringUserInteraction() {
    690      return this.selectedBrowser.canGoBackIgnoringUserInteraction;
    691    }
    692 
    693    get canGoForward() {
    694      return this.selectedBrowser.canGoForward;
    695    }
    696 
    697    goBack(requireUserInteraction) {
    698      return this.selectedBrowser.goBack(requireUserInteraction);
    699    }
    700 
    701    goForward(requireUserInteraction) {
    702      return this.selectedBrowser.goForward(requireUserInteraction);
    703    }
    704 
    705    reload() {
    706      return this.selectedBrowser.reload();
    707    }
    708 
    709    reloadWithFlags(aFlags) {
    710      return this.selectedBrowser.reloadWithFlags(aFlags);
    711    }
    712 
    713    stop() {
    714      return this.selectedBrowser.stop();
    715    }
    716 
    717    /**
    718     * throws exception for unknown schemes
    719     */
    720    loadURI(uri, params) {
    721      return this.selectedBrowser.loadURI(uri, params);
    722    }
    723    /**
    724     * throws exception for unknown schemes
    725     */
    726    fixupAndLoadURIString(uriString, params) {
    727      return this.selectedBrowser.fixupAndLoadURIString(uriString, params);
    728    }
    729 
    730    gotoIndex(aIndex) {
    731      return this.selectedBrowser.gotoIndex(aIndex);
    732    }
    733 
    734    get currentURI() {
    735      return this.selectedBrowser.currentURI;
    736    }
    737 
    738    get finder() {
    739      return this.selectedBrowser.finder;
    740    }
    741 
    742    get docShell() {
    743      return this.selectedBrowser.docShell;
    744    }
    745 
    746    get webNavigation() {
    747      return this.selectedBrowser.webNavigation;
    748    }
    749 
    750    get webProgress() {
    751      return this.selectedBrowser.webProgress;
    752    }
    753 
    754    get contentWindow() {
    755      return this.selectedBrowser.contentWindow;
    756    }
    757 
    758    get sessionHistory() {
    759      return this.selectedBrowser.sessionHistory;
    760    }
    761 
    762    get contentDocument() {
    763      return this.selectedBrowser.contentDocument;
    764    }
    765 
    766    get contentTitle() {
    767      return this.selectedBrowser.contentTitle;
    768    }
    769 
    770    get contentPrincipal() {
    771      return this.selectedBrowser.contentPrincipal;
    772    }
    773 
    774    get securityUI() {
    775      return this.selectedBrowser.securityUI;
    776    }
    777 
    778    set fullZoom(val) {
    779      this.selectedBrowser.fullZoom = val;
    780    }
    781 
    782    get fullZoom() {
    783      return this.selectedBrowser.fullZoom;
    784    }
    785 
    786    set textZoom(val) {
    787      this.selectedBrowser.textZoom = val;
    788    }
    789 
    790    get textZoom() {
    791      return this.selectedBrowser.textZoom;
    792    }
    793 
    794    get isSyntheticDocument() {
    795      return this.selectedBrowser.isSyntheticDocument;
    796    }
    797 
    798    set userTypedValue(val) {
    799      this.selectedBrowser.userTypedValue = val;
    800    }
    801 
    802    get userTypedValue() {
    803      return this.selectedBrowser.userTypedValue;
    804    }
    805 
    806    _setFindbarData() {
    807      // Ensure we know what the find bar key is in the content process:
    808      let { sharedData } = Services.ppmm;
    809      if (!sharedData.has("Findbar:Shortcut")) {
    810        let keyEl = document.getElementById("key_find");
    811        let mods = keyEl
    812          .getAttribute("modifiers")
    813          .replace(
    814            /accel/i,
    815            AppConstants.platform == "macosx" ? "meta" : "control"
    816          );
    817        sharedData.set("Findbar:Shortcut", {
    818          key: keyEl.getAttribute("key"),
    819          shiftKey: mods.includes("shift"),
    820          ctrlKey: mods.includes("control"),
    821          altKey: mods.includes("alt"),
    822          metaKey: mods.includes("meta"),
    823        });
    824      }
    825    }
    826 
    827    isFindBarInitialized(aTab) {
    828      return (aTab || this.selectedTab)._findBar != undefined;
    829    }
    830 
    831    /**
    832     * Get the already constructed findbar
    833     */
    834    getCachedFindBar(aTab = this.selectedTab) {
    835      return aTab._findBar;
    836    }
    837 
    838    /**
    839     * Get the findbar, and create it if it doesn't exist.
    840     *
    841     * @return the find bar (or null if the window or tab is closed/closing in the interim).
    842     */
    843    async getFindBar(aTab = this.selectedTab) {
    844      let findBar = this.getCachedFindBar(aTab);
    845      if (findBar) {
    846        return findBar;
    847      }
    848 
    849      // Avoid re-entrancy by caching the promise we're about to return.
    850      if (!aTab._pendingFindBar) {
    851        aTab._pendingFindBar = this._createFindBar(aTab);
    852      }
    853      return aTab._pendingFindBar;
    854    }
    855 
    856    /**
    857     * Create a findbar instance.
    858     *
    859     * @param aTab the tab to create the find bar for.
    860     * @return the created findbar, or null if the window or tab is closed/closing.
    861     */
    862    async _createFindBar(aTab) {
    863      let findBar = document.createXULElement("findbar");
    864      let browser = this.getBrowserForTab(aTab);
    865 
    866      browser.parentNode.insertAdjacentElement("afterend", findBar);
    867 
    868      await new Promise(r => requestAnimationFrame(r));
    869      delete aTab._pendingFindBar;
    870      if (window.closed || aTab.closing) {
    871        return null;
    872      }
    873 
    874      findBar.browser = browser;
    875      findBar._findField.value = this._lastFindValue;
    876 
    877      aTab._findBar = findBar;
    878 
    879      let event = document.createEvent("Events");
    880      event.initEvent("TabFindInitialized", true, false);
    881      aTab.dispatchEvent(event);
    882 
    883      return findBar;
    884    }
    885 
    886    appendStatusPanel(browser = this.selectedBrowser) {
    887      browser.insertAdjacentElement("afterend", StatusPanel.panel);
    888    }
    889 
    890    _updateTabBarForPinnedTabs() {
    891      this.tabContainer._unlockTabSizing();
    892      this.tabContainer._handleTabSelect(true);
    893      this.tabContainer._updateCloseButtons();
    894    }
    895 
    896    #notifyPinnedStatus(
    897      aTab,
    898      { telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
    899    ) {
    900      // browsingContext is expected to not be defined on discarded tabs.
    901      if (aTab.linkedBrowser.browsingContext) {
    902        aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned;
    903      }
    904 
    905      let event = new CustomEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", {
    906        bubbles: true,
    907        cancelable: false,
    908        detail: { telemetrySource },
    909      });
    910      aTab.dispatchEvent(event);
    911    }
    912 
    913    /**
    914     * Pin a tab.
    915     *
    916     * @param {MozTabbrowserTab} aTab
    917     *   The tab to pin.
    918     * @param {object} [options]
    919     * @property {string} [options.telemetrySource="unknown"]
    920     *   The means by which the tab was pinned.
    921     *   @see TabMetrics.METRIC_SOURCE for possible values.
    922     *   Defaults to "unknown".
    923     */
    924    pinTab(
    925      aTab,
    926      { telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
    927    ) {
    928      if (aTab.pinned || aTab == FirefoxViewHandler.tab) {
    929        return;
    930      }
    931 
    932      this.showTab(aTab);
    933      this.#handleTabMove(aTab, () => {
    934        let periphery = document.getElementById(
    935          "pinned-tabs-container-periphery"
    936        );
    937        // If periphery is null, append to end
    938        this.pinnedTabsContainer.insertBefore(aTab, periphery);
    939      });
    940 
    941      aTab.setAttribute("pinned", "true");
    942      this._updateTabBarForPinnedTabs();
    943      this.#notifyPinnedStatus(aTab, { telemetrySource });
    944    }
    945 
    946    unpinTab(aTab) {
    947      if (!aTab.pinned) {
    948        return;
    949      }
    950 
    951      this.#handleTabMove(aTab, () => {
    952        // we remove this attribute first, so that allTabs represents
    953        // the moving of a tab from the pinned tabs container
    954        // and back into arrowscrollbox.
    955        aTab.removeAttribute("pinned");
    956        this.tabContainer.arrowScrollbox.prepend(aTab);
    957      });
    958 
    959      aTab.style.marginInlineStart = "";
    960      aTab._pinnedUnscrollable = false;
    961      this._updateTabBarForPinnedTabs();
    962      this.#notifyPinnedStatus(aTab);
    963    }
    964 
    965    previewTab(aTab, aCallback) {
    966      let currentTab = this.selectedTab;
    967      try {
    968        // Suppress focus, ownership and selected tab changes
    969        this._previewMode = true;
    970        this.selectedTab = aTab;
    971        aCallback();
    972      } finally {
    973        this.selectedTab = currentTab;
    974        this._previewMode = false;
    975      }
    976    }
    977 
    978    getBrowserAtIndex(aIndex) {
    979      return this.browsers[aIndex];
    980    }
    981 
    982    getBrowserForOuterWindowID(aID) {
    983      for (let b of this.browsers) {
    984        if (b.outerWindowID == aID) {
    985          return b;
    986        }
    987      }
    988 
    989      return null;
    990    }
    991 
    992    getTabForBrowser(aBrowser) {
    993      return this._tabForBrowser.get(aBrowser);
    994    }
    995 
    996    getPanel(aBrowser) {
    997      return this.getBrowserContainer(aBrowser).parentNode;
    998    }
    999 
   1000    getBrowserContainer(aBrowser) {
   1001      return (aBrowser || this.selectedBrowser).parentNode.parentNode;
   1002    }
   1003 
   1004    getTabNotificationDeck() {
   1005      if (!this._tabNotificationDeck) {
   1006        let template = document.getElementById(
   1007          "tab-notification-deck-template"
   1008        );
   1009        template.replaceWith(template.content);
   1010        this._tabNotificationDeck = document.getElementById(
   1011          "tab-notification-deck"
   1012        );
   1013      }
   1014      return this._tabNotificationDeck;
   1015    }
   1016 
   1017    _nextNotificationBoxId = 0;
   1018    getNotificationBox(aBrowser) {
   1019      let browser = aBrowser || this.selectedBrowser;
   1020      if (!browser._notificationBox) {
   1021        browser._notificationBox = new MozElements.NotificationBox(element => {
   1022          element.setAttribute("notificationside", "top");
   1023          element.setAttribute(
   1024            "name",
   1025            `tab-notification-box-${this._nextNotificationBoxId++}`
   1026          );
   1027          this.getTabNotificationDeck().append(element);
   1028          if (browser == this.selectedBrowser) {
   1029            this._updateVisibleNotificationBox(browser);
   1030          }
   1031        }, this._notificationEnableDelay);
   1032      }
   1033      return browser._notificationBox;
   1034    }
   1035 
   1036    readNotificationBox(aBrowser) {
   1037      let browser = aBrowser || this.selectedBrowser;
   1038      return browser._notificationBox || null;
   1039    }
   1040 
   1041    _updateVisibleNotificationBox(aBrowser) {
   1042      if (!this._tabNotificationDeck) {
   1043        // If the deck hasn't been created we don't need to create it here.
   1044        return;
   1045      }
   1046      let notificationBox = this.readNotificationBox(aBrowser);
   1047      this.getTabNotificationDeck().selectedViewName = notificationBox
   1048        ? notificationBox.stack.getAttribute("name")
   1049        : "";
   1050    }
   1051 
   1052    getTabDialogBox(aBrowser) {
   1053      if (!aBrowser) {
   1054        throw new Error("aBrowser is required");
   1055      }
   1056      if (!aBrowser.tabDialogBox) {
   1057        aBrowser.tabDialogBox = new TabDialogBox(aBrowser);
   1058      }
   1059      return aBrowser.tabDialogBox;
   1060    }
   1061 
   1062    getTabFromAudioEvent(aEvent) {
   1063      if (!aEvent.isTrusted) {
   1064        return null;
   1065      }
   1066 
   1067      var browser = aEvent.originalTarget;
   1068      var tab = this.getTabForBrowser(browser);
   1069      return tab;
   1070    }
   1071 
   1072    _callProgressListeners(
   1073      aBrowser,
   1074      aMethod,
   1075      aArguments,
   1076      aCallGlobalListeners = true,
   1077      aCallTabsListeners = true
   1078    ) {
   1079      var rv = true;
   1080 
   1081      function callListeners(listeners, args) {
   1082        for (let p of listeners) {
   1083          if (aMethod in p) {
   1084            try {
   1085              if (!p[aMethod].apply(p, args)) {
   1086                rv = false;
   1087              }
   1088            } catch (e) {
   1089              // don't inhibit other listeners
   1090              console.error(e);
   1091            }
   1092          }
   1093        }
   1094      }
   1095 
   1096      aBrowser = aBrowser || this.selectedBrowser;
   1097 
   1098      if (aCallGlobalListeners && aBrowser == this.selectedBrowser) {
   1099        callListeners(this.mProgressListeners, aArguments);
   1100      }
   1101 
   1102      if (aCallTabsListeners) {
   1103        aArguments.unshift(aBrowser);
   1104 
   1105        callListeners(this.mTabsProgressListeners, aArguments);
   1106      }
   1107 
   1108      return rv;
   1109    }
   1110 
   1111    /**
   1112     * Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS.
   1113     */
   1114    setDefaultIcon(aTab, aURI) {
   1115      if (aURI && aURI.spec in FAVICON_DEFAULTS) {
   1116        this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]);
   1117      }
   1118    }
   1119 
   1120    setIcon(
   1121      aTab,
   1122      aIconURL = "",
   1123      aOriginalURL = aIconURL,
   1124      aClearImageFirst = false
   1125    ) {
   1126      let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url);
   1127 
   1128      aIconURL = makeString(aIconURL);
   1129      aOriginalURL = makeString(aOriginalURL);
   1130 
   1131      let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
   1132 
   1133      if (
   1134        aIconURL &&
   1135        !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
   1136      ) {
   1137        console.error(
   1138          `Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.`
   1139        );
   1140        return;
   1141      }
   1142 
   1143      let browser = this.getBrowserForTab(aTab);
   1144      browser.mIconURL = aIconURL;
   1145 
   1146      if (aIconURL != aTab.getAttribute("image")) {
   1147        if (aClearImageFirst) {
   1148          aTab.removeAttribute("image");
   1149        }
   1150        if (aIconURL) {
   1151          let url = aIconURL;
   1152          if (
   1153            this._remoteSVGIconDecoding &&
   1154            url.startsWith(this.FaviconUtils.SVG_DATA_URI_PREFIX)
   1155          ) {
   1156            // 16px is hardcoded for .tab-icon-image in tabs.css
   1157            let size = Math.floor(16 * window.devicePixelRatio);
   1158            url = this.FaviconUtils.getMozRemoteImageURL(url, size);
   1159          }
   1160          aTab.setAttribute("image", url);
   1161        } else {
   1162          aTab.removeAttribute("image");
   1163        }
   1164        this._tabAttrModified(aTab, ["image"]);
   1165      }
   1166 
   1167      // The aOriginalURL argument is currently only used by tests.
   1168      this._callProgressListeners(browser, "onLinkIconAvailable", [
   1169        aIconURL,
   1170        aOriginalURL,
   1171      ]);
   1172    }
   1173 
   1174    getIcon(aTab) {
   1175      let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
   1176      return browser.mIconURL;
   1177    }
   1178 
   1179    setPageInfo(tab, aURL, aDescription, aPreviewImage) {
   1180      if (aURL) {
   1181        let pageInfo = {
   1182          url: aURL,
   1183          description: aDescription,
   1184          previewImageURL: aPreviewImage,
   1185        };
   1186        PlacesUtils.history.update(pageInfo).catch(console.error);
   1187      }
   1188      if (tab) {
   1189        tab.description = aDescription;
   1190      }
   1191    }
   1192 
   1193    #cachedTitleInfo = null;
   1194    #populateTitleCache() {
   1195      this.#cachedTitleInfo = {};
   1196      for (let id of [
   1197        "mainWindowTitle",
   1198        "privateWindowTitle",
   1199        "privateWindowSuffixForContent",
   1200      ]) {
   1201        this.#cachedTitleInfo[id] =
   1202          document.getElementById(id)?.textContent || "";
   1203      }
   1204    }
   1205 
   1206    /**
   1207     * The current Taskbar Tab associated with this window. This cannot
   1208     * change after it is first set.
   1209     *
   1210     * @type {TaskbarTab|null}
   1211     */
   1212    #taskbarTab = null;
   1213 
   1214    /**
   1215     * The last title associated with this window, avoiding re-lookup
   1216     * of the container name and localizations.
   1217     *
   1218     * @type {string|null}
   1219     */
   1220    #taskbarTabTitle = null;
   1221 
   1222    /**
   1223     * The last profile used when determining the Taskbar Tab title. (This
   1224     * can change, for example if the first profile is made after opening
   1225     * the Taskbar Tab.)
   1226     *
   1227     * @type {string|null}
   1228     */
   1229    #taskbarTabTitleLastProfile = null;
   1230 
   1231    /**
   1232     * Determines the content of the window title that relates to the Taskbar
   1233     * Tab. This includes the name of the Taskbar Tab, of the container, and
   1234     * of the profile.
   1235     *
   1236     * If no Taskbar Tab is in use, the profile is added by
   1237     * getWindowTitleForBrowser and this returns null.
   1238     *
   1239     * @returns {string|null} The part of the title that was determined from
   1240     * the Taskbar Tab, or null if nothing is needed.
   1241     */
   1242    #determineTaskbarTabTitle(aProfile) {
   1243      if (!this._shouldExposeContentTitle) {
   1244        // The Taskbar Tab and container info expose what site the user's on.
   1245        return null;
   1246      }
   1247 
   1248      if (
   1249        this.#taskbarTabTitle &&
   1250        this.#taskbarTabTitleLastProfile == aProfile
   1251      ) {
   1252        return this.#taskbarTabTitle;
   1253      }
   1254 
   1255      let id = this.TaskbarTabsUtils.getTaskbarTabIdFromWindow(window);
   1256      if (!id) {
   1257        return null;
   1258      }
   1259 
   1260      if (!this.#taskbarTab) {
   1261        this.TaskbarTabs.getTaskbarTab(id)
   1262          .then(tt => {
   1263            this.#taskbarTab = tt;
   1264            this.updateTitlebar();
   1265          })
   1266          .catch(() => {
   1267            // The taskbar tab doesn't exist; leave it as-is.
   1268          });
   1269        return null;
   1270      }
   1271 
   1272      let containerLabel = this.#taskbarTab.userContextId
   1273        ? ContextualIdentityService.getUserContextLabel(
   1274            this.#taskbarTab.userContextId
   1275          )
   1276        : "";
   1277 
   1278      let stringName = "taskbar-tab-title-default";
   1279      if (containerLabel && aProfile) {
   1280        stringName = "taskbar-tab-title-container-profile";
   1281      } else if (containerLabel && !aProfile) {
   1282        stringName = "taskbar-tab-title-container";
   1283      } else if (!containerLabel && aProfile) {
   1284        stringName = "taskbar-tab-title-profile";
   1285      }
   1286 
   1287      this.#taskbarTabTitle = this.tabLocalization.formatValueSync(stringName, {
   1288        name: this.#taskbarTab.name,
   1289        container: containerLabel,
   1290        profile: aProfile,
   1291      });
   1292      return this.#taskbarTabTitle;
   1293    }
   1294 
   1295    #determineContentTitle(browser) {
   1296      let title = "";
   1297      if (
   1298        !this._shouldExposeContentTitle ||
   1299        (PrivateBrowsingUtils.isWindowPrivate(window) &&
   1300          !this._shouldExposeContentTitlePbm)
   1301      ) {
   1302        return title;
   1303      }
   1304 
   1305      let docElement = document.documentElement;
   1306      // If location bar is hidden and the URL type supports a host,
   1307      // add the scheme and host to the title to prevent spoofing.
   1308      // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
   1309      try {
   1310        if (docElement.getAttribute("chromehidden").includes("location")) {
   1311          const uri = Services.io.createExposableURI(browser.currentURI);
   1312          let prefix = uri.prePath;
   1313          if (uri.scheme == "about") {
   1314            prefix = uri.spec;
   1315          } else if (uri.scheme == "moz-extension") {
   1316            const ext = WebExtensionPolicy.getByHostname(uri.host);
   1317            if (ext && ext.name) {
   1318              let extensionLabel = document.getElementById(
   1319                "urlbar-label-extension"
   1320              );
   1321              prefix = `${extensionLabel.value} (${ext.name})`;
   1322            }
   1323          }
   1324          title = prefix + " - ";
   1325        }
   1326      } catch (e) {
   1327        // ignored
   1328      }
   1329 
   1330      if (docElement.hasAttribute("titlepreface")) {
   1331        title += docElement.getAttribute("titlepreface");
   1332      }
   1333 
   1334      let tab = this.getTabForBrowser(browser);
   1335      if (tab._labelIsContentTitle) {
   1336        // Strip out any null bytes in the content title, since the
   1337        // underlying widget implementations of nsWindow::SetTitle pass
   1338        // null-terminated strings to system APIs.
   1339        title += tab.getAttribute("label").replace(/\0/g, "");
   1340      }
   1341      return title;
   1342    }
   1343 
   1344    getWindowTitleForBrowser(browser) {
   1345      if (!this.#cachedTitleInfo) {
   1346        this.#populateTitleCache();
   1347      }
   1348      let contentTitle = this.#determineContentTitle(browser);
   1349      let docElement = document.documentElement;
   1350      let isTemporaryPrivateWindow =
   1351        docElement.getAttribute("privatebrowsingmode") == "temporary";
   1352 
   1353      let profileIdentifier =
   1354        SelectableProfileService?.isEnabled &&
   1355        SelectableProfileService.currentProfile?.name.replace(/\0/g, "");
   1356      // Note that empty/falsy bits get filtered below.
   1357 
   1358      let taskbarTabTitle = this.#determineTaskbarTabTitle(profileIdentifier);
   1359      let parts = [contentTitle, taskbarTabTitle ?? profileIdentifier];
   1360 
   1361      // On macOS PB windows, add the private window suffix if we have a content
   1362      // title. We'll add the brand name and private window suffix for all other
   1363      // platforms below.
   1364      if (
   1365        AppConstants.platform == "macosx" &&
   1366        contentTitle &&
   1367        isTemporaryPrivateWindow
   1368      ) {
   1369        parts.push(this.#cachedTitleInfo.privateWindowSuffixForContent);
   1370      }
   1371 
   1372      // Show the brand name if we aren't a Taskbar Tab, since that is done in
   1373      // #determineTaskbarTabTitle. On macOS we only do this if we don't have a
   1374      // content title; elsewhere, the brand becomes a suffix in the title bar.
   1375      if (
   1376        !taskbarTabTitle &&
   1377        (!contentTitle || AppConstants.platform != "macosx")
   1378      ) {
   1379        parts.push(
   1380          this.#cachedTitleInfo[
   1381            isTemporaryPrivateWindow ? "privateWindowTitle" : "mainWindowTitle"
   1382          ]
   1383        );
   1384      }
   1385 
   1386      return parts.filter(p => !!p).join(" — ");
   1387    }
   1388 
   1389    updateTitlebar() {
   1390      document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
   1391    }
   1392 
   1393    updateCurrentBrowser(aForceUpdate) {
   1394      let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
   1395      if (this.selectedBrowser == newBrowser && !aForceUpdate) {
   1396        return;
   1397      }
   1398 
   1399      let oldBrowser = this.selectedBrowser;
   1400      // Once the async switcher starts, it's unpredictable when it will touch
   1401      // the address bar, thus we store its state immediately.
   1402      gURLBar?.saveSelectionStateForBrowser(oldBrowser);
   1403 
   1404      let newTab = this.getTabForBrowser(newBrowser);
   1405 
   1406      let timerId;
   1407      if (!aForceUpdate) {
   1408        timerId = Glean.browserTabswitch.update.start();
   1409 
   1410        if (gMultiProcessBrowser) {
   1411          this._asyncTabSwitching = true;
   1412          this._getSwitcher().requestTab(newTab);
   1413          this._asyncTabSwitching = false;
   1414        }
   1415 
   1416        document.commandDispatcher.lock();
   1417      }
   1418 
   1419      let oldTab = this.selectedTab;
   1420 
   1421      // Preview mode should not reset the owner
   1422      if (!this._previewMode && !oldTab.selected) {
   1423        oldTab.owner = null;
   1424      }
   1425 
   1426      let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
   1427      if (lastRelatedTab) {
   1428        if (!lastRelatedTab.selected) {
   1429          lastRelatedTab.owner = null;
   1430        }
   1431      }
   1432      this._lastRelatedTabMap = new WeakMap();
   1433 
   1434      if (!gMultiProcessBrowser) {
   1435        oldBrowser.removeAttribute("primary");
   1436        oldBrowser.docShellIsActive = false;
   1437        newBrowser.setAttribute("primary", "true");
   1438        newBrowser.docShellIsActive = !document.hidden;
   1439      }
   1440 
   1441      this._selectedBrowser = newBrowser;
   1442      this._selectedTab = newTab;
   1443      this.showTab(newTab);
   1444 
   1445      this.appendStatusPanel();
   1446 
   1447      this._updateVisibleNotificationBox(newBrowser);
   1448 
   1449      let oldBrowserPopupsBlocked =
   1450        oldBrowser.popupAndRedirectBlocker.getBlockedPopupCount();
   1451      let newBrowserPopupsBlocked =
   1452        newBrowser.popupAndRedirectBlocker.getBlockedPopupCount();
   1453      if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
   1454        newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedPopupsEvent();
   1455      }
   1456 
   1457      let oldBrowserRedirectBlocked =
   1458        oldBrowser.popupAndRedirectBlocker.isRedirectBlocked();
   1459      let newBrowserRedirectBlocked =
   1460        newBrowser.popupAndRedirectBlocker.isRedirectBlocked();
   1461      if (oldBrowserRedirectBlocked != newBrowserRedirectBlocked) {
   1462        newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedRedirectEvent();
   1463      }
   1464 
   1465      // Update the URL bar.
   1466      let webProgress = newBrowser.webProgress;
   1467      this._callProgressListeners(
   1468        null,
   1469        "onLocationChange",
   1470        [webProgress, null, newBrowser.currentURI, 0, true],
   1471        true,
   1472        false
   1473      );
   1474 
   1475      let securityUI = newBrowser.securityUI;
   1476      if (securityUI) {
   1477        this._callProgressListeners(
   1478          null,
   1479          "onSecurityChange",
   1480          [webProgress, null, securityUI.state],
   1481          true,
   1482          false
   1483        );
   1484        // Include the true final argument to indicate that this event is
   1485        // simulated (instead of being observed by the webProgressListener).
   1486        this._callProgressListeners(
   1487          null,
   1488          "onContentBlockingEvent",
   1489          [webProgress, null, newBrowser.getContentBlockingEvents(), true],
   1490          true,
   1491          false
   1492        );
   1493      }
   1494 
   1495      let listener = this._tabListeners.get(newTab);
   1496      if (listener && listener.mStateFlags) {
   1497        this._callProgressListeners(
   1498          null,
   1499          "onUpdateCurrentBrowser",
   1500          [
   1501            listener.mStateFlags,
   1502            listener.mStatus,
   1503            listener.mMessage,
   1504            listener.mTotalProgress,
   1505          ],
   1506          true,
   1507          false
   1508        );
   1509      }
   1510 
   1511      if (!this._previewMode) {
   1512        newTab.recordTimeFromUnloadToReload();
   1513        newTab.updateLastAccessed();
   1514        oldTab.updateLastAccessed();
   1515        // if this is the foreground window, update the last-seen timestamps.
   1516        if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
   1517          newTab.updateLastSeenActive();
   1518          oldTab.updateLastSeenActive();
   1519        }
   1520 
   1521        let oldFindBar = oldTab._findBar;
   1522        if (
   1523          oldFindBar &&
   1524          oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
   1525          !oldFindBar.hidden
   1526        ) {
   1527          this._lastFindValue = oldFindBar._findField.value;
   1528        }
   1529 
   1530        this.updateTitlebar();
   1531 
   1532        newTab.removeAttribute("titlechanged");
   1533        newTab.attention = false;
   1534 
   1535        // The tab has been selected, it's not unselected anymore.
   1536        // Call the current browser's unselectedTabHover() with false
   1537        // to dispatch an event.
   1538        newBrowser.unselectedTabHover(false);
   1539      }
   1540 
   1541      // If the new tab is busy, and our current state is not busy, then
   1542      // we need to fire a start to all progress listeners.
   1543      if (newTab.hasAttribute("busy") && !this._isBusy) {
   1544        this._isBusy = true;
   1545        this._callProgressListeners(
   1546          null,
   1547          "onStateChange",
   1548          [
   1549            webProgress,
   1550            null,
   1551            Ci.nsIWebProgressListener.STATE_START |
   1552              Ci.nsIWebProgressListener.STATE_IS_NETWORK,
   1553            0,
   1554          ],
   1555          true,
   1556          false
   1557        );
   1558      }
   1559 
   1560      // If the new tab is not busy, and our current state is busy, then
   1561      // we need to fire a stop to all progress listeners.
   1562      if (!newTab.hasAttribute("busy") && this._isBusy) {
   1563        this._isBusy = false;
   1564        this._callProgressListeners(
   1565          null,
   1566          "onStateChange",
   1567          [
   1568            webProgress,
   1569            null,
   1570            Ci.nsIWebProgressListener.STATE_STOP |
   1571              Ci.nsIWebProgressListener.STATE_IS_NETWORK,
   1572            0,
   1573          ],
   1574          true,
   1575          false
   1576        );
   1577      }
   1578 
   1579      // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
   1580      // that might rely upon the other changes suppressed.
   1581      // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
   1582      if (!this._previewMode) {
   1583        // We've selected the new tab, so go ahead and notify listeners.
   1584        let event = new CustomEvent("TabSelect", {
   1585          bubbles: true,
   1586          cancelable: false,
   1587          detail: {
   1588            previousTab: oldTab,
   1589          },
   1590        });
   1591        newTab.dispatchEvent(event);
   1592 
   1593        this._tabAttrModified(oldTab, ["selected"]);
   1594        this._tabAttrModified(newTab, ["selected"]);
   1595 
   1596        this._startMultiSelectChange();
   1597        this._multiSelectChangeSelected = true;
   1598        this.clearMultiSelectedTabs();
   1599        if (this._multiSelectChangeAdditions.size) {
   1600          // Some tab has been multiselected just before switching tabs.
   1601          // The tab that was selected at that point should also be multiselected.
   1602          this.addToMultiSelectedTabs(oldTab);
   1603        }
   1604 
   1605        if (!gMultiProcessBrowser) {
   1606          this._adjustFocusBeforeTabSwitch(oldTab, newTab);
   1607          this._adjustFocusAfterTabSwitch(newTab);
   1608        }
   1609 
   1610        // Bug 1781806 - A forced update can indicate the tab was already
   1611        // selected. To ensure the internal state of the Urlbar is kept in
   1612        // sync, notify it as if focus changed. Alternatively, if there is no
   1613        // force update but the load context is not using remote tabs, there
   1614        // can be a focus change due to the _adjustFocus above.
   1615        if (aForceUpdate || !gMultiProcessBrowser) {
   1616          gURLBar.afterTabSwitchFocusChange();
   1617        }
   1618      }
   1619 
   1620      updateUserContextUIIndicator();
   1621      gPermissionPanel.updateSharingIndicator();
   1622 
   1623      // Enable touch events to start a native dragging
   1624      // session to allow the user to easily drag the selected tab.
   1625      // This is currently only supported on Windows.
   1626      oldTab.removeAttribute("touchdownstartsdrag");
   1627      newTab.setAttribute("touchdownstartsdrag", "true");
   1628 
   1629      if (!gMultiProcessBrowser) {
   1630        document.commandDispatcher.unlock();
   1631 
   1632        let event = new CustomEvent("TabSwitchDone", {
   1633          bubbles: true,
   1634          cancelable: true,
   1635        });
   1636        this.dispatchEvent(event);
   1637      }
   1638 
   1639      if (!aForceUpdate) {
   1640        Glean.browserTabswitch.update.stopAndAccumulate(timerId);
   1641      }
   1642    }
   1643 
   1644    _adjustFocusBeforeTabSwitch(oldTab, newTab) {
   1645      if (this._previewMode) {
   1646        return;
   1647      }
   1648 
   1649      let oldBrowser = oldTab.linkedBrowser;
   1650      let newBrowser = newTab.linkedBrowser;
   1651 
   1652      gURLBar.getBrowserState(oldBrowser).urlbarFocused = gURLBar.focused;
   1653 
   1654      if (this._asyncTabSwitching) {
   1655        newBrowser._userTypedValueAtBeforeTabSwitch = newBrowser.userTypedValue;
   1656      }
   1657 
   1658      if (this.isFindBarInitialized(oldTab)) {
   1659        let findBar = this.getCachedFindBar(oldTab);
   1660        oldTab._findBarFocused =
   1661          !findBar.hidden &&
   1662          findBar._findField.getAttribute("focused") == "true";
   1663      }
   1664 
   1665      let activeEl = document.activeElement;
   1666      // If focus is on the old tab, move it to the new tab.
   1667      if (activeEl == oldTab) {
   1668        newTab.focus();
   1669      } else if (
   1670        gMultiProcessBrowser &&
   1671        activeEl != newBrowser &&
   1672        activeEl != newTab
   1673      ) {
   1674        // In e10s, if focus isn't already in the tabstrip or on the new browser,
   1675        // and the new browser's previous focus wasn't in the url bar but focus is
   1676        // there now, we need to adjust focus further.
   1677        let keepFocusOnUrlBar =
   1678          newBrowser &&
   1679          gURLBar.getBrowserState(newBrowser).urlbarFocused &&
   1680          gURLBar.focused;
   1681        if (!keepFocusOnUrlBar) {
   1682          // Clear focus so that _adjustFocusAfterTabSwitch can detect if
   1683          // some element has been focused and respect that.
   1684          document.activeElement.blur();
   1685        }
   1686      }
   1687    }
   1688 
   1689    _adjustFocusAfterTabSwitch(newTab) {
   1690      // Don't steal focus from the tab bar.
   1691      if (document.activeElement == newTab) {
   1692        return;
   1693      }
   1694 
   1695      let newBrowser = this.getBrowserForTab(newTab);
   1696 
   1697      if (newBrowser.hasAttribute("tabDialogShowing")) {
   1698        newBrowser.tabDialogBox.focus();
   1699        return;
   1700      }
   1701      // Focus the location bar if it was previously focused for that tab.
   1702      // In full screen mode, only bother making the location bar visible
   1703      // if the tab is a blank one.
   1704      if (gURLBar.getBrowserState(newBrowser).urlbarFocused) {
   1705        let selectURL = () => {
   1706          if (this._asyncTabSwitching) {
   1707            // Set _awaitingSetURI flag to suppress popup notification
   1708            // explicitly while tab switching asynchronously.
   1709            newBrowser._awaitingSetURI = true;
   1710 
   1711            // The onLocationChange event called in updateCurrentBrowser() will
   1712            // be captured in browser.js, then it calls gURLBar.setURI(). In case
   1713            // of that doing processing of here before doing above processing,
   1714            // the selection status that gURLBar.select() does will be releasing
   1715            // by gURLBar.setURI(). To resolve it, we call gURLBar.select() after
   1716            // finishing gURLBar.setURI().
   1717            const currentActiveElement = document.activeElement;
   1718            gURLBar.inputField.addEventListener(
   1719              "SetURI",
   1720              () => {
   1721                delete newBrowser._awaitingSetURI;
   1722 
   1723                // If the user happened to type into the URL bar for this browser
   1724                // by the time we got here, focusing will cause the text to be
   1725                // selected which could cause them to overwrite what they've
   1726                // already typed in.
   1727                let userTypedValueAtBeforeTabSwitch =
   1728                  newBrowser._userTypedValueAtBeforeTabSwitch;
   1729                delete newBrowser._userTypedValueAtBeforeTabSwitch;
   1730                if (
   1731                  newBrowser.userTypedValue &&
   1732                  newBrowser.userTypedValue != userTypedValueAtBeforeTabSwitch
   1733                ) {
   1734                  return;
   1735                }
   1736 
   1737                if (currentActiveElement != document.activeElement) {
   1738                  return;
   1739                }
   1740                gURLBar.restoreSelectionStateForBrowser(newBrowser);
   1741              },
   1742              { once: true }
   1743            );
   1744          } else {
   1745            gURLBar.restoreSelectionStateForBrowser(newBrowser);
   1746          }
   1747        };
   1748 
   1749        // This inDOMFullscreen attribute indicates that the page has something
   1750        // such as a video in fullscreen mode. Opening a new tab will cancel
   1751        // fullscreen mode, so we need to wait for that to happen and then
   1752        // select the url field.
   1753        if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
   1754          window.addEventListener("MozDOMFullscreen:Exited", selectURL, {
   1755            once: true,
   1756            wantsUntrusted: false,
   1757          });
   1758          return;
   1759        }
   1760 
   1761        if (!window.fullScreen || newTab.isEmpty) {
   1762          selectURL();
   1763          return;
   1764        }
   1765      }
   1766 
   1767      // Focus the find bar if it was previously focused for that tab.
   1768      if (
   1769        gFindBarInitialized &&
   1770        !gFindBar.hidden &&
   1771        this.selectedTab._findBarFocused
   1772      ) {
   1773        gFindBar._findField.focus();
   1774        return;
   1775      }
   1776 
   1777      // Don't focus the content area if something has been focused after the
   1778      // tab switch was initiated.
   1779      if (gMultiProcessBrowser && document.activeElement != document.body) {
   1780        return;
   1781      }
   1782 
   1783      // We're now committed to focusing the content area.
   1784      let fm = Services.focus;
   1785      let focusFlags = fm.FLAG_NOSCROLL;
   1786 
   1787      if (!gMultiProcessBrowser) {
   1788        let newFocusedElement = fm.getFocusedElementForWindow(
   1789          window.content,
   1790          true,
   1791          {}
   1792        );
   1793 
   1794        // for anchors, use FLAG_SHOWRING so that it is clear what link was
   1795        // last clicked when switching back to that tab
   1796        if (
   1797          newFocusedElement &&
   1798          (HTMLAnchorElement.isInstance(newFocusedElement) ||
   1799            newFocusedElement.getAttributeNS(
   1800              "http://www.w3.org/1999/xlink",
   1801              "type"
   1802            ) == "simple")
   1803        ) {
   1804          focusFlags |= fm.FLAG_SHOWRING;
   1805        }
   1806      }
   1807 
   1808      fm.setFocus(newBrowser, focusFlags);
   1809    }
   1810 
   1811    _tabAttrModified(aTab, aChanged) {
   1812      if (aTab.closing) {
   1813        return;
   1814      }
   1815 
   1816      let event = new CustomEvent("TabAttrModified", {
   1817        bubbles: true,
   1818        cancelable: false,
   1819        detail: {
   1820          changed: aChanged,
   1821        },
   1822      });
   1823      aTab.dispatchEvent(event);
   1824    }
   1825 
   1826    resetBrowserSharing(aBrowser) {
   1827      let tab = this.getTabForBrowser(aBrowser);
   1828      if (!tab) {
   1829        return;
   1830      }
   1831      // If WebRTC was used, leave object to enable tracking of grace periods.
   1832      tab._sharingState = tab._sharingState?.webRTC ? { webRTC: {} } : {};
   1833      tab.removeAttribute("sharing");
   1834      this._tabAttrModified(tab, ["sharing"]);
   1835      if (aBrowser == this.selectedBrowser) {
   1836        gPermissionPanel.updateSharingIndicator();
   1837      }
   1838    }
   1839 
   1840    updateBrowserSharing(aBrowser, aState) {
   1841      let tab = this.getTabForBrowser(aBrowser);
   1842      if (!tab) {
   1843        return;
   1844      }
   1845      if (tab._sharingState == null) {
   1846        tab._sharingState = {};
   1847      }
   1848      tab._sharingState = Object.assign(tab._sharingState, aState);
   1849 
   1850      if ("webRTC" in aState) {
   1851        if (tab._sharingState.webRTC?.sharing) {
   1852          if (tab._sharingState.webRTC.paused) {
   1853            tab.removeAttribute("sharing");
   1854          } else {
   1855            tab.setAttribute("sharing", aState.webRTC.sharing);
   1856          }
   1857        } else {
   1858          tab.removeAttribute("sharing");
   1859        }
   1860        this._tabAttrModified(tab, ["sharing"]);
   1861      }
   1862 
   1863      if (aBrowser == this.selectedBrowser) {
   1864        gPermissionPanel.updateSharingIndicator();
   1865      }
   1866    }
   1867 
   1868    getTabSharingState(aTab) {
   1869      // Normalize the state object for consumers (ie.extensions).
   1870      let state = Object.assign(
   1871        {},
   1872        aTab._sharingState && aTab._sharingState.webRTC
   1873      );
   1874      return {
   1875        camera: !!state.camera,
   1876        microphone: !!state.microphone,
   1877        screen: state.screen && state.screen.replace("Paused", ""),
   1878      };
   1879    }
   1880 
   1881    setInitialTabTitle(aTab, aTitle, aOptions = {}) {
   1882      // Convert some non-content title (actually a url) to human readable title
   1883      if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) {
   1884        aTitle = this.tabContainer.emptyTabTitle;
   1885      }
   1886 
   1887      if (aTitle) {
   1888        if (!aTab.getAttribute("label")) {
   1889          aTab._labelIsInitialTitle = true;
   1890        }
   1891 
   1892        this._setTabLabel(aTab, aTitle, aOptions);
   1893      }
   1894    }
   1895 
   1896    _dataURLRegEx = /^data:[^,]+;base64,/i;
   1897 
   1898    // Regex to test if a string (potential tab label) consists of only non-
   1899    // printable characters. We consider Unicode categories Separator
   1900    // (spaces & line-breaks) and Other (control chars, private use, non-
   1901    // character codepoints) to be unprintable, along with a few specific
   1902    // characters whose expected rendering is blank:
   1903    //   U+2800 BRAILLE PATTERN BLANK (category So)
   1904    //   U+115F HANGUL CHOSEONG FILLER (category Lo)
   1905    //   U+1160 HANGUL JUNGSEONG FILLER (category Lo)
   1906    //   U+3164 HANGUL FILLER (category Lo)
   1907    //   U+FFA0 HALFWIDTH HANGUL FILLER (category Lo)
   1908    // We also ignore combining marks, as in the absence of a printable base
   1909    // character they are unlikely to be usefully rendered, and may well be
   1910    // clipped away entirely.
   1911    _nonPrintingRegEx =
   1912      /^[\p{Z}\p{C}\p{M}\u{115f}\u{1160}\u{2800}\u{3164}\u{ffa0}]*$/u;
   1913 
   1914    setTabTitle(aTab) {
   1915      var browser = this.getBrowserForTab(aTab);
   1916      var title = browser.contentTitle;
   1917 
   1918      if (aTab.hasAttribute("customizemode")) {
   1919        title = this.tabLocalization.formatValueSync(
   1920          "tabbrowser-customizemode-tab-title"
   1921        );
   1922      }
   1923 
   1924      // Don't replace an initially set label with the URL while the tab
   1925      // is loading.
   1926      if (aTab._labelIsInitialTitle) {
   1927        if (!title) {
   1928          return false;
   1929        }
   1930        delete aTab._labelIsInitialTitle;
   1931      }
   1932 
   1933      let isURL = false;
   1934 
   1935      // Trim leading and trailing whitespace from the title.
   1936      title = title.trim();
   1937 
   1938      // If the title contains only non-printing characters (or only combining
   1939      // marks, but no base character for them), we won't use it.
   1940      if (this._nonPrintingRegEx.test(title)) {
   1941        title = "";
   1942      }
   1943 
   1944      let isContentTitle = !!title;
   1945      if (!title) {
   1946        // See if we can use the URI as the title.
   1947        if (browser.currentURI.displaySpec) {
   1948          try {
   1949            title = Services.io.createExposableURI(
   1950              browser.currentURI
   1951            ).displaySpec;
   1952          } catch (ex) {
   1953            title = browser.currentURI.displaySpec;
   1954          }
   1955        }
   1956 
   1957        if (title && !isBlankPageURL(title)) {
   1958          isURL = true;
   1959          if (title.length <= 500 || !this._dataURLRegEx.test(title)) {
   1960            // Try to unescape not-ASCII URIs using the current character set.
   1961            try {
   1962              let characterSet = browser.characterSet;
   1963              title = Services.textToSubURI.unEscapeNonAsciiURI(
   1964                characterSet,
   1965                title
   1966              );
   1967            } catch (ex) {
   1968              /* Do nothing. */
   1969            }
   1970          }
   1971        } else {
   1972          // No suitable URI? Fall back to our untitled string.
   1973          title = this.tabContainer.emptyTabTitle;
   1974        }
   1975      }
   1976 
   1977      return this._setTabLabel(aTab, title, { isContentTitle, isURL });
   1978    }
   1979 
   1980    // 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,
   1981    // but of the origin that is requesting authentication.
   1982    // This is to prevent possible auth spoofing scenarios.
   1983    // See bug 791594 for reference.
   1984    setTabLabelForAuthPrompts(aTab, aLabel) {
   1985      return this._setTabLabel(aTab, aLabel);
   1986    }
   1987 
   1988    _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
   1989      if (!aLabel || aLabel.includes("about:reader?")) {
   1990        return false;
   1991      }
   1992 
   1993      // If it's a long data: URI that uses base64 encoding, truncate to a
   1994      // reasonable length rather than trying to display the entire thing,
   1995      // which can hang or crash the browser.
   1996      // We can't shorten arbitrary URIs like this, as bidi etc might mean
   1997      // we need the trailing characters for display. But a base64-encoded
   1998      // data-URI is plain ASCII, so this is OK for tab-title display.
   1999      // (See bug 1408854.)
   2000      if (isURL && aLabel.length > 500 && this._dataURLRegEx.test(aLabel)) {
   2001        aLabel = aLabel.substring(0, 500) + "\u2026";
   2002      }
   2003 
   2004      aTab._fullLabel = aLabel;
   2005 
   2006      if (!isContentTitle) {
   2007        // Remove protocol and "www."
   2008        if (!("_regex_shortenURLForTabLabel" in this)) {
   2009          this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
   2010        }
   2011        aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
   2012      }
   2013 
   2014      aTab._labelIsContentTitle = isContentTitle;
   2015 
   2016      if (aTab.getAttribute("label") == aLabel) {
   2017        return false;
   2018      }
   2019 
   2020      let dwu = window.windowUtils;
   2021      let isRTL =
   2022        dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
   2023 
   2024      aTab.setAttribute("label", aLabel);
   2025      aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
   2026      aTab.toggleAttribute("labelendaligned", isRTL != (document.dir == "rtl"));
   2027 
   2028      // Dispatch TabAttrModified event unless we're setting the label
   2029      // before the TabOpen event was dispatched.
   2030      if (!beforeTabOpen) {
   2031        this._tabAttrModified(aTab, ["label"]);
   2032      }
   2033 
   2034      if (aTab.selected) {
   2035        this.updateTitlebar();
   2036      }
   2037 
   2038      return true;
   2039    }
   2040 
   2041    loadTabs(
   2042      aURIs,
   2043      {
   2044        allowInheritPrincipal,
   2045        allowThirdPartyFixup,
   2046        inBackground,
   2047        newIndex,
   2048        elementIndex,
   2049        postDatas,
   2050        replace,
   2051        tabGroup,
   2052        targetTab,
   2053        triggeringPrincipal,
   2054        policyContainer,
   2055        userContextId,
   2056        fromExternal,
   2057      } = {}
   2058    ) {
   2059      if (!aURIs.length) {
   2060        return;
   2061      }
   2062 
   2063      // The tab selected after this new tab is closed (i.e. the new tab's
   2064      // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
   2065      // when several urls are opened here (i.e. closing the first should select
   2066      // the next of many URLs opened) or if the pref to have UI links opened in
   2067      // the background is set (i.e. the link is not being opened modally)
   2068      //
   2069      // i.e.
   2070      //    Number of URLs    Load UI Links in BG       Focus Last Viewed?
   2071      //    == 1              false                     YES
   2072      //    == 1              true                      NO
   2073      //    > 1               false/true                NO
   2074      var multiple = aURIs.length > 1;
   2075      var owner = multiple || inBackground ? null : this.selectedTab;
   2076      var firstTabAdded = null;
   2077      var targetTabIndex = -1;
   2078 
   2079      if (typeof elementIndex == "number") {
   2080        newIndex = this.#elementIndexToTabIndex(elementIndex);
   2081      }
   2082      if (typeof newIndex != "number") {
   2083        newIndex = -1;
   2084      }
   2085 
   2086      // When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent
   2087      // if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks
   2088      // open in the same order they are in the folder.
   2089      if (
   2090        multiple &&
   2091        newIndex < 0 &&
   2092        Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")
   2093      ) {
   2094        newIndex = this.selectedTab._tPos + 1;
   2095      }
   2096 
   2097      if (replace) {
   2098        if (this.isTabGroupLabel(targetTab)) {
   2099          throw new Error(
   2100            "Replacing a tab group label with a tab is not supported"
   2101          );
   2102        }
   2103        let browser;
   2104        if (targetTab) {
   2105          browser = this.getBrowserForTab(targetTab);
   2106          targetTabIndex = targetTab._tPos;
   2107        } else {
   2108          browser = this.selectedBrowser;
   2109          targetTabIndex = this.tabContainer.selectedIndex;
   2110        }
   2111        let loadFlags = LOAD_FLAGS_NONE;
   2112        if (allowThirdPartyFixup) {
   2113          loadFlags |=
   2114            LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
   2115        }
   2116        if (!allowInheritPrincipal) {
   2117          loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
   2118        }
   2119        if (fromExternal) {
   2120          loadFlags |= LOAD_FLAGS_FROM_EXTERNAL;
   2121        }
   2122        try {
   2123          browser.fixupAndLoadURIString(aURIs[0], {
   2124            loadFlags,
   2125            postData: postDatas && postDatas[0],
   2126            triggeringPrincipal,
   2127            policyContainer,
   2128          });
   2129        } catch (e) {
   2130          // Ignore failure in case a URI is wrong, so we can continue
   2131          // opening the next ones.
   2132        }
   2133      } else {
   2134        let params = {
   2135          allowInheritPrincipal,
   2136          ownerTab: owner,
   2137          skipAnimation: multiple,
   2138          allowThirdPartyFixup,
   2139          postData: postDatas && postDatas[0],
   2140          userContextId,
   2141          triggeringPrincipal,
   2142          bulkOrderedOpen: multiple,
   2143          policyContainer,
   2144          fromExternal,
   2145          tabGroup,
   2146        };
   2147        if (newIndex > -1) {
   2148          params.tabIndex = newIndex;
   2149        }
   2150        firstTabAdded = this.addTab(aURIs[0], params);
   2151        if (newIndex > -1) {
   2152          targetTabIndex = firstTabAdded._tPos;
   2153        }
   2154      }
   2155 
   2156      let tabNum = targetTabIndex;
   2157      for (let i = 1; i < aURIs.length; ++i) {
   2158        let params = {
   2159          allowInheritPrincipal,
   2160          skipAnimation: true,
   2161          allowThirdPartyFixup,
   2162          postData: postDatas && postDatas[i],
   2163          userContextId,
   2164          triggeringPrincipal,
   2165          bulkOrderedOpen: true,
   2166          policyContainer,
   2167          fromExternal,
   2168          tabGroup,
   2169        };
   2170        if (targetTabIndex > -1) {
   2171          params.tabIndex = ++tabNum;
   2172        }
   2173        this.addTab(aURIs[i], params);
   2174      }
   2175 
   2176      if (firstTabAdded && !inBackground) {
   2177        this.selectedTab = firstTabAdded;
   2178      }
   2179    }
   2180 
   2181    updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) {
   2182      let isRemote = aBrowser.getAttribute("remote") == "true";
   2183 
   2184      // We have to be careful with this here, as the "no remote type" is null,
   2185      // not a string. Make sure to check only for undefined, since null is
   2186      // allowed.
   2187      if (remoteType === undefined) {
   2188        throw new Error("Remote type must be set!");
   2189      }
   2190 
   2191      let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
   2192 
   2193      if (!gMultiProcessBrowser && shouldBeRemote) {
   2194        throw new Error(
   2195          "Cannot switch to remote browser in a window " +
   2196            "without the remote tabs load context."
   2197        );
   2198      }
   2199 
   2200      // Abort if we're not going to change anything
   2201      let oldRemoteType = aBrowser.remoteType;
   2202      if (
   2203        isRemote == shouldBeRemote &&
   2204        !newFrameloader &&
   2205        (!isRemote || oldRemoteType == remoteType)
   2206      ) {
   2207        return false;
   2208      }
   2209 
   2210      let tab = this.getTabForBrowser(aBrowser);
   2211      // aBrowser needs to be inserted now if it hasn't been already.
   2212      this._insertBrowser(tab);
   2213 
   2214      let evt = document.createEvent("Events");
   2215      evt.initEvent("BeforeTabRemotenessChange", true, false);
   2216      tab.dispatchEvent(evt);
   2217 
   2218      // Unhook our progress listener.
   2219      let filter = this._tabFilters.get(tab);
   2220      let listener = this._tabListeners.get(tab);
   2221      // We should always have a filter, but if we fail to create a content
   2222      // process when creating a new tab, we can end up here trying to switch
   2223      // remoteness to load about:tabcrashed, without a filter/listener.
   2224      if (filter) {
   2225        aBrowser.webProgress.removeProgressListener(filter);
   2226        filter.removeProgressListener(listener);
   2227      }
   2228 
   2229      // We'll be creating a new listener, so destroy the old one.
   2230      listener?.destroy();
   2231 
   2232      let oldDroppedLinkHandler = aBrowser.droppedLinkHandler;
   2233      let oldUserTypedValue = aBrowser.userTypedValue;
   2234      let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
   2235 
   2236      // Change the "remote" attribute.
   2237 
   2238      // Make sure the browser is destroyed so it unregisters from observer notifications
   2239      aBrowser.destroy();
   2240 
   2241      if (shouldBeRemote) {
   2242        aBrowser.setAttribute("remote", "true");
   2243        aBrowser.setAttribute("remoteType", remoteType);
   2244      } else {
   2245        aBrowser.setAttribute("remote", "false");
   2246        aBrowser.removeAttribute("remoteType");
   2247      }
   2248 
   2249      // This call actually switches out our frameloaders. Do this as late as
   2250      // possible before rebuilding the browser, as we'll need the new browser
   2251      // state set up completely first.
   2252      aBrowser.changeRemoteness({
   2253        remoteType,
   2254      });
   2255 
   2256      // Once we have new frameloaders, this call sets the browser back up.
   2257      aBrowser.construct();
   2258 
   2259      aBrowser.userTypedValue = oldUserTypedValue;
   2260      if (hadStartedLoad) {
   2261        aBrowser.urlbarChangeTracker.startedLoad();
   2262      }
   2263 
   2264      aBrowser.droppedLinkHandler = oldDroppedLinkHandler;
   2265 
   2266      // This shouldn't really be necessary, however, this has the side effect
   2267      // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
   2268      // frames, which the tab switcher depends on.
   2269      //
   2270      // eslint-disable-next-line no-self-assign
   2271      aBrowser.docShellIsActive = aBrowser.docShellIsActive;
   2272 
   2273      // Create a new tab progress listener for the new browser we just injected,
   2274      // since tab progress listeners have logic for handling the initial about:blank
   2275      // load
   2276      listener = new TabProgressListener(tab, aBrowser, true, false);
   2277      this._tabListeners.set(tab, listener);
   2278      if (!filter) {
   2279        filter = Cc[
   2280          "@mozilla.org/appshell/component/browser-status-filter;1"
   2281        ].createInstance(Ci.nsIWebProgress);
   2282        this._tabFilters.set(tab, filter);
   2283      }
   2284      filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
   2285 
   2286      // Restore the progress listener.
   2287      aBrowser.webProgress.addProgressListener(
   2288        filter,
   2289        Ci.nsIWebProgress.NOTIFY_ALL
   2290      );
   2291 
   2292      // Restore the securityUI state.
   2293      let securityUI = aBrowser.securityUI;
   2294      let state = securityUI
   2295        ? securityUI.state
   2296        : Ci.nsIWebProgressListener.STATE_IS_INSECURE;
   2297      this._callProgressListeners(
   2298        aBrowser,
   2299        "onSecurityChange",
   2300        [aBrowser.webProgress, null, state],
   2301        true,
   2302        false
   2303      );
   2304      let event = aBrowser.getContentBlockingEvents();
   2305      // Include the true final argument to indicate that this event is
   2306      // simulated (instead of being observed by the webProgressListener).
   2307      this._callProgressListeners(
   2308        aBrowser,
   2309        "onContentBlockingEvent",
   2310        [aBrowser.webProgress, null, event, true],
   2311        true,
   2312        false
   2313      );
   2314 
   2315      if (shouldBeRemote) {
   2316        // Switching the browser to be remote will connect to a new child
   2317        // process so the browser can no longer be considered to be
   2318        // crashed.
   2319        tab.removeAttribute("crashed");
   2320      }
   2321 
   2322      // If the findbar has been initialised, reset its browser reference.
   2323      if (this.isFindBarInitialized(tab)) {
   2324        this.getCachedFindBar(tab).browser = aBrowser;
   2325      }
   2326 
   2327      evt = document.createEvent("Events");
   2328      evt.initEvent("TabRemotenessChange", true, false);
   2329      tab.dispatchEvent(evt);
   2330 
   2331      return true;
   2332    }
   2333 
   2334    updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
   2335      if (!gMultiProcessBrowser) {
   2336        return this.updateBrowserRemoteness(aBrowser, {
   2337          remoteType: E10SUtils.NOT_REMOTE,
   2338        });
   2339      }
   2340 
   2341      let oldRemoteType = aBrowser.remoteType;
   2342 
   2343      let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });
   2344 
   2345      aOptions.remoteType = E10SUtils.getRemoteTypeForURI(
   2346        aURL,
   2347        gMultiProcessBrowser,
   2348        gFissionBrowser,
   2349        oldRemoteType,
   2350        aBrowser.currentURI,
   2351        oa
   2352      );
   2353 
   2354      // If this URL can't load in the current browser then flip it to the
   2355      // correct type.
   2356      if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) {
   2357        return this.updateBrowserRemoteness(aBrowser, aOptions);
   2358      }
   2359 
   2360      return false;
   2361    }
   2362 
   2363    createBrowser({
   2364      isPreloadBrowser,
   2365      name,
   2366      openWindowInfo,
   2367      remoteType,
   2368      initialBrowsingContextGroupId,
   2369      uriIsAboutBlank,
   2370      userContextId,
   2371      skipLoad,
   2372    } = {}) {
   2373      let b = document.createXULElement("browser");
   2374      // Use the JSM global to create the permanentKey, so that if the
   2375      // permanentKey is held by something after this window closes, it
   2376      // doesn't keep the window alive.
   2377      b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
   2378 
   2379      const defaultBrowserAttributes = {
   2380        contextmenu: "contentAreaContextMenu",
   2381        message: "true",
   2382        messagemanagergroup: "browsers",
   2383        tooltip: "aHTMLTooltip",
   2384        type: "content",
   2385        manualactiveness: "true",
   2386      };
   2387      for (let attribute in defaultBrowserAttributes) {
   2388        b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
   2389      }
   2390 
   2391      if (gMultiProcessBrowser || remoteType) {
   2392        b.setAttribute("maychangeremoteness", "true");
   2393      }
   2394 
   2395      if (userContextId) {
   2396        b.setAttribute("usercontextid", userContextId);
   2397      }
   2398 
   2399      if (remoteType) {
   2400        b.setAttribute("remoteType", remoteType);
   2401        b.setAttribute("remote", "true");
   2402      }
   2403 
   2404      if (!isPreloadBrowser) {
   2405        b.setAttribute("autocompletepopup", "PopupAutoComplete");
   2406      }
   2407 
   2408      /*
   2409       * This attribute is meant to describe if the browser is the
   2410       * preloaded browser. When the preloaded browser is created, the
   2411       * 'preloadedState' attribute for that browser is set to "preloaded", and
   2412       * when a new tab is opened, and it is time to show that preloaded
   2413       * browser, the 'preloadedState' attribute for that browser is removed.
   2414       *
   2415       * See more details on Bug 1420285.
   2416       */
   2417      if (isPreloadBrowser) {
   2418        b.setAttribute("preloadedState", "preloaded");
   2419      }
   2420 
   2421      // Ensure that the browser will be created in a specific initial
   2422      // BrowsingContextGroup. This may change the process selection behaviour
   2423      // of the newly created browser, and is often used in combination with
   2424      // "remoteType" to ensure that the initial about:blank load occurs
   2425      // within the same process as another window.
   2426      if (initialBrowsingContextGroupId) {
   2427        b.setAttribute(
   2428          "initialBrowsingContextGroupId",
   2429          initialBrowsingContextGroupId
   2430        );
   2431      }
   2432 
   2433      // Propagate information about the opening content window to the browser.
   2434      if (openWindowInfo) {
   2435        b.openWindowInfo = openWindowInfo;
   2436      }
   2437 
   2438      // This will be used by gecko to control the name of the opened
   2439      // window.
   2440      if (name) {
   2441        // XXX: The `name` property is special in HTML and XUL. Should
   2442        // we use a different attribute name for this?
   2443        b.setAttribute("name", name);
   2444      }
   2445 
   2446      if (AIWindow.isAIWindowActive(window) || this._allowTransparentBrowser) {
   2447        b.setAttribute("transparent", "true");
   2448      }
   2449 
   2450      let stack = document.createXULElement("stack");
   2451      stack.className = "browserStack";
   2452      stack.appendChild(b);
   2453 
   2454      let decorator = document.createXULElement("hbox");
   2455      decorator.className = "browserDecorator";
   2456      stack.appendChild(decorator);
   2457 
   2458      let browserContainer = document.createXULElement("vbox");
   2459      browserContainer.className = "browserContainer";
   2460      browserContainer.appendChild(stack);
   2461 
   2462      let browserSidebarContainer = document.createXULElement("hbox");
   2463      browserSidebarContainer.className = "browserSidebarContainer";
   2464      browserSidebarContainer.appendChild(browserContainer);
   2465 
   2466      // Prevent the superfluous initial load of a blank document
   2467      // if we're going to load something other than about:blank.
   2468      if (!uriIsAboutBlank || skipLoad) {
   2469        b.setAttribute("nodefaultsrc", "true");
   2470      }
   2471 
   2472      return b;
   2473    }
   2474 
   2475    _createLazyBrowser(aTab) {
   2476      let browser = aTab.linkedBrowser;
   2477 
   2478      let names = this._browserBindingProperties;
   2479 
   2480      for (let i = 0; i < names.length; i++) {
   2481        let name = names[i];
   2482        let getter;
   2483        let setter;
   2484        switch (name) {
   2485          case "audioMuted":
   2486            getter = () => aTab.hasAttribute("muted");
   2487            break;
   2488          case "contentTitle":
   2489            getter = () => SessionStore.getLazyTabValue(aTab, "title");
   2490            break;
   2491          case "currentURI":
   2492            getter = () => {
   2493              // Avoid recreating the same nsIURI object over and over again...
   2494              if (browser._cachedCurrentURI) {
   2495                return browser._cachedCurrentURI;
   2496              }
   2497              let url =
   2498                SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
   2499              return (browser._cachedCurrentURI = Services.io.newURI(url));
   2500            };
   2501            break;
   2502          case "didStartLoadSinceLastUserTyping":
   2503            getter = () => () => false;
   2504            break;
   2505          case "fullZoom":
   2506          case "textZoom":
   2507            getter = () => 1;
   2508            break;
   2509          case "tabHasCustomZoom":
   2510            getter = () => false;
   2511            break;
   2512          case "getTabBrowser":
   2513            getter = () => () => this;
   2514            break;
   2515          case "isRemoteBrowser":
   2516            getter = () => browser.getAttribute("remote") == "true";
   2517            break;
   2518          case "permitUnload":
   2519            getter = () => () => ({ permitUnload: true });
   2520            break;
   2521          case "reload":
   2522          case "reloadWithFlags":
   2523            getter = () => params => {
   2524              // Wait for load handler to be instantiated before
   2525              // initializing the reload.
   2526              aTab.addEventListener(
   2527                "SSTabRestoring",
   2528                () => {
   2529                  browser[name](params);
   2530                },
   2531                { once: true }
   2532              );
   2533              gBrowser._insertBrowser(aTab);
   2534            };
   2535            break;
   2536          case "remoteType":
   2537            getter = () => {
   2538              let url =
   2539                SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
   2540              // Avoid recreating the same nsIURI object over and over again...
   2541              let uri;
   2542              if (browser._cachedCurrentURI) {
   2543                uri = browser._cachedCurrentURI;
   2544              } else {
   2545                uri = browser._cachedCurrentURI = Services.io.newURI(url);
   2546              }
   2547              let oa = E10SUtils.predictOriginAttributes({
   2548                browser,
   2549                userContextId: aTab.getAttribute("usercontextid"),
   2550              });
   2551              return E10SUtils.getRemoteTypeForURI(
   2552                url,
   2553                gMultiProcessBrowser,
   2554                gFissionBrowser,
   2555                undefined,
   2556                uri,
   2557                oa
   2558              );
   2559            };
   2560            break;
   2561          case "userTypedValue":
   2562          case "userTypedClear":
   2563            getter = () => SessionStore.getLazyTabValue(aTab, name);
   2564            break;
   2565          default:
   2566            getter = () => {
   2567              if (AppConstants.NIGHTLY_BUILD) {
   2568                let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
   2569                Services.console.logStringMessage(message + new Error().stack);
   2570              }
   2571              this._insertBrowser(aTab);
   2572              return browser[name];
   2573            };
   2574            setter = value => {
   2575              if (AppConstants.NIGHTLY_BUILD) {
   2576                let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
   2577                Services.console.logStringMessage(message + new Error().stack);
   2578              }
   2579              this._insertBrowser(aTab);
   2580              return (browser[name] = value);
   2581            };
   2582        }
   2583        Object.defineProperty(browser, name, {
   2584          get: getter,
   2585          set: setter,
   2586          configurable: true,
   2587          enumerable: true,
   2588        });
   2589      }
   2590    }
   2591 
   2592    _insertBrowser(aTab, aInsertedOnTabCreation) {
   2593      "use strict";
   2594 
   2595      // If browser is already inserted or window is closed don't do anything.
   2596      if (aTab.linkedPanel || window.closed) {
   2597        return;
   2598      }
   2599 
   2600      let browser = aTab.linkedBrowser;
   2601 
   2602      // If browser is a lazy browser, delete the substitute properties.
   2603      if (this._browserBindingProperties[0] in browser) {
   2604        for (let name of this._browserBindingProperties) {
   2605          delete browser[name];
   2606        }
   2607      }
   2608 
   2609      let { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams;
   2610      delete aTab._browserParams;
   2611      delete browser._cachedCurrentURI;
   2612 
   2613      let panel = this.getPanel(browser);
   2614      let uniqueId = this._generateUniquePanelID();
   2615      panel.id = uniqueId;
   2616      aTab.linkedPanel = uniqueId;
   2617 
   2618      // Inject the <browser> into the DOM if necessary.
   2619      if (!panel.parentNode) {
   2620        // NB: this appendChild call causes us to run constructors for the
   2621        // browser element, which fires off a bunch of notifications. Some
   2622        // of those notifications can cause code to run that inspects our
   2623        // state, so it is important that the tab element is fully
   2624        // initialized by this point.
   2625        // AppendChild will cause a synchronous about:blank load.
   2626        this.tabpanels.appendChild(panel);
   2627      }
   2628 
   2629      // wire up a progress listener for the new browser object.
   2630      let tabListener = new TabProgressListener(
   2631        aTab,
   2632        browser,
   2633        uriIsAboutBlank,
   2634        usingPreloadedContent
   2635      );
   2636      const filter = Cc[
   2637        "@mozilla.org/appshell/component/browser-status-filter;1"
   2638      ].createInstance(Ci.nsIWebProgress);
   2639      filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
   2640      browser.webProgress.addProgressListener(
   2641        filter,
   2642        Ci.nsIWebProgress.NOTIFY_ALL
   2643      );
   2644      this._tabListeners.set(aTab, tabListener);
   2645      this._tabFilters.set(aTab, filter);
   2646 
   2647      browser.droppedLinkHandler = handleDroppedLink;
   2648      browser.loadURI = URILoadingWrapper.loadURI.bind(
   2649        URILoadingWrapper,
   2650        browser
   2651      );
   2652      browser.fixupAndLoadURIString =
   2653        URILoadingWrapper.fixupAndLoadURIString.bind(
   2654          URILoadingWrapper,
   2655          browser
   2656        );
   2657 
   2658      // Most of the time, we start our browser's docShells out as inactive,
   2659      // and then maintain activeness in the tab switcher. Preloaded about:newtab's
   2660      // are already created with their docShell's as inactive, but then explicitly
   2661      // render their layers to ensure that we can switch to them quickly. We avoid
   2662      // setting docShellIsActive to false again in this case, since that'd cause
   2663      // the layers for the preloaded tab to be dropped, and we'd see a flash
   2664      // of empty content instead.
   2665      //
   2666      // So for all browsers except for the preloaded case, we set the browser
   2667      // docShell to inactive.
   2668      if (!usingPreloadedContent) {
   2669        browser.docShellIsActive = false;
   2670      }
   2671 
   2672      // If we transitioned from one browser to two browsers, we need to set
   2673      // hasSiblings=false on both the existing browser and the new browser.
   2674      if (this.tabs.length == 2) {
   2675        this.tabs[0].linkedBrowser.browsingContext.hasSiblings = true;
   2676        this.tabs[1].linkedBrowser.browsingContext.hasSiblings = true;
   2677      } else {
   2678        aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
   2679      }
   2680 
   2681      if (aTab.userContextId) {
   2682        browser.setAttribute("usercontextid", aTab.userContextId);
   2683      }
   2684 
   2685      browser.browsingContext.isAppTab = aTab.pinned;
   2686 
   2687      // We don't want to update the container icon and identifier if
   2688      // this is not the selected browser.
   2689      if (aTab.selected) {
   2690        updateUserContextUIIndicator();
   2691      }
   2692 
   2693      // Only fire this event if the tab is already in the DOM
   2694      // and will be handled by a listener.
   2695      if (aTab.isConnected) {
   2696        var evt = new CustomEvent("TabBrowserInserted", {
   2697          bubbles: true,
   2698          detail: { insertedOnTabCreation: aInsertedOnTabCreation },
   2699        });
   2700        aTab.dispatchEvent(evt);
   2701      }
   2702    }
   2703 
   2704    _mayDiscardBrowser(aTab, aForceDiscard) {
   2705      let browser = aTab.linkedBrowser;
   2706      let action = aForceDiscard ? "unload" : "dontUnload";
   2707 
   2708      if (
   2709        !aTab ||
   2710        aTab.selected ||
   2711        aTab.closing ||
   2712        this._windowIsClosing ||
   2713        !browser.isConnected ||
   2714        !browser.isRemoteBrowser ||
   2715        !browser.permitUnload(action).permitUnload
   2716      ) {
   2717        return false;
   2718      }
   2719 
   2720      // discarding a browser will dismiss any dialogs, so don't
   2721      // allow this unless we're forcing it.
   2722      if (
   2723        !aForceDiscard &&
   2724        this.getTabDialogBox(browser)._tabDialogManager._dialogs.length
   2725      ) {
   2726        return false;
   2727      }
   2728 
   2729      return true;
   2730    }
   2731 
   2732    async prepareDiscardBrowser(aTab) {
   2733      let browser = aTab.linkedBrowser;
   2734      // This is similar to the checks in _mayDiscardBrowser, but
   2735      // doesn't have to be complete (and we want to be sure not to
   2736      // fire the beforeunload event). Calling TabStateFlusher.flush()
   2737      // and then not unloading the browser is fine.
   2738      if (aTab.closing || this._windowIsClosing || !browser.isRemoteBrowser) {
   2739        return;
   2740      }
   2741 
   2742      // Flush the tab's state so session restore has the latest data.
   2743      await this.TabStateFlusher.flush(browser);
   2744    }
   2745 
   2746    discardBrowser(aTab, aForceDiscard) {
   2747      "use strict";
   2748      let browser = aTab.linkedBrowser;
   2749 
   2750      if (!this._mayDiscardBrowser(aTab, aForceDiscard)) {
   2751        return false;
   2752      }
   2753 
   2754      // Reset sharing state.
   2755      if (aTab._sharingState) {
   2756        this.resetBrowserSharing(browser);
   2757      }
   2758      webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);
   2759 
   2760      // Abort any dialogs since the browser is about to be discarded.
   2761      let tabDialogBox = this.getTabDialogBox(browser);
   2762      tabDialogBox.abortAllDialogs();
   2763 
   2764      // Set browser parameters for when browser is restored.  Also remove
   2765      // listeners and set up lazy restore data in SessionStore. This must
   2766      // be done before browser is destroyed and removed from the document.
   2767      aTab._browserParams = {
   2768        uriIsAboutBlank: browser.currentURI.spec == "about:blank",
   2769        remoteType: browser.remoteType,
   2770        usingPreloadedContent: false,
   2771      };
   2772 
   2773      SessionStore.resetBrowserToLazyState(aTab);
   2774      // Indicate that this tab was explicitly unloaded (i.e. not
   2775      // from a session restore) in case we want to style that
   2776      // differently.
   2777      if (aForceDiscard) {
   2778        aTab.toggleAttribute("discarded", true);
   2779      }
   2780 
   2781      // Remove the tab's filter and progress listener.
   2782      let filter = this._tabFilters.get(aTab);
   2783      let listener = this._tabListeners.get(aTab);
   2784      browser.webProgress.removeProgressListener(filter);
   2785      filter.removeProgressListener(listener);
   2786      listener.destroy();
   2787 
   2788      this._tabListeners.delete(aTab);
   2789      this._tabFilters.delete(aTab);
   2790 
   2791      // Reset the findbar and remove it if it is attached to the tab.
   2792      if (aTab._findBar) {
   2793        aTab._findBar.close(true);
   2794        aTab._findBar.remove();
   2795        delete aTab._findBar;
   2796      }
   2797 
   2798      // Remove potentially stale attributes.
   2799      let attributesToRemove = [
   2800        "activemedia-blocked",
   2801        "busy",
   2802        "pendingicon",
   2803        "progress",
   2804        "soundplaying",
   2805      ];
   2806      let removedAttributes = [];
   2807      for (let attr of attributesToRemove) {
   2808        if (aTab.hasAttribute(attr)) {
   2809          removedAttributes.push(attr);
   2810          aTab.removeAttribute(attr);
   2811        }
   2812      }
   2813      if (removedAttributes.length) {
   2814        this._tabAttrModified(aTab, removedAttributes);
   2815      }
   2816 
   2817      browser.destroy();
   2818      this.getPanel(browser).remove();
   2819      aTab.removeAttribute("linkedpanel");
   2820 
   2821      this._createLazyBrowser(aTab);
   2822 
   2823      let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
   2824      aTab.dispatchEvent(evt);
   2825      return true;
   2826    }
   2827 
   2828    /**
   2829     * Loads a tab with a default null principal unless specified
   2830     *
   2831     * @param {string} aURI
   2832     * @param {object} [params]
   2833     *   Options from `Tabbrowser.addTab`.
   2834     */
   2835    addWebTab(aURI, params = {}) {
   2836      if (!params.triggeringPrincipal) {
   2837        params.triggeringPrincipal =
   2838          Services.scriptSecurityManager.createNullPrincipal({
   2839            userContextId: params.userContextId,
   2840          });
   2841      }
   2842      if (params.triggeringPrincipal.isSystemPrincipal) {
   2843        throw new Error(
   2844          "System principal should never be passed into addWebTab()"
   2845        );
   2846      }
   2847      return this.addTab(aURI, params);
   2848    }
   2849 
   2850    /**
   2851     * @param {MozTabbrowserTab} tab
   2852     * @returns {void}
   2853     *   New tab will be the `Tabbrowser.selectedTab` or the subject of a
   2854     *   notification on the `browser-open-newtab-start` topic.
   2855     */
   2856    addAdjacentNewTab(tab) {
   2857      Services.obs.notifyObservers(
   2858        {
   2859          wrappedJSObject: new Promise(resolve => {
   2860            this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
   2861              tabIndex: tab._tPos + 1,
   2862              userContextId: tab.userContextId,
   2863              tabGroup: tab.group,
   2864              focusUrlBar: true,
   2865            });
   2866            resolve(this.selectedBrowser);
   2867          }),
   2868        },
   2869        "browser-open-newtab-start"
   2870      );
   2871    }
   2872 
   2873    /**
   2874     * Creates a tab directly after `tab`.
   2875     *
   2876     * @param {MozTabbrowserTab} adjacentTab
   2877     * @param {string} uriString
   2878     * @param {object} [options]
   2879     *   Options from `Tabbrowser.addTab`.
   2880     */
   2881    addAdjacentTab(adjacentTab, uriString, options = {}) {
   2882      // If the caller opted out of tab group membership but `tab` is in a
   2883      // tab group, insert the tab after `tab`'s group. Otherwise, insert the
   2884      // new tab right after `tab`.
   2885      const tabIndex =
   2886        !options.tabGroup && adjacentTab.group
   2887          ? adjacentTab.group.tabs.at(-1)._tPos + 1
   2888          : adjacentTab._tPos + 1;
   2889 
   2890      return this.addTab(uriString, {
   2891        ...options,
   2892        tabIndex,
   2893      });
   2894    }
   2895 
   2896    /**
   2897     * Must only be used sparingly for content that came from Chrome context
   2898     * If in doubt use addWebTab
   2899     *
   2900     * @param {string} aURI
   2901     * @param {object} [options]
   2902     * @see this.addTab options
   2903     * @returns {MozTabbrowserTab|null}
   2904     */
   2905    addTrustedTab(aURI, options = {}) {
   2906      options.triggeringPrincipal =
   2907        Services.scriptSecurityManager.getSystemPrincipal();
   2908      return this.addTab(aURI, options);
   2909    }
   2910 
   2911    /**
   2912     * @param {string} uriString
   2913     * @param {object} options
   2914     * @param {object} [options.eventDetail]
   2915     *   Additional information to include in the `CustomEvent.detail`
   2916     *   of the resulting TabOpen event.
   2917     * @param {boolean} [options.fromExternal]
   2918     *   Whether this tab was opened from a URL supplied to Firefox from an
   2919     *   external application.
   2920     * @param {MozTabbrowserTabGroup} [options.tabGroup]
   2921     *   A related tab group where this tab should be added, when applicable.
   2922     *   When present, the tab is expected to reside in this tab group. When
   2923     *   absent, the tab is expected to be a standalone tab.
   2924     * @returns {MozTabbrowserTab|null}
   2925     *    The new tab. The return value will be null if the tab couldn't be
   2926     *    created; this shouldn't normally happen, and an error will be logged
   2927     *    to the console if it does.
   2928     */
   2929    addTab(
   2930      uriString,
   2931      {
   2932        allowInheritPrincipal,
   2933        allowThirdPartyFixup,
   2934        bulkOrderedOpen,
   2935        charset,
   2936        createLazyBrowser,
   2937        eventDetail,
   2938        focusUrlBar,
   2939        forceNotRemote,
   2940        forceAllowDataURI,
   2941        fromExternal,
   2942        inBackground = true,
   2943        isCaptivePortalTab,
   2944        elementIndex,
   2945        tabIndex,
   2946        lazyTabTitle,
   2947        name,
   2948        noInitialLabel,
   2949        openWindowInfo,
   2950        openerBrowser,
   2951        originPrincipal,
   2952        originStoragePrincipal,
   2953        ownerTab,
   2954        pinned,
   2955        postData,
   2956        preferredRemoteType,
   2957        referrerInfo,
   2958        relatedToCurrent,
   2959        initialBrowsingContextGroupId,
   2960        skipAnimation,
   2961        skipBackgroundNotify,
   2962        tabGroup,
   2963        triggeringPrincipal,
   2964        userContextId,
   2965        policyContainer,
   2966        skipLoad = createLazyBrowser,
   2967        insertTab = true,
   2968        globalHistoryOptions,
   2969        triggeringRemoteType,
   2970        schemelessInput,
   2971        hasValidUserGestureActivation = false,
   2972        textDirectiveUserActivation = false,
   2973      } = {}
   2974    ) {
   2975      // all callers of addTab that pass a params object need to pass
   2976      // a valid triggeringPrincipal.
   2977      if (!triggeringPrincipal) {
   2978        throw new Error(
   2979          "Required argument triggeringPrincipal missing within addTab"
   2980        );
   2981      }
   2982 
   2983      if (!UserInteraction.running("browser.tabs.opening", window)) {
   2984        UserInteraction.start("browser.tabs.opening", "initting", window);
   2985      }
   2986 
   2987      // If we're opening a foreground tab, set the owner by default.
   2988      ownerTab ??= inBackground ? null : this.selectedTab;
   2989 
   2990      // if we're adding tabs, we're past interrupt mode, ditch the owner
   2991      if (this.selectedTab.owner) {
   2992        this.selectedTab.owner = null;
   2993      }
   2994 
   2995      // Find the tab that opened this one, if any. This is used for
   2996      // determining positioning, and inherited attributes such as the
   2997      // user context ID.
   2998      //
   2999      // If we have a browser opener (which is usually the browser
   3000      // element from a remote window.open() call), use that.
   3001      //
   3002      // Otherwise, if the tab is related to the current tab (e.g.,
   3003      // because it was opened by a link click), use the selected tab as
   3004      // the owner. If referrerInfo is set, and we don't have an
   3005      // explicit relatedToCurrent arg, we assume that the tab is
   3006      // related to the current tab, since referrerURI is null or
   3007      // undefined if the tab is opened from an external application or
   3008      // bookmark (i.e. somewhere other than an existing tab).
   3009      if (relatedToCurrent == null) {
   3010        relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer);
   3011      }
   3012      let openerTab =
   3013        (openerBrowser && this.getTabForBrowser(openerBrowser)) ||
   3014        (relatedToCurrent && this.selectedTab) ||
   3015        null;
   3016 
   3017      // When overflowing, new tabs are scrolled into view smoothly, which
   3018      // doesn't go well together with the width transition. So we skip the
   3019      // transition in that case.
   3020      let animate =
   3021        !skipAnimation &&
   3022        !pinned &&
   3023        !this.tabContainer.verticalMode &&
   3024        !this.tabContainer.overflowing &&
   3025        !gReduceMotion;
   3026 
   3027      let uriInfo = this._determineURIToLoad(uriString, createLazyBrowser);
   3028      let { uri, uriIsAboutBlank, lazyBrowserURI } = uriInfo;
   3029      // Have to overwrite this if we're lazy-loading. Should go away
   3030      // with bug 1818777.
   3031      ({ uriString } = uriInfo);
   3032 
   3033      let usingPreloadedContent = false;
   3034      let b, t;
   3035 
   3036      try {
   3037        t = this._createTab({
   3038          uriString,
   3039          animate,
   3040          userContextId,
   3041          openerTab,
   3042          pinned,
   3043          noInitialLabel,
   3044          skipBackgroundNotify,
   3045        });
   3046        if (insertTab) {
   3047          // Insert the tab into the tab container in the correct position.
   3048          this.#insertTabAtIndex(t, {
   3049            elementIndex,
   3050            tabIndex,
   3051            ownerTab,
   3052            openerTab,
   3053            pinned,
   3054            bulkOrderedOpen,
   3055            tabGroup: tabGroup ?? openerTab?.group,
   3056          });
   3057        }
   3058 
   3059        ({ browser: b, usingPreloadedContent } = this._createBrowserForTab(t, {
   3060          uriString,
   3061          uri,
   3062          preferredRemoteType,
   3063          openerBrowser,
   3064          uriIsAboutBlank,
   3065          referrerInfo,
   3066          forceNotRemote,
   3067          name,
   3068          initialBrowsingContextGroupId,
   3069          openWindowInfo,
   3070          skipLoad,
   3071          triggeringRemoteType,
   3072        }));
   3073 
   3074        if (focusUrlBar) {
   3075          gURLBar.getBrowserState(b).urlbarFocused = true;
   3076        }
   3077 
   3078        // If the caller opts in, create a lazy browser.
   3079        if (createLazyBrowser) {
   3080          this._createLazyBrowser(t);
   3081 
   3082          if (lazyBrowserURI) {
   3083            // Lazy browser must be explicitly registered so tab will appear as
   3084            // a switch-to-tab candidate in autocomplete.
   3085            this.UrlbarProviderOpenTabs.registerOpenTab(
   3086              lazyBrowserURI.spec,
   3087              t.userContextId,
   3088              tabGroup?.id,
   3089              PrivateBrowsingUtils.isWindowPrivate(window)
   3090            );
   3091            b.registeredOpenURI = lazyBrowserURI;
   3092          }
   3093          // If we're not inserting the tab into the DOM, we can't set the tab
   3094          // state meaningfully. Session restore (the only caller who does this)
   3095          // will have to do this work itself later, when the tabs have been
   3096          // inserted.
   3097          if (insertTab) {
   3098            SessionStore.setTabState(t, {
   3099              entries: [
   3100                {
   3101                  url: lazyBrowserURI?.spec || "about:blank",
   3102                  title: lazyTabTitle,
   3103                  triggeringPrincipal_base64:
   3104                    E10SUtils.serializePrincipal(triggeringPrincipal),
   3105                },
   3106              ],
   3107              // Make sure to store the userContextId associated to the lazy tab
   3108              // otherwise it would be created as a default tab when recreated on a
   3109              // session restore (See Bug 1819794).
   3110              userContextId,
   3111            });
   3112          }
   3113        } else {
   3114          this._insertBrowser(t, true);
   3115          // If we were called by frontend and don't have openWindowInfo,
   3116          // but we were opened from another browser, set the cross group
   3117          // opener ID:
   3118          if (openerBrowser?.browsingContext && !openWindowInfo) {
   3119            b.browsingContext.crossGroupOpener = openerBrowser.browsingContext;
   3120          }
   3121        }
   3122      } catch (e) {
   3123        console.error("Failed to create tab");
   3124        console.error(e);
   3125        t?.remove();
   3126        if (t?.linkedBrowser) {
   3127          this._tabFilters.delete(t);
   3128          this._tabListeners.delete(t);
   3129          this.getPanel(t.linkedBrowser).remove();
   3130        }
   3131        return null;
   3132      }
   3133 
   3134      if (insertTab) {
   3135        // Fire a TabOpen event
   3136        const tabOpenDetail = {
   3137          ...eventDetail,
   3138          fromExternal,
   3139        };
   3140        this._fireTabOpen(t, tabOpenDetail);
   3141 
   3142        this._kickOffBrowserLoad(b, {
   3143          uri,
   3144          uriString,
   3145          usingPreloadedContent,
   3146          triggeringPrincipal,
   3147          originPrincipal,
   3148          originStoragePrincipal,
   3149          uriIsAboutBlank,
   3150          allowInheritPrincipal,
   3151          allowThirdPartyFixup,
   3152          fromExternal,
   3153          forceAllowDataURI,
   3154          isCaptivePortalTab,
   3155          skipLoad: skipLoad || uriIsAboutBlank,
   3156          referrerInfo,
   3157          charset,
   3158          postData,
   3159          policyContainer,
   3160          globalHistoryOptions,
   3161          triggeringRemoteType,
   3162          schemelessInput,
   3163          hasValidUserGestureActivation:
   3164            hasValidUserGestureActivation ||
   3165            !!openWindowInfo?.hasValidUserGestureActivation,
   3166          textDirectiveUserActivation:
   3167            textDirectiveUserActivation ||
   3168            !!openWindowInfo?.textDirectiveUserActivation,
   3169        });
   3170      }
   3171 
   3172      // This field is updated regardless if we actually animate
   3173      // since it's important that we keep this count correct in all cases.
   3174      this.tabAnimationsInProgress++;
   3175 
   3176      if (animate) {
   3177        // Kick the animation off.
   3178        // TODO: we should figure out a better solution here. We use RAF
   3179        // to avoid jank of the animation due to synchronous work happening
   3180        // on tab open.
   3181        // With preloaded content though a single RAF happens too early. and
   3182        // both the transition and the transitionend event don't happen.
   3183        if (usingPreloadedContent) {
   3184          requestAnimationFrame(() => {
   3185            requestAnimationFrame(() => {
   3186              t.setAttribute("fadein", "true");
   3187            });
   3188          });
   3189        } else {
   3190          requestAnimationFrame(() => {
   3191            t.setAttribute("fadein", "true");
   3192          });
   3193        }
   3194      }
   3195 
   3196      // Additionally send pinned tab events
   3197      if (pinned) {
   3198        this.#notifyPinnedStatus(t);
   3199      }
   3200 
   3201      gSharedTabWarning.tabAdded(t);
   3202 
   3203      if (!inBackground) {
   3204        this.selectedTab = t;
   3205      }
   3206      return t;
   3207    }
   3208 
   3209    #elementIndexToTabIndex(elementIndex) {
   3210      if (elementIndex < 0) {
   3211        return -1;
   3212      }
   3213      if (elementIndex >= this.tabContainer.dragAndDropElements.length) {
   3214        return this.tabs.length;
   3215      }
   3216      let element = this.tabContainer.dragAndDropElements[elementIndex];
   3217      if (this.isTabGroupLabel(element)) {
   3218        element = element.group.tabs[0];
   3219      }
   3220      if (this.isSplitViewWrapper(element)) {
   3221        element = element.tabs[0];
   3222      }
   3223      return element._tPos;
   3224    }
   3225 
   3226    /**
   3227     * @param {string} id
   3228     * @returns {MozTabSplitViewWrapper}
   3229     */
   3230    _createTabSplitView(id) {
   3231      let splitview = document.createXULElement("tab-split-view-wrapper", {
   3232        is: "tab-split-view-wrapper",
   3233      });
   3234      splitview.splitViewId = id;
   3235      return splitview;
   3236    }
   3237 
   3238    /**
   3239     * Adds a new tab split view.
   3240     *
   3241     * @param {object[]} tabs
   3242     *   The set of tabs to include in the split view.
   3243     * @param {object} [options]
   3244     * @param {string} [options.id]
   3245     *   Optionally assign an ID to the split view. Useful when rebuilding an
   3246     *   existing splitview e.g. when restoring. A pseudorandom string will be
   3247     *   generated if not set.
   3248     * @param {MozTabbrowserTab} [options.insertBefore]
   3249     *   An optional argument that accepts a single tab, which, if passed, will
   3250     *   cause the split view to be inserted just before this tab in the tab strip. By
   3251     *   default, the split view will be created at the end of the tab strip.
   3252     */
   3253    addTabSplitView(tabs, { id = null, insertBefore = null } = {}) {
   3254      if (!tabs?.length) {
   3255        throw new Error("Cannot create split view with zero tabs");
   3256      }
   3257 
   3258      if (!id) {
   3259        id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
   3260      }
   3261      let splitview = this._createTabSplitView(id);
   3262      this.tabContainer.insertBefore(
   3263        splitview,
   3264        insertBefore?.splitview ?? insertBefore
   3265      );
   3266      splitview.addTabs(tabs);
   3267 
   3268      // Bail out if the split view is empty at this point. This can happen if all
   3269      // provided tabs are pinned and therefore cannot be grouped.
   3270      if (!splitview.tabs.length) {
   3271        splitview.remove();
   3272        return null;
   3273      }
   3274 
   3275      this.tabContainer.dispatchEvent(
   3276        new CustomEvent("SplitViewCreated", {
   3277          bubbles: true,
   3278        })
   3279      );
   3280      return splitview;
   3281    }
   3282 
   3283    /**
   3284     * Removes all tabs from a split view wrapper. This also removes the split view wrapper component
   3285     *
   3286     * @param {MozTabSplitViewWrapper} [splitView]
   3287     *   The split view to remove.
   3288     */
   3289    unsplitTabs(splitview) {
   3290      if (!splitview) {
   3291        return;
   3292      }
   3293 
   3294      gBrowser.setIsSplitViewActive(false, splitview.tabs);
   3295 
   3296      for (let i = splitview.tabs.length - 1; i >= 0; i--) {
   3297        this.#handleTabMove(splitview.tabs[i], () =>
   3298          gBrowser.tabContainer.insertBefore(
   3299            splitview.tabs[i],
   3300            splitview.nextElementSibling
   3301          )
   3302        );
   3303      }
   3304 
   3305      splitview.remove();
   3306    }
   3307 
   3308    /**
   3309     * Show the list of tabs <browsers> that are part of a split view.
   3310     *
   3311     * @param {MozTabbrowserTab[]} tabs
   3312     */
   3313    showSplitViewPanels(tabs) {
   3314      const panels = [];
   3315      for (const tab of tabs) {
   3316        this._insertBrowser(tab);
   3317        this.#insertSplitViewFooter(tab);
   3318        tab.linkedBrowser.docShellIsActive = true;
   3319        panels.push(tab.linkedPanel);
   3320      }
   3321      this.tabpanels.splitViewPanels = panels;
   3322    }
   3323 
   3324    /**
   3325     * Hide the list of tabs <browsers> that are part of a split view.
   3326     *
   3327     * @param {MozTabbrowserTab[]} tabs
   3328     */
   3329    hideSplitViewPanels(tabs) {
   3330      for (const tab of tabs) {
   3331        this.tabpanels.removePanelFromSplitView(tab.linkedPanel);
   3332      }
   3333    }
   3334 
   3335    /**
   3336     * Toggle split view active attribute
   3337     *
   3338     * @param {boolean} isActive
   3339     * @param {MozTabbrowserTab[]} tabs
   3340     */
   3341    setIsSplitViewActive(isActive, tabs) {
   3342      for (const tab of tabs) {
   3343        this.tabpanels.setSplitViewPanelActive(isActive, tab.linkedPanel);
   3344      }
   3345      this.tabpanels.isSplitViewActive = gBrowser.selectedTab.splitview;
   3346    }
   3347 
   3348    /**
   3349     * Ensures the split view footer exists for the given tab.
   3350     *
   3351     * @param {MozTabbrowserTab} tab
   3352     */
   3353    #insertSplitViewFooter(tab) {
   3354      const panelEl = document.getElementById(tab.linkedPanel);
   3355      if (panelEl?.querySelector("split-view-footer")) {
   3356        return;
   3357      }
   3358      if (panelEl) {
   3359        const footer = document.createXULElement("split-view-footer");
   3360        footer.setTab(tab);
   3361        panelEl.querySelector(".browserStack").appendChild(footer);
   3362      }
   3363    }
   3364 
   3365    openSplitViewMenu(anchorElement) {
   3366      const menu = document.getElementById("split-view-menu");
   3367      menu.openPopup(anchorElement, "after_start");
   3368    }
   3369 
   3370    /**
   3371     * @param {string} id
   3372     * @param {string} color
   3373     * @param {boolean} collapsed
   3374     * @param {string} [label=]
   3375     * @param {boolean} [isAdoptingGroup=false]
   3376     * @returns {MozTabbrowserTabGroup}
   3377     */
   3378    _createTabGroup(id, color, collapsed, label = "", isAdoptingGroup = false) {
   3379      let group = document.createXULElement("tab-group", { is: "tab-group" });
   3380      group.id = id;
   3381      group.collapsed = collapsed;
   3382      group.color = color;
   3383      group.label = label;
   3384      group.wasCreatedByAdoption = isAdoptingGroup;
   3385      return group;
   3386    }
   3387 
   3388    /**
   3389     * Adds a new tab group.
   3390     *
   3391     * @param {object[]} tabs
   3392     *   The set of tabs or split view to include in the group.
   3393     * @param {object} [options]
   3394     * @param {string} [options.id]
   3395     *   Optionally assign an ID to the tab group. Useful when rebuilding an
   3396     *   existing group e.g. when restoring. A pseudorandom string will be
   3397     *   generated if not set.
   3398     * @param {string} [options.color]
   3399     *   Color for the group label. See tabgroup-menu.js for possible values.
   3400     *   If no color specified, will attempt to assign an unused group color.
   3401     * @param {string} [options.label]
   3402     *   Label for the group.
   3403     * @param {MozTabbrowserTab} [options.insertBefore]
   3404     *   An optional argument that accepts a single tab, which, if passed, will
   3405     *   cause the group to be inserted just before this tab in the tab strip. By
   3406     *   default, the group will be created at the end of the tab strip.
   3407     * @param {boolean} [options.isAdoptingGroup]
   3408     *   Whether the tab group was created because a tab group with the same
   3409     *   properties is being adopted from a different window.
   3410     * @param {boolean} [options.isUserTriggered]
   3411     *   Should be true if this group is being created in response to an
   3412     *   explicit request from the user (as opposed to a group being created
   3413     *   for technical reasons, such as when an already existing group
   3414     *   switches windows).
   3415     *   Causes the group create UI to be displayed and telemetry events to be fired.
   3416     * @param {string} [options.telemetryUserCreateSource]
   3417     *   The means by which the tab group was created.
   3418     *   @see TabMetrics.METRIC_SOURCE for possible values.
   3419     *   Defaults to "unknown".
   3420     */
   3421    addTabGroup(
   3422      tabsAndSplitViews,
   3423      {
   3424        id = null,
   3425        color = null,
   3426        label = "",
   3427        insertBefore = null,
   3428        isAdoptingGroup = false,
   3429        isUserTriggered = false,
   3430        telemetryUserCreateSource = "unknown",
   3431      } = {}
   3432    ) {
   3433      if (
   3434        !tabsAndSplitViews?.length ||
   3435        tabsAndSplitViews.some(
   3436          tabOrSplitView =>
   3437            !this.isTab(tabOrSplitView) &&
   3438            !this.isSplitViewWrapper(tabOrSplitView)
   3439        )
   3440      ) {
   3441        throw new Error(
   3442          "Cannot create tab group with zero tabs or split views"
   3443        );
   3444      }
   3445 
   3446      if (!color) {
   3447        color = this.tabGroupMenu.nextUnusedColor;
   3448      }
   3449 
   3450      if (!id) {
   3451        // Note: If this changes, make sure to also update the
   3452        // getExtTabGroupIdForInternalTabGroupId implementation in
   3453        // browser/components/extensions/parent/ext-browser.js.
   3454        // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
   3455        id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
   3456      }
   3457      let group = this._createTabGroup(
   3458        id,
   3459        color,
   3460        false,
   3461        label,
   3462        isAdoptingGroup
   3463      );
   3464      this.tabContainer.insertBefore(
   3465        group,
   3466        insertBefore?.group ?? insertBefore
   3467      );
   3468      group.addTabs(tabsAndSplitViews);
   3469 
   3470      // Bail out if the group is empty at this point. This can happen if all
   3471      // provided tabs are pinned and therefore cannot be grouped.
   3472      if (!group.tabs.length) {
   3473        group.remove();
   3474        return null;
   3475      }
   3476 
   3477      if (isUserTriggered) {
   3478        group.dispatchEvent(
   3479          new CustomEvent("TabGroupCreateByUser", {
   3480            bubbles: true,
   3481            detail: {
   3482              telemetryUserCreateSource,
   3483            },
   3484          })
   3485        );
   3486      }
   3487 
   3488      // Fixes bug1953801 and bug1954689
   3489      // Ensure that the tab state cache is updated immediately after creating
   3490      // a group. This is necessary because we consider group creation a
   3491      // deliberate user action indicating the tab has importance for the user.
   3492      // Without this, it is not possible to save and close a tab group with
   3493      // a short lifetime.
   3494      group.tabs.forEach(tab => {
   3495        this.TabStateFlusher.flush(tab.linkedBrowser);
   3496      });
   3497 
   3498      return group;
   3499    }
   3500 
   3501    /**
   3502     * Removes the tab group. This has the effect of closing all the tabs
   3503     * in the group.
   3504     *
   3505     * @param {MozTabbrowserTabGroup} [group]
   3506     *   The tab group to remove.
   3507     * @param {object} [options]
   3508     *   Options to use when removing tabs. @see removeTabs for more info.
   3509     * @param {boolean} [options.isUserTriggered=false]
   3510     *   Should be true if this group is being removed by an explicit
   3511     *   request from the user (as opposed to a group being removed
   3512     *   for technical reasons, such as when an already existing group
   3513     *   switches windows). This causes telemetry events to fire.
   3514     * @param {string} [options.telemetrySource="unknown"]
   3515     *   The means by which the tab group was removed.
   3516     *   @see TabMetrics.METRIC_SOURCE for possible values.
   3517     *   Defaults to "unknown".
   3518     */
   3519    async removeTabGroup(
   3520      group,
   3521      options = {
   3522        isUserTriggered: false,
   3523        telemetrySource: this.TabMetrics.METRIC_SOURCE.UNKNOWN,
   3524      }
   3525    ) {
   3526      if (this.tabGroupMenu.panel.state != "closed") {
   3527        this.tabGroupMenu.panel.hidePopup(options.animate);
   3528      }
   3529 
   3530      if (!options.skipPermitUnload) {
   3531        // Process permit unload handlers and allow user cancel
   3532        let cancel = await this.runBeforeUnloadForTabs(group.tabs);
   3533        if (cancel) {
   3534          if (SessionStore.getSavedTabGroup(group.id)) {
   3535            // If this group is currently saved, it's being removed as part of a
   3536            // save & close operation. We need to forget the saved group
   3537            // if the close is canceled.
   3538            SessionStore.forgetSavedTabGroup(group.id);
   3539          }
   3540          return;
   3541        }
   3542        options.skipPermitUnload = true;
   3543      }
   3544 
   3545      if (group.tabs.length == this.tabs.length) {
   3546        // explicit calls to removeTabGroup are not expected to save groups.
   3547        // if removing this group closes a window, we need to tell the window
   3548        // not to save the group.
   3549        group.saveOnWindowClose = false;
   3550      }
   3551 
   3552      // This needs to be fired before tabs are removed because session store
   3553      // needs to respond to this while tabs are still part of the group
   3554      group.dispatchEvent(
   3555        new CustomEvent("TabGroupRemoveRequested", {
   3556          bubbles: true,
   3557          detail: {
   3558            skipSessionStore: options.skipSessionStore,
   3559            isUserTriggered: options.isUserTriggered,
   3560            telemetrySource: options.telemetrySource,
   3561          },
   3562        })
   3563      );
   3564 
   3565      // Skip session store on a per-tab basis since these tabs will get
   3566      // recorded as part of a group
   3567      options.skipSessionStore = true;
   3568 
   3569      // tell removeTabs not to subprocess groups since we're removing a group.
   3570      options.skipGroupCheck = true;
   3571 
   3572      this.removeTabs(group.tabs, options);
   3573    }
   3574 
   3575    /**
   3576     * Removes a tab from a group. This has no effect on tabs that are not
   3577     * already in a group.
   3578     *
   3579     * @param tab The tab to ungroup
   3580     */
   3581    ungroupTab(tab) {
   3582      if (!tab.group) {
   3583        return;
   3584      }
   3585 
   3586      this.#handleTabMove(tab, () =>
   3587        gBrowser.tabContainer.insertBefore(tab, tab.group.nextElementSibling)
   3588      );
   3589    }
   3590 
   3591    ungroupSplitView(splitView) {
   3592      if (!this.isSplitViewWrapper(splitView)) {
   3593        return;
   3594      }
   3595 
   3596      this.#handleTabMove(splitView, () =>
   3597        gBrowser.tabContainer.insertBefore(
   3598          splitView,
   3599          splitView.tabs[0].group.nextElementSibling
   3600        )
   3601      );
   3602    }
   3603 
   3604    /**
   3605     * @param {MozTabbrowserTabGroup} group
   3606     * @param {object} [options]
   3607     * @param {number} [options.elementIndex]
   3608     * @param {number} [options.tabIndex]
   3609     * @param {boolean} [options.selectTab]
   3610     * @returns {MozTabbrowserTabGroup}
   3611     */
   3612    adoptTabGroup(group, { elementIndex, tabIndex, selectTab } = {}) {
   3613      if (group.ownerDocument == document) {
   3614        return group;
   3615      }
   3616      group.removedByAdoption = true;
   3617      group.saveOnWindowClose = false;
   3618 
   3619      let oldSelectedTab = selectTab && group.ownerGlobal.gBrowser.selectedTab;
   3620      let newTabs = [];
   3621      let adoptedTab;
   3622      let splitview;
   3623 
   3624      // bug1969925 adopting a tab group will cause the window to close if it
   3625      // is the only thing on the tab strip
   3626      // In this case, the `TabUngrouped` event will not fire, so we have to do it manually
   3627      let noOtherTabsInWindow = group.ownerGlobal.gBrowser.nonHiddenTabs.every(
   3628        t => t.group == group
   3629      );
   3630 
   3631      // We dispatch this event in a separate for loop because the tab extension API
   3632      // expects event.detail to be a tab.
   3633      if (noOtherTabsInWindow) {
   3634        for (let element of group.tabs) {
   3635          group.dispatchEvent(
   3636            new CustomEvent("TabUngrouped", {
   3637              bubbles: true,
   3638              detail: element,
   3639            })
   3640          );
   3641        }
   3642      }
   3643 
   3644      for (let element of group.tabsAndSplitViews) {
   3645        if (element.tagName == "tab-split-view-wrapper") {
   3646          splitview = this.adoptSplitView(element, {
   3647            elementIndex,
   3648            tabIndex,
   3649          });
   3650          newTabs.push(splitview);
   3651          tabIndex = splitview.tabs[0]._tPos + splitview.tabs.length;
   3652        } else {
   3653          adoptedTab = this.adoptTab(element, {
   3654            elementIndex,
   3655            tabIndex,
   3656            selectTab: element === oldSelectedTab,
   3657          });
   3658          newTabs.push(adoptedTab);
   3659          // Put next tab after current one.
   3660          elementIndex = undefined;
   3661          tabIndex = adoptedTab._tPos + 1;
   3662        }
   3663      }
   3664 
   3665      return this.addTabGroup(newTabs, {
   3666        id: group.id,
   3667        label: group.label,
   3668        color: group.color,
   3669        insertBefore: newTabs[0],
   3670        isAdoptingGroup: true,
   3671      });
   3672    }
   3673 
   3674    /**
   3675     * @param {MozSplitViewWrapper} container
   3676     * @param {object} [options]
   3677     * @param {number} [options.elementIndex]
   3678     * @param {number} [options.tabIndex]
   3679     * @param {boolean} [options.selectTab]
   3680     * @returns {MozSplitViewWrapper}
   3681     */
   3682    adoptSplitView(container, { elementIndex, tabIndex } = {}) {
   3683      if (container.ownerDocument == document) {
   3684        return container;
   3685      }
   3686 
   3687      let newTabs = [];
   3688 
   3689      if (!tabIndex) {
   3690        tabIndex = this.#elementIndexToTabIndex(elementIndex);
   3691      }
   3692 
   3693      for (let tab of container.tabs) {
   3694        let adoptedTab = this.adoptTab(tab, {
   3695          tabIndex,
   3696        });
   3697        newTabs.push(adoptedTab);
   3698        tabIndex = adoptedTab._tPos + 1;
   3699      }
   3700 
   3701      return this.addTabSplitView(newTabs, {
   3702        id: container.splitViewId,
   3703        insertBefore: newTabs[0],
   3704      });
   3705    }
   3706 
   3707    /**
   3708     * Get all open tab groups from all windows. Does not include saved groups.
   3709     *
   3710     * @param {object} [options]
   3711     * @param {boolean} [options.sortByLastSeenActive]
   3712     *   Sort groups so that groups that have more recently seen and active
   3713     *   tabs appear first. Defaults to false.
   3714     */
   3715    getAllTabGroups({ sortByLastSeenActive = false } = {}) {
   3716      let groups = BrowserWindowTracker.getOrderedWindows({
   3717        private: PrivateBrowsingUtils.isWindowPrivate(window),
   3718      }).reduce(
   3719        (acc, thisWindow) => acc.concat(thisWindow.gBrowser.tabGroups),
   3720        []
   3721      );
   3722      if (sortByLastSeenActive) {
   3723        groups.sort(
   3724          (group1, group2) => group2.lastSeenActive - group1.lastSeenActive
   3725        );
   3726      }
   3727      return groups;
   3728    }
   3729 
   3730    getTabGroupById(id) {
   3731      for (const win of BrowserWindowTracker.getOrderedWindows({
   3732        private: PrivateBrowsingUtils.isWindowPrivate(window),
   3733      })) {
   3734        for (const group of win.gBrowser.tabGroups) {
   3735          if (group.id === id) {
   3736            return group;
   3737          }
   3738        }
   3739      }
   3740      return null;
   3741    }
   3742 
   3743    _determineURIToLoad(uriString, createLazyBrowser) {
   3744      uriString = uriString || "about:blank";
   3745      let aURIObject = null;
   3746      try {
   3747        aURIObject = Services.io.newURI(uriString);
   3748      } catch (ex) {
   3749        /* we'll try to fix up this URL later */
   3750      }
   3751 
   3752      let lazyBrowserURI;
   3753      if (createLazyBrowser && uriString != "about:blank") {
   3754        lazyBrowserURI = aURIObject;
   3755        uriString = "about:blank";
   3756      }
   3757 
   3758      let uriIsAboutBlank = uriString == "about:blank";
   3759      return { uri: aURIObject, uriIsAboutBlank, lazyBrowserURI, uriString };
   3760    }
   3761 
   3762    /**
   3763     * @param {object} options
   3764     * @returns {MozTabbrowserTab}
   3765     */
   3766    _createTab({
   3767      uriString,
   3768      userContextId,
   3769      openerTab,
   3770      pinned,
   3771      noInitialLabel,
   3772      skipBackgroundNotify,
   3773      animate,
   3774    }) {
   3775      var t = document.createXULElement("tab", { is: "tabbrowser-tab" });
   3776      // Tag the tab as being created so extension code can ignore events
   3777      // prior to TabOpen.
   3778      t.initializingTab = true;
   3779      t.openerTab = openerTab;
   3780 
   3781      // Related tab inherits current tab's user context unless a different
   3782      // usercontextid is specified
   3783      if (userContextId == null && openerTab) {
   3784        userContextId = openerTab.getAttribute("usercontextid") || 0;
   3785      }
   3786 
   3787      if (!noInitialLabel) {
   3788        if (isBlankPageURL(uriString)) {
   3789          t.setAttribute("label", this.tabContainer.emptyTabTitle);
   3790        } else {
   3791          // Set URL as label so that the tab isn't empty initially.
   3792          this.setInitialTabTitle(t, uriString, {
   3793            beforeTabOpen: true,
   3794            isURL: true,
   3795          });
   3796        }
   3797      }
   3798 
   3799      if (userContextId) {
   3800        t.setAttribute("usercontextid", userContextId);
   3801        ContextualIdentityService.setTabStyle(t);
   3802      }
   3803 
   3804      if (skipBackgroundNotify) {
   3805        t.setAttribute("skipbackgroundnotify", true);
   3806      }
   3807 
   3808      if (pinned) {
   3809        t.setAttribute("pinned", "true");
   3810      }
   3811 
   3812      t.classList.add("tabbrowser-tab");
   3813 
   3814      this.tabContainer._unlockTabSizing();
   3815 
   3816      if (!animate) {
   3817        UserInteraction.update("browser.tabs.opening", "not-animated", window);
   3818        t.setAttribute("fadein", "true");
   3819 
   3820        // Call _handleNewTab asynchronously as it needs to know if the
   3821        // new tab is selected.
   3822        setTimeout(
   3823          function (tabContainer) {
   3824            tabContainer._handleNewTab(t);
   3825          },
   3826          0,
   3827          this.tabContainer
   3828        );
   3829      } else {
   3830        UserInteraction.update("browser.tabs.opening", "animated", window);
   3831      }
   3832 
   3833      return t;
   3834    }
   3835 
   3836    /**
   3837     *
   3838     * @param {object} options
   3839     * @param {nsIPrincipal} [options.originPrincipal]
   3840     *   If uriString is given, uri might inherit principals, and no preloaded browser is used,
   3841     *   this is the origin principal to be inherited by the initial about:blank.
   3842     * @param {nsIPrincipal} [options.originStoragePrincipal]
   3843     *   If uriString is given, uri might inherit principals, and no preloaded browser is used,
   3844     *   this is the origin storage principal to be inherited by the initial about:blank.
   3845     */
   3846    _createBrowserForTab(
   3847      tab,
   3848      {
   3849        uriString,
   3850        uri,
   3851        name,
   3852        preferredRemoteType,
   3853        openerBrowser,
   3854        uriIsAboutBlank,
   3855        referrerInfo,
   3856        forceNotRemote,
   3857        initialBrowsingContextGroupId,
   3858        openWindowInfo,
   3859        skipLoad,
   3860        triggeringRemoteType,
   3861      }
   3862    ) {
   3863      // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
   3864      // we have a remote triggering remote type, use that instead.
   3865      if (!preferredRemoteType && triggeringRemoteType) {
   3866        preferredRemoteType = triggeringRemoteType;
   3867      }
   3868 
   3869      // If we don't have a preferred remote type, and we have a remote
   3870      // opener, use the opener's remote type.
   3871      if (!preferredRemoteType && openerBrowser) {
   3872        preferredRemoteType = openerBrowser.remoteType;
   3873      }
   3874 
   3875      let { userContextId } = tab;
   3876 
   3877      var oa = E10SUtils.predictOriginAttributes({ window, userContextId });
   3878 
   3879      // If URI is about:blank and we don't have a preferred remote type,
   3880      // then we need to use the referrer, if we have one, to get the
   3881      // correct remote type for the new tab.
   3882      if (
   3883        uriIsAboutBlank &&
   3884        !preferredRemoteType &&
   3885        referrerInfo &&
   3886        referrerInfo.originalReferrer
   3887      ) {
   3888        preferredRemoteType = E10SUtils.getRemoteTypeForURI(
   3889          referrerInfo.originalReferrer.spec,
   3890          gMultiProcessBrowser,
   3891          gFissionBrowser,
   3892          E10SUtils.DEFAULT_REMOTE_TYPE,
   3893          null,
   3894          oa
   3895        );
   3896      }
   3897 
   3898      let remoteType = forceNotRemote
   3899        ? E10SUtils.NOT_REMOTE
   3900        : E10SUtils.getRemoteTypeForURI(
   3901            uriString,
   3902            gMultiProcessBrowser,
   3903            gFissionBrowser,
   3904            preferredRemoteType,
   3905            null,
   3906            oa
   3907          );
   3908 
   3909      let b,
   3910        usingPreloadedContent = false;
   3911      // If we open a new tab with the newtab URL in the default
   3912      // userContext, check if there is a preloaded browser ready.
   3913      if (uriString == BROWSER_NEW_TAB_URL && !userContextId) {
   3914        b = NewTabPagePreloading.getPreloadedBrowser(window);
   3915        if (b) {
   3916          usingPreloadedContent = true;
   3917        }
   3918      }
   3919 
   3920      if (!b) {
   3921        // No preloaded browser found, create one.
   3922        b = this.createBrowser({
   3923          remoteType,
   3924          uriIsAboutBlank,
   3925          userContextId,
   3926          initialBrowsingContextGroupId,
   3927          openWindowInfo,
   3928          name,
   3929          skipLoad,
   3930        });
   3931      }
   3932 
   3933      tab.linkedBrowser = b;
   3934 
   3935      this._tabForBrowser.set(b, tab);
   3936      tab.permanentKey = b.permanentKey;
   3937      tab._browserParams = {
   3938        uriIsAboutBlank,
   3939        remoteType,
   3940        usingPreloadedContent,
   3941      };
   3942 
   3943      // Hack to ensure that the about:newtab, and about:welcome favicon is loaded
   3944      // instantaneously, to avoid flickering and improve perceived performance.
   3945      this.setDefaultIcon(tab, uri);
   3946 
   3947      return { browser: b, usingPreloadedContent };
   3948    }
   3949 
   3950    _kickOffBrowserLoad(
   3951      browser,
   3952      {
   3953        uri,
   3954        uriString,
   3955        usingPreloadedContent,
   3956        triggeringPrincipal,
   3957        originPrincipal,
   3958        originStoragePrincipal,
   3959        uriIsAboutBlank,
   3960        allowInheritPrincipal,
   3961        allowThirdPartyFixup,
   3962        fromExternal,
   3963        forceAllowDataURI,
   3964        isCaptivePortalTab,
   3965        skipLoad,
   3966        referrerInfo,
   3967        charset,
   3968        postData,
   3969        policyContainer,
   3970        globalHistoryOptions,
   3971        triggeringRemoteType,
   3972        schemelessInput,
   3973        hasValidUserGestureActivation,
   3974        textDirectiveUserActivation,
   3975      }
   3976    ) {
   3977      const shouldInheritSecurityContext = (() => {
   3978        if (
   3979          !usingPreloadedContent &&
   3980          originPrincipal &&
   3981          originStoragePrincipal &&
   3982          uriString
   3983        ) {
   3984          let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
   3985          // Unless we know for sure we're not inheriting principals,
   3986          // force the about:blank viewer to have the right principal:
   3987          if (!uri || doGetProtocolFlags(uri) & URI_INHERITS_SECURITY_CONTEXT) {
   3988            return true;
   3989          }
   3990        }
   3991        return false;
   3992      })();
   3993 
   3994      if (shouldInheritSecurityContext) {
   3995        browser.createAboutBlankDocumentViewer(
   3996          originPrincipal,
   3997          originStoragePrincipal
   3998        );
   3999      }
   4000 
   4001      // If we didn't swap docShells with a preloaded browser
   4002      // then let's just continue loading the page normally.
   4003      if (
   4004        !usingPreloadedContent &&
   4005        (!uriIsAboutBlank || !allowInheritPrincipal) &&
   4006        !skipLoad
   4007      ) {
   4008        // pretend the user typed this so it'll be available till
   4009        // the document successfully loads
   4010        if (uriString && !gInitialPages.includes(uriString)) {
   4011          browser.userTypedValue = uriString;
   4012        }
   4013 
   4014        let loadFlags = LOAD_FLAGS_NONE;
   4015        if (allowThirdPartyFixup) {
   4016          loadFlags |=
   4017            LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
   4018        }
   4019        if (fromExternal) {
   4020          loadFlags |= LOAD_FLAGS_FROM_EXTERNAL;
   4021        } else if (!triggeringPrincipal.isSystemPrincipal) {
   4022          // XXX this code must be reviewed and changed when bug 1616353
   4023          // lands.
   4024          // The purpose of LOAD_FLAGS_FIRST_LOAD is to close a new
   4025          // tab if it turns out to be a download.
   4026          loadFlags |= LOAD_FLAGS_FIRST_LOAD;
   4027        }
   4028        if (!allowInheritPrincipal) {
   4029          loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
   4030        }
   4031        if (isCaptivePortalTab) {
   4032          loadFlags |= LOAD_FLAGS_DISABLE_TRR;
   4033        }
   4034        if (forceAllowDataURI) {
   4035          loadFlags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
   4036        }
   4037        try {
   4038          browser.fixupAndLoadURIString(uriString, {
   4039            loadFlags,
   4040            triggeringPrincipal,
   4041            referrerInfo,
   4042            charset,
   4043            postData,
   4044            policyContainer,
   4045            globalHistoryOptions,
   4046            triggeringRemoteType,
   4047            schemelessInput,
   4048            hasValidUserGestureActivation,
   4049            textDirectiveUserActivation,
   4050            isCaptivePortalTab,
   4051          });
   4052        } catch (ex) {
   4053          console.error(ex);
   4054        }
   4055      }
   4056    }
   4057 
   4058    /**
   4059     * @typedef {object} TabGroupWorkingData
   4060     * @property {TabGroupStateData} stateData
   4061     * @property {MozTabbrowserTabGroup|undefined} node
   4062     * @property {DocumentFragment} containingTabsFragment
   4063     */
   4064 
   4065    /**
   4066     * @param {boolean} restoreTabsLazily
   4067     * @param {number} selectTab see SessionStoreInternal.restoreTabs { aSelectTab }
   4068     * @param {TabStateData[]} tabDataList
   4069     * @param {TabGroupStateData[]} tabGroupDataList
   4070     * @returns {MozTabbrowserTab[]}
   4071     */
   4072    createTabsForSessionRestore(
   4073      restoreTabsLazily,
   4074      selectTab,
   4075      tabDataList,
   4076      tabGroupDataList
   4077    ) {
   4078      let tabs = [];
   4079      let tabsFragment = document.createDocumentFragment();
   4080      let tabToSelect = null;
   4081      let hiddenTabs = new Map();
   4082      /** @type {Map<TabGroupStateData['id'], TabGroupWorkingData>} */
   4083      let tabGroupWorkingData = new Map();
   4084 
   4085      for (const tabGroupData of tabGroupDataList) {
   4086        tabGroupWorkingData.set(tabGroupData.id, {
   4087          stateData: tabGroupData,
   4088          node: undefined,
   4089          containingTabsFragment: document.createDocumentFragment(),
   4090        });
   4091      }
   4092 
   4093      // We create each tab and browser, but only insert them
   4094      // into a document fragment so that we can insert them all
   4095      // together. This prevents synch reflow for each tab
   4096      // insertion.
   4097      for (var i = 0; i < tabDataList.length; i++) {
   4098        let tabData = tabDataList[i];
   4099 
   4100        let userContextId = tabData.userContextId;
   4101        let select = i == selectTab - 1;
   4102        let tab;
   4103        let tabWasReused = false;
   4104 
   4105        // Re-use existing selected tab if possible to avoid the overhead of
   4106        // selecting a new tab. For now, we only do this for horizontal tabs;
   4107        // we'll let tabs.js handle pinning for vertical tabs until we unify
   4108        // the logic for both horizontal and vertical tabs in bug 1910097.
   4109        if (
   4110          select &&
   4111          this.selectedTab.userContextId == userContextId &&
   4112          !SessionStore.isTabRestoring(this.selectedTab) &&
   4113          !this.tabContainer.verticalMode
   4114        ) {
   4115          tabWasReused = true;
   4116          tab = this.selectedTab;
   4117          if (!tabData.pinned) {
   4118            this.unpinTab(tab);
   4119          } else {
   4120            this.pinTab(tab);
   4121          }
   4122        }
   4123 
   4124        // Add a new tab if needed.
   4125        if (!tab) {
   4126          let createLazyBrowser =
   4127            restoreTabsLazily && !select && !tabData.pinned;
   4128 
   4129          let url = "about:blank";
   4130          if (tabData.entries?.length) {
   4131            let activeIndex = (tabData.index || tabData.entries.length) - 1;
   4132            // Ensure the index is in bounds.
   4133            activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
   4134            activeIndex = Math.max(activeIndex, 0);
   4135            url = tabData.entries[activeIndex].url;
   4136          }
   4137 
   4138          let preferredRemoteType = E10SUtils.getRemoteTypeForURI(
   4139            url,
   4140            gMultiProcessBrowser,
   4141            gFissionBrowser,
   4142            E10SUtils.DEFAULT_REMOTE_TYPE,
   4143            null,
   4144            E10SUtils.predictOriginAttributes({ window, userContextId })
   4145          );
   4146 
   4147          // If we're creating a lazy browser, let tabbrowser know the future
   4148          // URI because progress listeners won't get onLocationChange
   4149          // notification before the browser is inserted.
   4150          //
   4151          // Setting noInitialLabel is a perf optimization. Rendering tab labels
   4152          // would make resizing the tabs more expensive as we're adding them.
   4153          // Each tab will get its initial label set in restoreTab.
   4154          tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", {
   4155            createLazyBrowser,
   4156            skipAnimation: true,
   4157            noInitialLabel: true,
   4158            userContextId,
   4159            skipBackgroundNotify: true,
   4160            bulkOrderedOpen: true,
   4161            insertTab: false,
   4162            skipLoad: true,
   4163            preferredRemoteType,
   4164          });
   4165 
   4166          if (select) {
   4167            tabToSelect = tab;
   4168          }
   4169        }
   4170 
   4171        tabs.push(tab);
   4172 
   4173        if (tabData.pinned) {
   4174          this.pinTab(tab);
   4175          // Then ensure all the tab open/pinning information is sent.
   4176          this._fireTabOpen(tab, {});
   4177        } else if (tabData.groupId) {
   4178          let { groupId } = tabData;
   4179          const tabGroup = tabGroupWorkingData.get(groupId);
   4180          // if a tab refers to a tab group we don't know, skip any group
   4181          // processing
   4182          if (tabGroup) {
   4183            tabGroup.containingTabsFragment.appendChild(tab);
   4184            // if this is the first time encountering a tab group, create its
   4185            // DOM node once and place it in the tabs bar fragment
   4186            if (!tabGroup.node) {
   4187              tabGroup.node = this._createTabGroup(
   4188                tabGroup.stateData.id,
   4189                tabGroup.stateData.color,
   4190                tabGroup.stateData.collapsed,
   4191                tabGroup.stateData.name
   4192              );
   4193              tabsFragment.appendChild(tabGroup.node);
   4194            }
   4195          }
   4196        } else {
   4197          if (tab.hidden) {
   4198            tab.hidden = true;
   4199            hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy);
   4200          }
   4201 
   4202          tabsFragment.appendChild(tab);
   4203          if (tabWasReused) {
   4204            this.tabContainer._invalidateCachedTabs();
   4205          }
   4206        }
   4207 
   4208        tab.initialize();
   4209      }
   4210 
   4211      // inject the top-level tab and tab group DOM nodes
   4212      this.tabContainer.appendChild(tabsFragment);
   4213 
   4214      // inject tab DOM nodes into the now-connected tab group DOM nodes
   4215      for (const tabGroup of tabGroupWorkingData.values()) {
   4216        if (tabGroup.node) {
   4217          tabGroup.node.appendChild(tabGroup.containingTabsFragment);
   4218        }
   4219      }
   4220 
   4221      for (let [tab, hiddenBy] of hiddenTabs) {
   4222        let event = document.createEvent("Events");
   4223        event.initEvent("TabHide", true, false);
   4224        tab.dispatchEvent(event);
   4225        if (hiddenBy) {
   4226          SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy);
   4227        }
   4228      }
   4229 
   4230      this.tabContainer._invalidateCachedTabs();
   4231 
   4232      // We need to wait until after all tabs have been appended to the DOM
   4233      // to remove the old selected tab.
   4234      if (tabToSelect) {
   4235        let leftoverTab = this.selectedTab;
   4236        this.selectedTab = tabToSelect;
   4237        this.removeTab(leftoverTab);
   4238      }
   4239 
   4240      if (tabs.length > 1 || !tabs[0].selected) {
   4241        this._updateTabsAfterInsert();
   4242        TabBarVisibility.update();
   4243 
   4244        for (let tab of tabs) {
   4245          // If tabToSelect is a tab, we didn't reuse the selected tab.
   4246          if (tabToSelect || !tab.selected) {
   4247            // Fire a TabOpen event for all unpinned tabs, except reused selected
   4248            // tabs.
   4249            if (!tab.pinned) {
   4250              this._fireTabOpen(tab, {});
   4251            }
   4252 
   4253            // Fire a TabBrowserInserted event on all tabs that have a connected,
   4254            // real browser, except for reused selected tabs.
   4255            if (tab.linkedPanel) {
   4256              var evt = new CustomEvent("TabBrowserInserted", {
   4257                bubbles: true,
   4258                detail: { insertedOnTabCreation: true },
   4259              });
   4260              tab.dispatchEvent(evt);
   4261            }
   4262          }
   4263        }
   4264      }
   4265 
   4266      return tabs;
   4267    }
   4268 
   4269    moveTabsToStart(contextTab) {
   4270      let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
   4271      // Walk the array in reverse order so the tabs are kept in order.
   4272      for (let i = tabs.length - 1; i >= 0; i--) {
   4273        this.moveTabToStart(tabs[i]);
   4274      }
   4275    }
   4276 
   4277    moveTabsToEnd(contextTab) {
   4278      let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
   4279      for (let tab of tabs) {
   4280        this.moveTabToEnd(tab);
   4281      }
   4282    }
   4283 
   4284    warnAboutClosingTabs(tabsToClose, aCloseTabs) {
   4285      // We want to warn about closing duplicates even if there was only a
   4286      // single duplicate, so we intentionally place this above the check for
   4287      // tabsToClose <= 1.
   4288      const shownDupeDialogPref =
   4289        "browser.tabs.haveShownCloseAllDuplicateTabsWarning";
   4290      var ps = Services.prompt;
   4291      if (
   4292        aCloseTabs == this.closingTabsEnum.ALL_DUPLICATES &&
   4293        !Services.prefs.getBoolPref(shownDupeDialogPref, false)
   4294      ) {
   4295        // The first time a user closes all duplicate tabs, tell them what will
   4296        // happen and give them a chance to back away.
   4297        Services.prefs.setBoolPref(shownDupeDialogPref, true);
   4298 
   4299        window.focus();
   4300        const [title, text, button] = this.tabLocalization.formatValuesSync([
   4301          { id: "tabbrowser-confirm-close-all-duplicate-tabs-title" },
   4302          { id: "tabbrowser-confirm-close-all-duplicate-tabs-text" },
   4303          {
   4304            id: "tabbrowser-confirm-close-all-duplicate-tabs-button-closetabs",
   4305          },
   4306        ]);
   4307 
   4308        const flags =
   4309          ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
   4310          ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
   4311          ps.BUTTON_POS_0_DEFAULT;
   4312 
   4313        // buttonPressed will be 0 for close tabs, 1 for cancel.
   4314        const buttonPressed = ps.confirmEx(
   4315          window,
   4316          title,
   4317          text,
   4318          flags,
   4319          button,
   4320          null,
   4321          null,
   4322          null,
   4323          {}
   4324        );
   4325        return buttonPressed == 0;
   4326      }
   4327 
   4328      if (tabsToClose <= 1) {
   4329        return true;
   4330      }
   4331 
   4332      const pref =
   4333        aCloseTabs == this.closingTabsEnum.ALL
   4334          ? "browser.tabs.warnOnClose"
   4335          : "browser.tabs.warnOnCloseOtherTabs";
   4336      var shouldPrompt = Services.prefs.getBoolPref(pref);
   4337      if (!shouldPrompt) {
   4338        return true;
   4339      }
   4340 
   4341      const maxTabsUndo = Services.prefs.getIntPref(
   4342        "browser.sessionstore.max_tabs_undo"
   4343      );
   4344      if (
   4345        aCloseTabs != this.closingTabsEnum.ALL &&
   4346        tabsToClose <= maxTabsUndo
   4347      ) {
   4348        return true;
   4349      }
   4350 
   4351      // Our prompt to close this window is most important, so replace others.
   4352      gDialogBox.replaceDialogIfOpen();
   4353 
   4354      // default to true: if it were false, we wouldn't get this far
   4355      var warnOnClose = { value: true };
   4356 
   4357      // focus the window before prompting.
   4358      // this will raise any minimized window, which will
   4359      // make it obvious which window the prompt is for and will
   4360      // solve the problem of windows "obscuring" the prompt.
   4361      // see bug #350299 for more details
   4362      window.focus();
   4363      const [title, button, checkbox] = this.tabLocalization.formatValuesSync([
   4364        {
   4365          id: "tabbrowser-confirm-close-tabs-title",
   4366          args: { tabCount: tabsToClose },
   4367        },
   4368        { id: "tabbrowser-confirm-close-tabs-button" },
   4369        { id: "tabbrowser-ask-close-tabs-checkbox" },
   4370      ]);
   4371      let flags =
   4372        ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
   4373        ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1;
   4374      let checkboxLabel =
   4375        aCloseTabs == this.closingTabsEnum.ALL ? checkbox : null;
   4376      var buttonPressed = ps.confirmEx(
   4377        window,
   4378        title,
   4379        null,
   4380        flags,
   4381        button,
   4382        null,
   4383        null,
   4384        checkboxLabel,
   4385        warnOnClose
   4386      );
   4387 
   4388      var reallyClose = buttonPressed == 0;
   4389 
   4390      // don't set the pref unless they press OK and it's false
   4391      if (
   4392        aCloseTabs == this.closingTabsEnum.ALL &&
   4393        reallyClose &&
   4394        !warnOnClose.value
   4395      ) {
   4396        Services.prefs.setBoolPref(pref, false);
   4397      }
   4398 
   4399      return reallyClose;
   4400    }
   4401 
   4402    /**
   4403     * This determines where the tab should be inserted within the tabContainer,
   4404     * and inserts it.
   4405     *
   4406     * @param {MozTabbrowserTab} tab
   4407     * @param {object} [options]
   4408     * @param {number} [options.elementIndex]
   4409     * @param {number} [options.tabIndex]
   4410     * @param {MozTabbrowserTabGroup} [options.tabGroup]
   4411     *   A related tab group where this tab should be added, when applicable.
   4412     */
   4413    #insertTabAtIndex(
   4414      tab,
   4415      {
   4416        tabIndex,
   4417        elementIndex,
   4418        ownerTab,
   4419        openerTab,
   4420        pinned,
   4421        bulkOrderedOpen,
   4422        tabGroup,
   4423      } = {}
   4424    ) {
   4425      // If this new tab is owned by another, assert that relationship
   4426      if (ownerTab) {
   4427        tab.owner = ownerTab;
   4428      }
   4429 
   4430      // Ensure we have an index if one was not provided.
   4431      if (typeof elementIndex != "number" && typeof tabIndex != "number") {
   4432        // Move the new tab after another tab if needed, to the end otherwise.
   4433        elementIndex = Infinity;
   4434        if (
   4435          !bulkOrderedOpen &&
   4436          ((openerTab &&
   4437            Services.prefs.getBoolPref(
   4438              "browser.tabs.insertRelatedAfterCurrent"
   4439            )) ||
   4440            Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent"))
   4441        ) {
   4442          let lastRelatedTab =
   4443            openerTab && this._lastRelatedTabMap.get(openerTab);
   4444          let previousTab = lastRelatedTab || openerTab || this.selectedTab;
   4445          if (!tabGroup) {
   4446            tabGroup = previousTab.group;
   4447          }
   4448          if (
   4449            Services.prefs.getBoolPref(
   4450              "browser.tabs.insertAfterCurrentExceptPinned"
   4451            ) &&
   4452            previousTab.pinned
   4453          ) {
   4454            elementIndex = Infinity;
   4455          } else if (previousTab.visible && previousTab.splitview) {
   4456            elementIndex =
   4457              this.tabContainer.dragAndDropElements.indexOf(
   4458                previousTab.splitview
   4459              ) + 1;
   4460          } else if (previousTab.visible) {
   4461            elementIndex = previousTab.elementIndex + 1;
   4462          } else if (previousTab == FirefoxViewHandler.tab) {
   4463            elementIndex = 0;
   4464          }
   4465 
   4466          if (lastRelatedTab) {
   4467            lastRelatedTab.owner = null;
   4468          } else if (openerTab) {
   4469            tab.owner = openerTab;
   4470          }
   4471          // Always set related map if opener exists.
   4472          if (openerTab) {
   4473            this._lastRelatedTabMap.set(openerTab, tab);
   4474          }
   4475        }
   4476      }
   4477 
   4478      let allItems;
   4479      let index;
   4480      if (typeof elementIndex == "number") {
   4481        allItems = this.tabContainer.dragAndDropElements;
   4482        index = elementIndex;
   4483      } else {
   4484        allItems = this.tabs;
   4485        index = tabIndex;
   4486      }
   4487      // Ensure index is within bounds.
   4488      if (tab.pinned) {
   4489        index = Math.max(index, 0);
   4490        index = Math.min(index, this.pinnedTabCount);
   4491      } else {
   4492        index = Math.max(index, this.pinnedTabCount);
   4493        index = Math.min(index, allItems.length);
   4494      }
   4495      /** @type {MozTabbrowserTab|undefined} */
   4496      let itemAfter = allItems.at(index);
   4497 
   4498      if (pinned && !itemAfter?.pinned) {
   4499        itemAfter = null;
   4500      } else if (itemAfter?.splitview) {
   4501        itemAfter = itemAfter.splitview?.nextElementSibling || null;
   4502      }
   4503      // Prevent a flash of unstyled content by setting up the tab content
   4504      // and inherited attributes before appending it (see Bug 1592054):
   4505      tab.initialize();
   4506 
   4507      this.tabContainer._invalidateCachedTabs();
   4508 
   4509      if (tabGroup) {
   4510        if (
   4511          (this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
   4512          this.isSplitViewWrapper(itemAfter)
   4513        ) {
   4514          // Place at the front of, or between tabs in, the same tab group
   4515          this.tabContainer.insertBefore(tab, itemAfter);
   4516        } else {
   4517          // Place tab at the end of the contextual tab group because one of:
   4518          // 1) no `itemAfter` so `tab` should be the last tab in the tab strip
   4519          // 2) `itemAfter` is in a different tab group
   4520          tabGroup.appendChild(tab);
   4521        }
   4522      } else if (
   4523        (this.isTab(itemAfter) && itemAfter.group?.tabs[0] == itemAfter) ||
   4524        this.isTabGroupLabel(itemAfter)
   4525      ) {
   4526        // If there is ambiguity around whether or not a tab should be inserted
   4527        // into a group (i.e. because the new tab is being inserted on the
   4528        // edges of the group), prefer not to insert the tab into the group.
   4529        //
   4530        // We only need to handle the case where the tab is being inserted at
   4531        // the starting boundary of a group because `insertBefore` called on
   4532        // the tab just after a tab group will not add it to the group by
   4533        // default.
   4534        this.tabContainer.insertBefore(tab, itemAfter.group);
   4535      } else {
   4536        // Place ungrouped tab before `itemAfter` by default
   4537        const tabContainer = pinned
   4538          ? this.tabContainer.pinnedTabsContainer
   4539          : this.tabContainer;
   4540        tabContainer.insertBefore(tab, itemAfter);
   4541      }
   4542 
   4543      if (tab.group?.collapsed) {
   4544        // Bug 1997096: automatically expand the group if we are adding a new
   4545        // tab to a collapsed group, and that tab does not have automatic focus
   4546        // (i.e. if the user right clicks and clicks "Open in New Tab")
   4547        tab.group.collapsed = false;
   4548      }
   4549 
   4550      this._updateTabsAfterInsert();
   4551 
   4552      if (pinned) {
   4553        this._updateTabBarForPinnedTabs();
   4554      }
   4555 
   4556      TabBarVisibility.update();
   4557    }
   4558 
   4559    /**
   4560     * Dispatch a new tab event. This should be called when things are in a
   4561     * consistent state, such that listeners of this event can again open
   4562     * or close tabs.
   4563     */
   4564    _fireTabOpen(tab, eventDetail) {
   4565      delete tab.initializingTab;
   4566      let evt = new CustomEvent("TabOpen", {
   4567        bubbles: true,
   4568        detail: eventDetail || {},
   4569      });
   4570      tab.dispatchEvent(evt);
   4571    }
   4572 
   4573    /**
   4574     * @param {MozTabbrowserTab} aTab
   4575     * @returns {MozTabbrowserTab[]}
   4576     */
   4577    _getTabsToTheStartFrom(aTab) {
   4578      let tabsToStart = [];
   4579      if (!aTab.visible) {
   4580        return tabsToStart;
   4581      }
   4582      let tabs = this.openTabs;
   4583      for (let i = 0; i < tabs.length; ++i) {
   4584        if (tabs[i] == aTab) {
   4585          break;
   4586        }
   4587        // Ignore pinned and hidden tabs.
   4588        if (tabs[i].pinned || tabs[i].hidden) {
   4589          continue;
   4590        }
   4591        // In a multi-select context, select all unselected tabs
   4592        // starting from the context tab.
   4593        if (aTab.multiselected && tabs[i].multiselected) {
   4594          continue;
   4595        }
   4596        tabsToStart.push(tabs[i]);
   4597      }
   4598      return tabsToStart;
   4599    }
   4600 
   4601    /**
   4602     * @param {MozTabbrowserTab} aTab
   4603     * @returns {MozTabbrowserTab[]}
   4604     */
   4605    _getTabsToTheEndFrom(aTab) {
   4606      let tabsToEnd = [];
   4607      if (!aTab.visible) {
   4608        return tabsToEnd;
   4609      }
   4610      let tabs = this.openTabs;
   4611      for (let i = tabs.length - 1; i >= 0; --i) {
   4612        if (tabs[i] == aTab) {
   4613          break;
   4614        }
   4615        // Ignore pinned and hidden tabs.
   4616        if (tabs[i].pinned || tabs[i].hidden) {
   4617          continue;
   4618        }
   4619        // In a multi-select context, select all unselected tabs
   4620        // starting from the context tab.
   4621        if (aTab.multiselected && tabs[i].multiselected) {
   4622          continue;
   4623        }
   4624        tabsToEnd.push(tabs[i]);
   4625      }
   4626      return tabsToEnd;
   4627    }
   4628 
   4629    getDuplicateTabsToClose(aTab) {
   4630      // One would think that a set is better, but it would need to copy all
   4631      // the strings instead of just keeping references to the nsIURI objects,
   4632      // and the array is presumed to be small anyways.
   4633      let keys = [];
   4634      let keyForTab = tab => {
   4635        let uri = tab.linkedBrowser?.currentURI;
   4636        if (!uri) {
   4637          return null;
   4638        }
   4639        return {
   4640          uri,
   4641          userContextId: tab.userContextId,
   4642        };
   4643      };
   4644      let keyEquals = (a, b) => {
   4645        return a.userContextId == b.userContextId && a.uri.equals(b.uri);
   4646      };
   4647      if (aTab.multiselected) {
   4648        for (let tab of this.selectedTabs) {
   4649          let key = keyForTab(tab);
   4650          if (key) {
   4651            keys.push(key);
   4652          }
   4653        }
   4654      } else {
   4655        let key = keyForTab(aTab);
   4656        if (key) {
   4657          keys.push(key);
   4658        }
   4659      }
   4660 
   4661      if (!keys.length) {
   4662        return [];
   4663      }
   4664 
   4665      let duplicateTabs = [];
   4666      for (let tab of this.tabs) {
   4667        if (tab == aTab || tab.pinned) {
   4668          continue;
   4669        }
   4670        if (aTab.multiselected && tab.multiselected) {
   4671          continue;
   4672        }
   4673        let key = keyForTab(tab);
   4674        if (key && keys.some(k => keyEquals(k, key))) {
   4675          duplicateTabs.push(tab);
   4676        }
   4677      }
   4678 
   4679      return duplicateTabs;
   4680    }
   4681 
   4682    getAllDuplicateTabsToClose() {
   4683      let lastSeenTabs = this.tabs.toSorted(
   4684        (a, b) => b.lastSeenActive - a.lastSeenActive
   4685      );
   4686      let duplicateTabs = [];
   4687      let keys = [];
   4688      for (let tab of lastSeenTabs) {
   4689        const uri = tab.linkedBrowser?.currentURI;
   4690        if (!uri) {
   4691          // Can't tell if it's a duplicate without a URI.
   4692          // Safest to leave it be.
   4693          continue;
   4694        }
   4695 
   4696        const key = {
   4697          uri,
   4698          userContextId: tab.userContextId,
   4699        };
   4700        if (
   4701          !tab.pinned &&
   4702          keys.some(
   4703            k => k.userContextId == key.userContextId && k.uri.equals(key.uri)
   4704          )
   4705        ) {
   4706          duplicateTabs.push(tab);
   4707        }
   4708        keys.push(key);
   4709      }
   4710      return duplicateTabs;
   4711    }
   4712 
   4713    removeDuplicateTabs(aTab, options) {
   4714      this._removeDuplicateTabs(
   4715        aTab,
   4716        this.getDuplicateTabsToClose(aTab),
   4717        this.closingTabsEnum.DUPLICATES,
   4718        options
   4719      );
   4720    }
   4721 
   4722    _removeDuplicateTabs(aConfirmationAnchor, tabs, aCloseTabs, options) {
   4723      if (!tabs.length) {
   4724        return;
   4725      }
   4726 
   4727      if (!this.warnAboutClosingTabs(tabs.length, aCloseTabs)) {
   4728        return;
   4729      }
   4730 
   4731      this.removeTabs(tabs, options);
   4732      ConfirmationHint.show(
   4733        aConfirmationAnchor,
   4734        "confirmation-hint-duplicate-tabs-closed",
   4735        { l10nArgs: { tabCount: tabs.length } }
   4736      );
   4737    }
   4738 
   4739    removeAllDuplicateTabs() {
   4740      // I would like to have the caller provide this target,
   4741      // but the caller lives in a different document.
   4742      let alltabsButton = document.getElementById("alltabs-button");
   4743      this._removeDuplicateTabs(
   4744        alltabsButton,
   4745        this.getAllDuplicateTabsToClose(),
   4746        this.closingTabsEnum.ALL_DUPLICATES
   4747      );
   4748    }
   4749 
   4750    /**
   4751     * In a multi-select context, the tabs (except pinned tabs) that are located to the
   4752     * left of the leftmost selected tab will be removed.
   4753     */
   4754    removeTabsToTheStartFrom(aTab, options) {
   4755      let tabs = this._getTabsToTheStartFrom(aTab);
   4756      if (
   4757        !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
   4758      ) {
   4759        return;
   4760      }
   4761 
   4762      this.removeTabs(tabs, options);
   4763    }
   4764 
   4765    /**
   4766     * In a multi-select context, the tabs (except pinned tabs) that are located to the
   4767     * right of the rightmost selected tab will be removed.
   4768     */
   4769    removeTabsToTheEndFrom(aTab, options) {
   4770      let tabs = this._getTabsToTheEndFrom(aTab);
   4771      if (
   4772        !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
   4773      ) {
   4774        return;
   4775      }
   4776 
   4777      this.removeTabs(tabs, options);
   4778    }
   4779 
   4780    /**
   4781     * Remove all tabs but `aTab`. By default, in a multi-select context, all
   4782     * unpinned and unselected tabs are removed. Otherwise all unpinned tabs
   4783     * except aTab are removed. This behavior can be changed using the the bool
   4784     * flags below.
   4785     *
   4786     * @param {MozTabbrowserTab} aTab
   4787     *   The tab we will skip removing
   4788     * @param {object} [aParams]
   4789     *   An optional set of parameters that will be passed to the
   4790     *   `removeTabs` function.
   4791     * @param {boolean} [aParams.skipWarnAboutClosingTabs=false]
   4792     *   Skip showing the tab close warning prompt.
   4793     * @param {boolean} [aParams.skipPinnedOrSelectedTabs=true]
   4794     *   Skip closing tabs that are selected or pinned.
   4795     */
   4796    removeAllTabsBut(aTab, aParams = {}) {
   4797      let {
   4798        skipWarnAboutClosingTabs = false,
   4799        skipPinnedOrSelectedTabs = true,
   4800      } = aParams;
   4801 
   4802      /** @type {function(MozTabbrowserTab):boolean} */
   4803      let filterFn;
   4804 
   4805      // If enabled also filter by selected or pinned state.
   4806      if (skipPinnedOrSelectedTabs) {
   4807        if (aTab?.multiselected) {
   4808          filterFn = tab => !tab.multiselected && !tab.pinned && !tab.hidden;
   4809        } else {
   4810          filterFn = tab => tab != aTab && !tab.pinned && !tab.hidden;
   4811        }
   4812      } else {
   4813        // Exclude just aTab from being removed.
   4814        filterFn = tab => tab != aTab;
   4815      }
   4816 
   4817      let tabsToRemove = this.openTabs.filter(filterFn);
   4818 
   4819      // If enabled show the tab close warning.
   4820      if (
   4821        !skipWarnAboutClosingTabs &&
   4822        !this.warnAboutClosingTabs(
   4823          tabsToRemove.length,
   4824          this.closingTabsEnum.OTHER
   4825        )
   4826      ) {
   4827        return;
   4828      }
   4829 
   4830      this.removeTabs(tabsToRemove, aParams);
   4831    }
   4832 
   4833    removeMultiSelectedTabs({ isUserTriggered, telemetrySource } = {}) {
   4834      let selectedTabs = this.selectedTabs;
   4835      if (
   4836        !this.warnAboutClosingTabs(
   4837          selectedTabs.length,
   4838          this.closingTabsEnum.MULTI_SELECTED
   4839        )
   4840      ) {
   4841        return;
   4842      }
   4843 
   4844      this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
   4845    }
   4846 
   4847    /**
   4848     * @typedef {object} _startRemoveTabsReturnValue
   4849     * @property {Promise<void>} beforeUnloadComplete
   4850     *   A promise that is resolved once all the beforeunload handlers have been
   4851     *   called.
   4852     * @property {object[]} tabsWithBeforeUnloadPrompt
   4853     *   An array of tabs with unload prompts that need to be handled.
   4854     * @property {object} [lastToClose]
   4855     *   The last tab to be closed, if appropriate.
   4856     */
   4857 
   4858    /**
   4859     * Starts to remove tabs from the UI: checking for beforeunload handlers,
   4860     * closing tabs where possible and triggering running of the unload handlers.
   4861     *
   4862     * @param {object[]} tabs
   4863     *   The set of tabs to remove.
   4864     * @param {object} options
   4865     * @param {boolean} options.animate
   4866     *   Whether or not to animate closing.
   4867     * @param {boolean} options.suppressWarnAboutClosingWindow
   4868     *   This will supress the warning about closing a window with the last tab.
   4869     * @param {boolean} options.skipPermitUnload
   4870     *   Skips the before unload checks for the tabs. Only set this to true when
   4871     *   using it in tandem with `runBeforeUnloadForTabs`.
   4872     * @param {boolean} options.skipRemoves
   4873     *   Skips actually removing the tabs. The beforeunload handlers still run.
   4874     * @param {boolean} options.skipSessionStore
   4875     *   If true, don't record the closed tabs in SessionStore.
   4876     * @returns {_startRemoveTabsReturnValue}
   4877     */
   4878    _startRemoveTabs(
   4879      tabs,
   4880      {
   4881        animate,
   4882        // See bug 1883051
   4883        // eslint-disable-next-line no-unused-vars
   4884        suppressWarnAboutClosingWindow,
   4885        skipPermitUnload,
   4886        skipRemoves,
   4887        skipSessionStore,
   4888        isUserTriggered,
   4889        telemetrySource,
   4890      }
   4891    ) {
   4892      // Note: if you change any of the unload algorithm, consider also
   4893      // changing `runBeforeUnloadForTabs` above.
   4894      /** @type {MozTabbrowserTab[]} */
   4895      let tabsWithBeforeUnloadPrompt = [];
   4896      /** @type {MozTabbrowserTab[]} */
   4897      let tabsWithoutBeforeUnload = [];
   4898      /** @type {Promise<void>[]} */
   4899      let beforeUnloadPromises = [];
   4900      /** @type {MozTabbrowserTab|undefined} */
   4901      let lastToClose;
   4902 
   4903      for (let tab of tabs) {
   4904        if (!skipRemoves) {
   4905          tab._closedInMultiselection = true;
   4906        }
   4907        if (!skipRemoves && tab.selected) {
   4908          lastToClose = tab;
   4909          let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
   4910          if (toBlurTo) {
   4911            this._getSwitcher().warmupTab(toBlurTo);
   4912          }
   4913        } else if (!skipPermitUnload && this._hasBeforeUnload(tab)) {
   4914          let timerId = Glean.browserTabclose.permitUnloadTime.start();
   4915          // We need to block while calling permitUnload() because it
   4916          // processes the event queue and may lead to another removeTab()
   4917          // call before permitUnload() returns.
   4918          tab._pendingPermitUnload = true;
   4919          beforeUnloadPromises.push(
   4920            // To save time, we first run the beforeunload event listeners in all
   4921            // content processes in parallel. Tabs that would have shown a prompt
   4922            // will be handled again later.
   4923            tab.linkedBrowser.asyncPermitUnload("dontUnload").then(
   4924              ({ permitUnload }) => {
   4925                tab._pendingPermitUnload = false;
   4926                Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(
   4927                  timerId
   4928                );
   4929                if (tab.closing) {
   4930                  // The tab was closed by the user while we were in permitUnload, don't
   4931                  // attempt to close it a second time.
   4932                } else if (permitUnload) {
   4933                  if (!skipRemoves) {
   4934                    // OK to close without prompting, do it immediately.
   4935                    this.removeTab(tab, {
   4936                      animate,
   4937                      prewarmed: true,
   4938                      skipPermitUnload: true,
   4939                      skipSessionStore,
   4940                    });
   4941                  }
   4942                } else {
   4943                  // We will need to prompt, queue it so it happens sequentially.
   4944                  tabsWithBeforeUnloadPrompt.push(tab);
   4945                }
   4946              },
   4947              err => {
   4948                console.error("error while calling asyncPermitUnload", err);
   4949                tab._pendingPermitUnload = false;
   4950                Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(
   4951                  timerId
   4952                );
   4953              }
   4954            )
   4955          );
   4956        } else {
   4957          tabsWithoutBeforeUnload.push(tab);
   4958        }
   4959      }
   4960 
   4961      // Now that all the beforeunload IPCs have been sent to content processes,
   4962      // we can queue unload messages for all the tabs without beforeunload listeners.
   4963      // Doing this first would cause content process main threads to be busy and delay
   4964      // beforeunload responses, which would be user-visible.
   4965      if (!skipRemoves) {
   4966        for (let tab of tabsWithoutBeforeUnload) {
   4967          this.removeTab(tab, {
   4968            animate,
   4969            prewarmed: true,
   4970            skipPermitUnload,
   4971            skipSessionStore,
   4972            isUserTriggered,
   4973            telemetrySource,
   4974          });
   4975        }
   4976      }
   4977 
   4978      return {
   4979        beforeUnloadComplete: Promise.all(beforeUnloadPromises),
   4980        tabsWithBeforeUnloadPrompt,
   4981        lastToClose,
   4982      };
   4983    }
   4984 
   4985    /**
   4986     * Runs the before unload handler for the provided tabs, waiting for them
   4987     * to complete.
   4988     *
   4989     * This can be used in tandem with removeTabs to allow any before unload
   4990     * prompts to happen before any tab closures. This should only be used
   4991     * in the case where any prompts need to happen before other items before
   4992     * the actual tabs are closed.
   4993     *
   4994     * When using this function alongside removeTabs, specify the `skipUnload`
   4995     * option to removeTabs.
   4996     *
   4997     * @param {object[]} tabs
   4998     *   An array of tabs to remove.
   4999     * @returns {Promise<boolean>}
   5000     *   Returns true if the unload has been blocked by the user. False if tabs
   5001     *   may be subsequently closed.
   5002     */
   5003    async runBeforeUnloadForTabs(tabs) {
   5004      try {
   5005        let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt } =
   5006          this._startRemoveTabs(tabs, {
   5007            animate: false,
   5008            suppressWarnAboutClosingWindow: false,
   5009            skipPermitUnload: false,
   5010            skipRemoves: true,
   5011          });
   5012 
   5013        await beforeUnloadComplete;
   5014 
   5015        // Now run again sequentially the beforeunload listeners that will result in a prompt.
   5016        for (let tab of tabsWithBeforeUnloadPrompt) {
   5017          tab._pendingPermitUnload = true;
   5018          let { permitUnload } = this.getBrowserForTab(tab).permitUnload();
   5019          tab._pendingPermitUnload = false;
   5020          if (!permitUnload) {
   5021            return true;
   5022          }
   5023        }
   5024      } catch (e) {
   5025        console.error(e);
   5026      }
   5027      return false;
   5028    }
   5029 
   5030    /**
   5031     * Given an array of tabs, returns a tuple [groups, leftoverTabs] such that:
   5032     *  - groups contains all groups whose tabs are a subset of the initial array
   5033     *  - leftoverTabs contains the remaining tabs
   5034     *
   5035     * @param {Array} tabs list of tabs
   5036     * @returns {Array} a tuple where the first element is an array of groups
   5037     *                  and the second is an array of tabs
   5038     */
   5039    #separateWholeGroups(tabs) {
   5040      /**
   5041       * Map of tab group to surviving tabs in the group.
   5042       * If any of the `tabs` to be removed belong to a tab group, keep track
   5043       * of how many tabs in the tab group will be left after removing `tabs`.
   5044       * For any tab group with 0 surviving tabs, we can know that that tab
   5045       * group will be removed as a consequence of removing these `tabs`.
   5046       *
   5047       * @type {Map<MozTabbrowserTabGroup, Set<MozTabbrowserTab>>}
   5048       */
   5049      let tabGroupSurvivingTabs = new Map();
   5050      let wholeGroups = [];
   5051      for (let tab of tabs) {
   5052        if (tab.group) {
   5053          if (!tabGroupSurvivingTabs.has(tab.group)) {
   5054            tabGroupSurvivingTabs.set(tab.group, new Set(tab.group.tabs));
   5055          }
   5056          tabGroupSurvivingTabs.get(tab.group).delete(tab);
   5057        }
   5058      }
   5059 
   5060      for (let [tabGroup, survivingTabs] of tabGroupSurvivingTabs.entries()) {
   5061        if (!survivingTabs.size) {
   5062          wholeGroups.push(tabGroup);
   5063          tabs = tabs.filter(t => !tabGroup.tabs.includes(t));
   5064        }
   5065      }
   5066 
   5067      return [wholeGroups, tabs];
   5068    }
   5069 
   5070    /**
   5071     * Removes multiple tabs from the tab browser.
   5072     *
   5073     * @param {MozTabbrowserTab[]} tabs
   5074     *   The set of tabs to remove.
   5075     * @param {object} [options]
   5076     * @param {boolean} [options.animate]
   5077     *   Whether or not to animate closing, defaults to true.
   5078     * @param {boolean} [options.suppressWarnAboutClosingWindow]
   5079     *   This will supress the warning about closing a window with the last tab.
   5080     * @param {boolean} [options.skipPermitUnload]
   5081     *   Skips the before unload checks for the tabs. Only set this to true when
   5082     *   using it in tandem with `runBeforeUnloadForTabs`.
   5083     * @param {boolean}  [options.skipSessionStore]
   5084     *   If true, don't record the closed tabs in SessionStore.
   5085     * @param {boolean} [options.skipGroupCheck]
   5086     *   Skip separate processing of whole tab groups from the set of tabs.
   5087     *   Used by removeTabGroup.
   5088     * @param {boolean} [options.isUserTriggered]
   5089     *   Whether or not the removal is the direct result of a user action.
   5090     *   Used for telemetry.
   5091     * @param {string} [options.telemetrySource]
   5092     *   The system, surface, or control the user used to take this action.
   5093     *   @see TabMetrics.METRIC_SOURCE for possible values.
   5094     */
   5095    removeTabs(
   5096      tabs,
   5097      {
   5098        animate = true,
   5099        suppressWarnAboutClosingWindow = false,
   5100        skipPermitUnload = false,
   5101        skipSessionStore = false,
   5102        skipGroupCheck = false,
   5103        isUserTriggered = false,
   5104        telemetrySource,
   5105      } = {}
   5106    ) {
   5107      // When 'closeWindowWithLastTab' pref is enabled, closing all tabs
   5108      // can be considered equivalent to closing the window.
   5109      if (
   5110        this.tabs.length == tabs.length &&
   5111        Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
   5112      ) {
   5113        window.closeWindow(
   5114          true,
   5115          suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow,
   5116          "close-last-tab"
   5117        );
   5118        return;
   5119      }
   5120 
   5121      if (!skipSessionStore) {
   5122        SessionStore.resetLastClosedTabCount(window);
   5123      }
   5124      this._clearMultiSelectionLocked = true;
   5125 
   5126      // Guarantee that _clearMultiSelectionLocked lock gets released.
   5127      try {
   5128        // If selection includes entire groups, we might want to save them
   5129        if (!skipGroupCheck) {
   5130          let [groups, leftoverTabs] = this.#separateWholeGroups(tabs);
   5131          groups.forEach(group => {
   5132            if (!skipSessionStore) {
   5133              group.save();
   5134            }
   5135            this.removeTabGroup(group, {
   5136              animate,
   5137              skipSessionStore,
   5138              skipPermitUnload,
   5139              isUserTriggered,
   5140              telemetrySource,
   5141            });
   5142          });
   5143          tabs = leftoverTabs;
   5144        }
   5145 
   5146        let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt, lastToClose } =
   5147          this._startRemoveTabs(tabs, {
   5148            animate,
   5149            suppressWarnAboutClosingWindow,
   5150            skipPermitUnload,
   5151            skipRemoves: false,
   5152            skipSessionStore,
   5153            isUserTriggered,
   5154            telemetrySource,
   5155          });
   5156 
   5157        // Wait for all the beforeunload events to have been processed by content processes.
   5158        // The permitUnload() promise will, alas, not call its resolution
   5159        // callbacks after the browser window the promise lives in has closed,
   5160        // so we have to check for that case explicitly.
   5161        let done = false;
   5162        beforeUnloadComplete.then(() => {
   5163          done = true;
   5164        });
   5165        Services.tm.spinEventLoopUntilOrQuit(
   5166          "tabbrowser.js:removeTabs",
   5167          () => done || window.closed
   5168        );
   5169        if (!done) {
   5170          return;
   5171        }
   5172 
   5173        let aParams = {
   5174          animate,
   5175          prewarmed: true,
   5176          skipPermitUnload,
   5177          skipSessionStore,
   5178          isUserTriggered,
   5179          telemetrySource,
   5180        };
   5181 
   5182        // Now run again sequentially the beforeunload listeners that will result in a prompt.
   5183        for (let tab of tabsWithBeforeUnloadPrompt) {
   5184          this.removeTab(tab, aParams);
   5185          if (!tab.closing) {
   5186            // If we abort the closing of the tab.
   5187            tab._closedInMultiselection = false;
   5188          }
   5189        }
   5190 
   5191        // Avoid changing the selected browser several times by removing it,
   5192        // if appropriate, lastly.
   5193        if (lastToClose) {
   5194          this.removeTab(lastToClose, aParams);
   5195        }
   5196      } catch (e) {
   5197        console.error(e);
   5198      }
   5199 
   5200      this._clearMultiSelectionLocked = false;
   5201      this._avoidSingleSelectedTab();
   5202    }
   5203 
   5204    removeCurrentTab(aParams) {
   5205      this.removeTab(this.selectedTab, aParams);
   5206    }
   5207 
   5208    removeTab(
   5209      aTab,
   5210      {
   5211        animate,
   5212        triggeringEvent,
   5213        skipPermitUnload,
   5214        closeWindowWithLastTab,
   5215        prewarmed,
   5216        skipSessionStore,
   5217        isUserTriggered,
   5218        telemetrySource,
   5219      } = {}
   5220    ) {
   5221      if (UserInteraction.running("browser.tabs.opening", window)) {
   5222        UserInteraction.finish("browser.tabs.opening", window);
   5223      }
   5224 
   5225      // Telemetry stopwatches may already be running if removeTab gets
   5226      // called again for an already closing tab.
   5227      if (!aTab._closeTimeAnimTimerId && !aTab._closeTimeNoAnimTimerId) {
   5228        // Speculatevely start both stopwatches now. We'll cancel one of
   5229        // the two later depending on whether we're animating.
   5230        aTab._closeTimeAnimTimerId = Glean.browserTabclose.timeAnim.start();
   5231        aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
   5232      }
   5233 
   5234      // Handle requests for synchronously removing an already
   5235      // asynchronously closing tab.
   5236      if (!animate && aTab.closing) {
   5237        this._endRemoveTab(aTab);
   5238        return;
   5239      }
   5240 
   5241      let isVisibleTab = aTab.visible;
   5242      // We have to sample the tab width now, since _beginRemoveTab might
   5243      // end up modifying the DOM in such a way that aTab gets a new
   5244      // frame created for it (for example, by updating the visually selected
   5245      // state).
   5246      let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
   5247      let isLastTab = this.#isLastTabInWindow(aTab);
   5248      if (
   5249        !this._beginRemoveTab(aTab, {
   5250          closeWindowFastpath: true,
   5251          skipPermitUnload,
   5252          closeWindowWithLastTab,
   5253          prewarmed,
   5254          skipSessionStore,
   5255          isUserTriggered,
   5256          telemetrySource,
   5257        })
   5258      ) {
   5259        Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
   5260        aTab._closeTimeAnimTimerId = null;
   5261        Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId);
   5262        aTab._closeTimeNoAnimTimerId = null;
   5263        return;
   5264      }
   5265 
   5266      let lockTabSizing =
   5267        !this.tabContainer.verticalMode &&
   5268        !aTab.pinned &&
   5269        isVisibleTab &&
   5270        aTab._fullyOpen &&
   5271        triggeringEvent?.inputSource == MouseEvent.MOZ_SOURCE_MOUSE &&
   5272        triggeringEvent?.target.closest(".tabbrowser-tab");
   5273      if (lockTabSizing) {
   5274        this.tabContainer._lockTabSizing(aTab, tabWidth);
   5275      } else {
   5276        this.tabContainer._unlockTabSizing();
   5277      }
   5278 
   5279      if (
   5280        !animate /* the caller didn't opt in */ ||
   5281        gReduceMotion ||
   5282        isLastTab ||
   5283        aTab.pinned ||
   5284        !isVisibleTab ||
   5285        this.tabContainer.verticalMode ||
   5286        this._removingTabs.size >
   5287          3 /* don't want lots of concurrent animations */ ||
   5288        !aTab.hasAttribute(
   5289          "fadein"
   5290        ) /* fade-in transition hasn't been triggered yet */ ||
   5291        tabWidth == 0 /* fade-in transition hasn't moved yet */
   5292      ) {
   5293        // We're not animating, so we can cancel the animation stopwatch.
   5294        Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
   5295        aTab._closeTimeAnimTimerId = null;
   5296        this._endRemoveTab(aTab);
   5297        return;
   5298      }
   5299 
   5300      // We're animating, so we can cancel the non-animation stopwatch.
   5301      Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId);
   5302      aTab._closeTimeNoAnimTimerId = null;
   5303 
   5304      aTab.style.maxWidth = ""; // ensure that fade-out transition happens
   5305      aTab.removeAttribute("fadein");
   5306      aTab.removeAttribute("bursting");
   5307 
   5308      setTimeout(
   5309        function (tab, tabbrowser) {
   5310          if (
   5311            tab.container &&
   5312            window.getComputedStyle(tab).maxWidth == "0.1px"
   5313          ) {
   5314            console.assert(
   5315              false,
   5316              "Giving up waiting for the tab closing animation to finish (bug 608589)"
   5317            );
   5318            tabbrowser._endRemoveTab(tab);
   5319          }
   5320        },
   5321        3000,
   5322        aTab,
   5323        this
   5324      );
   5325    }
   5326 
   5327    /**
   5328     * Returns `true` if `tab` is the last tab in this window. This logic is
   5329     * intended for cases like determining if a window should close due to `tab`
   5330     * being closed, therefore hidden tabs are not considered in this function.
   5331     *
   5332     * Note: must be called before `tab` is closed/closing.
   5333     *
   5334     * @param {MozTabbrowserTab} tab
   5335     * @returns {boolean}
   5336     */
   5337    #isLastTabInWindow(tab) {
   5338      for (const otherTab of this.tabs) {
   5339        if (otherTab != tab && otherTab.isOpen && !otherTab.hidden) {
   5340          return false;
   5341        }
   5342      }
   5343      return true;
   5344    }
   5345 
   5346    _hasBeforeUnload(aTab) {
   5347      let browser = aTab.linkedBrowser;
   5348      if (browser.isRemoteBrowser && browser.frameLoader) {
   5349        return browser.hasBeforeUnload;
   5350      }
   5351      return false;
   5352    }
   5353 
   5354    _beginRemoveTab(
   5355      aTab,
   5356      {
   5357        adoptedByTab,
   5358        closeWindowWithLastTab,
   5359        closeWindowFastpath,
   5360        skipPermitUnload,
   5361        prewarmed,
   5362        skipSessionStore = false,
   5363        isUserTriggered,
   5364        telemetrySource,
   5365      } = {}
   5366    ) {
   5367      if (aTab.closing || this._windowIsClosing) {
   5368        return false;
   5369      }
   5370 
   5371      var browser = this.getBrowserForTab(aTab);
   5372      if (
   5373        !skipPermitUnload &&
   5374        !adoptedByTab &&
   5375        aTab.linkedPanel &&
   5376        !aTab._pendingPermitUnload &&
   5377        (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
   5378      ) {
   5379        if (!prewarmed) {
   5380          let blurTab = this._findTabToBlurTo(aTab);
   5381          if (blurTab) {
   5382            this.warmupTab(blurTab);
   5383          }
   5384        }
   5385 
   5386        let timerId = Glean.browserTabclose.permitUnloadTime.start();
   5387 
   5388        // We need to block while calling permitUnload() because it
   5389        // processes the event queue and may lead to another removeTab()
   5390        // call before permitUnload() returns.
   5391        aTab._pendingPermitUnload = true;
   5392        let { permitUnload } = browser.permitUnload();
   5393        aTab._pendingPermitUnload = false;
   5394 
   5395        Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(timerId);
   5396 
   5397        // If we were closed during onbeforeunload, we return false now
   5398        // so we don't (try to) close the same tab again. Of course, we
   5399        // also stop if the unload was cancelled by the user:
   5400        if (aTab.closing || !permitUnload) {
   5401          return false;
   5402        }
   5403      }
   5404 
   5405      this.tabContainer._invalidateCachedVisibleTabs();
   5406 
   5407      // this._switcher would normally cover removing a tab from this
   5408      // cache, but we may not have one at this time.
   5409      let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
   5410      if (tabCacheIndex != -1) {
   5411        this._tabLayerCache.splice(tabCacheIndex, 1);
   5412      }
   5413 
   5414      // Delay hiding the the active tab if we're screen sharing.
   5415      // See Bug 1642747.
   5416      let screenShareInActiveTab =
   5417        aTab == this.selectedTab && aTab._sharingState?.webRTC?.screen;
   5418 
   5419      if (!screenShareInActiveTab) {
   5420        this._blurTab(aTab);
   5421      }
   5422 
   5423      var closeWindow = false;
   5424      var newTab = false;
   5425      if (this.#isLastTabInWindow(aTab)) {
   5426        closeWindow =
   5427          closeWindowWithLastTab != null
   5428            ? closeWindowWithLastTab
   5429            : !window.toolbar.visible ||
   5430              Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
   5431 
   5432        if (closeWindow) {
   5433          // We've already called beforeunload on all the relevant tabs if we get here,
   5434          // so avoid calling it again:
   5435          window.skipNextCanClose = true;
   5436        }
   5437 
   5438        // Closing the tab and replacing it with a blank one is notably slower
   5439        // than closing the window right away. If the caller opts in, take
   5440        // the fast path.
   5441        if (closeWindow && closeWindowFastpath && !this._removingTabs.size) {
   5442          // This call actually closes the window, unless the user
   5443          // cancels the operation.  We are finished here in both cases.
   5444          this._windowIsClosing = window.closeWindow(
   5445            true,
   5446            window.warnAboutClosingWindow,
   5447            "close-last-tab"
   5448          );
   5449          return false;
   5450        }
   5451 
   5452        newTab = true;
   5453      }
   5454      aTab._endRemoveArgs = [closeWindow, newTab];
   5455 
   5456      // swapBrowsersAndCloseOther will take care of closing the window without animation.
   5457      if (closeWindow && adoptedByTab) {
   5458        // Remove the tab's filter and progress listener to avoid leaking.
   5459        if (aTab.linkedPanel) {
   5460          const filter = this._tabFilters.get(aTab);
   5461          browser.webProgress.removeProgressListener(filter);
   5462          const listener = this._tabListeners.get(aTab);
   5463          filter.removeProgressListener(listener);
   5464          listener.destroy();
   5465          this._tabListeners.delete(aTab);
   5466          this._tabFilters.delete(aTab);
   5467        }
   5468        return true;
   5469      }
   5470 
   5471      if (!aTab._fullyOpen) {
   5472        // If the opening tab animation hasn't finished before we start closing the
   5473        // tab, decrement the animation count since _handleNewTab will not get called.
   5474        this.tabAnimationsInProgress--;
   5475      }
   5476 
   5477      this.tabAnimationsInProgress++;
   5478 
   5479      // Mute audio immediately to improve perceived speed of tab closure.
   5480      if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
   5481        // Don't persist the muted state as this wasn't a user action.
   5482        // This lets undo-close-tab return it to an unmuted state.
   5483        aTab.linkedBrowser.mute(true);
   5484      }
   5485 
   5486      aTab.closing = true;
   5487      this._removingTabs.add(aTab);
   5488      this.tabContainer._invalidateCachedTabs();
   5489 
   5490      // Invalidate hovered tab state tracking for this closing tab.
   5491      aTab._mouseleave();
   5492 
   5493      if (newTab) {
   5494        this.addTrustedTab(BROWSER_NEW_TAB_URL, {
   5495          skipAnimation: true,
   5496          // In the event that insertAfterCurrent is set and the current tab is
   5497          // inside a group that is being closed we want to avoid creating the
   5498          // new tab inside that group.
   5499          tabIndex: 0,
   5500        });
   5501      } else {
   5502        TabBarVisibility.update();
   5503      }
   5504 
   5505      // Splice this tab out of any lines of succession before any events are
   5506      // dispatched.
   5507      this.replaceInSuccession(aTab, aTab.successor);
   5508      this.setSuccessor(aTab, null);
   5509 
   5510      // We're committed to closing the tab now.
   5511      // Dispatch a notification.
   5512      // We dispatch it before any teardown so that event listeners can
   5513      // inspect the tab that's about to close.
   5514      let evt = new CustomEvent("TabClose", {
   5515        bubbles: true,
   5516        detail: {
   5517          adoptedBy: adoptedByTab,
   5518          skipSessionStore,
   5519          isUserTriggered,
   5520          telemetrySource,
   5521        },
   5522      });
   5523      aTab.dispatchEvent(evt);
   5524 
   5525      if (this.tabs.length == 2) {
   5526        // We're closing one of our two open tabs, inform the other tab that its
   5527        // sibling is going away.
   5528        for (let tab of this.tabs) {
   5529          let bc = tab.linkedBrowser.browsingContext;
   5530          if (bc) {
   5531            bc.hasSiblings = false;
   5532          }
   5533        }
   5534      }
   5535 
   5536      let notificationBox = this.readNotificationBox(browser);
   5537      notificationBox?._stack?.remove();
   5538 
   5539      if (aTab.linkedPanel) {
   5540        if (!adoptedByTab && !gMultiProcessBrowser) {
   5541          // Prevent this tab from showing further dialogs, since we're closing it
   5542          browser.contentWindow.windowUtils.disableDialogs();
   5543        }
   5544 
   5545        // Remove the tab's filter and progress listener.
   5546        const filter = this._tabFilters.get(aTab);
   5547 
   5548        browser.webProgress.removeProgressListener(filter);
   5549 
   5550        const listener = this._tabListeners.get(aTab);
   5551        filter.removeProgressListener(listener);
   5552        listener.destroy();
   5553      }
   5554 
   5555      if (browser.registeredOpenURI && !adoptedByTab) {
   5556        let userContextId = browser.getAttribute("usercontextid") || 0;
   5557        this.UrlbarProviderOpenTabs.unregisterOpenTab(
   5558          browser.registeredOpenURI.spec,
   5559          userContextId,
   5560          aTab.group?.id,
   5561          PrivateBrowsingUtils.isWindowPrivate(window)
   5562        );
   5563        delete browser.registeredOpenURI;
   5564      }
   5565 
   5566      // We are no longer the primary content area.
   5567      browser.removeAttribute("primary");
   5568 
   5569      return true;
   5570    }
   5571 
   5572    _endRemoveTab(aTab) {
   5573      if (!aTab || !aTab._endRemoveArgs) {
   5574        return;
   5575      }
   5576 
   5577      var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
   5578      aTab._endRemoveArgs = null;
   5579 
   5580      if (this._windowIsClosing) {
   5581        aCloseWindow = false;
   5582        aNewTab = false;
   5583      }
   5584 
   5585      this.tabAnimationsInProgress--;
   5586 
   5587      this._lastRelatedTabMap = new WeakMap();
   5588 
   5589      // update the UI early for responsiveness
   5590      aTab.collapsed = true;
   5591      this._blurTab(aTab);
   5592 
   5593      this._removingTabs.delete(aTab);
   5594 
   5595      if (aCloseWindow) {
   5596        this._windowIsClosing = true;
   5597        for (let tab of this._removingTabs) {
   5598          this._endRemoveTab(tab);
   5599        }
   5600      } else if (!this._windowIsClosing) {
   5601        if (aNewTab) {
   5602          gURLBar.select();
   5603        }
   5604      }
   5605 
   5606      // We're going to remove the tab and the browser now.
   5607      this._tabFilters.delete(aTab);
   5608      this._tabListeners.delete(aTab);
   5609 
   5610      var browser = this.getBrowserForTab(aTab);
   5611 
   5612      if (aTab.linkedPanel) {
   5613        // Because of the fact that we are setting JS properties on
   5614        // the browser elements, and we have code in place
   5615        // to preserve the JS objects for any elements that have
   5616        // JS properties set on them, the browser element won't be
   5617        // destroyed until the document goes away.  So we force a
   5618        // cleanup ourselves.
   5619        // This has to happen before we remove the child since functions
   5620        // like `getBrowserContainer` expect the browser to be parented.
   5621        browser.destroy();
   5622      }
   5623 
   5624      // Remove the tab ...
   5625      aTab.remove();
   5626      this.tabContainer._invalidateCachedTabs();
   5627 
   5628      // ... and fix up the _tPos properties immediately.
   5629      for (let i = aTab._tPos; i < this.tabs.length; i++) {
   5630        this.tabs[i]._tPos = i;
   5631      }
   5632 
   5633      if (!this._windowIsClosing) {
   5634        // update tab close buttons state
   5635        this.tabContainer._updateCloseButtons();
   5636 
   5637        setTimeout(
   5638          function (tabs) {
   5639            tabs._lastTabClosedByMouse = false;
   5640          },
   5641          0,
   5642          this.tabContainer
   5643        );
   5644      }
   5645 
   5646      // update tab positional properties and attributes
   5647      this.selectedTab._selected = true;
   5648 
   5649      // Removing the panel requires fixing up selectedPanel immediately
   5650      // (see below), which would be hindered by the potentially expensive
   5651      // browser removal. So we remove the browser and the panel in two
   5652      // steps.
   5653 
   5654      var panel = this.getPanel(browser);
   5655 
   5656      // In the multi-process case, it's possible an asynchronous tab switch
   5657      // is still underway. If so, then it's possible that the last visible
   5658      // browser is the one we're in the process of removing. There's the
   5659      // risk of displaying preloaded browsers that are at the end of the
   5660      // deck if we remove the browser before the switch is complete, so
   5661      // we alert the switcher in order to show a spinner instead.
   5662      if (this._switcher) {
   5663        this._switcher.onTabRemoved(aTab);
   5664      }
   5665 
   5666      // This will unload the document. An unload handler could remove
   5667      // dependant tabs, so it's important that the tabbrowser is now in
   5668      // a consistent state (tab removed, tab positions updated, etc.).
   5669      browser.remove();
   5670 
   5671      // Release the browser in case something is erroneously holding a
   5672      // reference to the tab after its removal.
   5673      this._tabForBrowser.delete(aTab.linkedBrowser);
   5674      aTab.linkedBrowser = null;
   5675 
   5676      panel.remove();
   5677 
   5678      // closeWindow might wait an arbitrary length of time if we're supposed
   5679      // to warn about closing the window, so we'll just stop the tab close
   5680      // stopwatches here instead.
   5681      if (aTab._closeTimeAnimTimerId) {
   5682        Glean.browserTabclose.timeAnim.stopAndAccumulate(
   5683          aTab._closeTimeAnimTimerId
   5684        );
   5685        aTab._closeTimeAnimTimerId = null;
   5686      }
   5687      if (aTab._closeTimeNoAnimTimerId) {
   5688        Glean.browserTabclose.timeNoAnim.stopAndAccumulate(
   5689          aTab._closeTimeNoAnimTimerId
   5690        );
   5691        aTab._closeTimeNoAnimTimerId = null;
   5692      }
   5693 
   5694      if (aCloseWindow) {
   5695        this._windowIsClosing = closeWindow(
   5696          true,
   5697          window.warnAboutClosingWindow,
   5698          "close-last-tab"
   5699        );
   5700      }
   5701    }
   5702 
   5703    /**
   5704     * Closes all tabs matching the list of nsURIs.
   5705     * This does not close any tabs that have a beforeUnload prompt.
   5706     *
   5707     * @param {nsURI[]} urisToClose
   5708     *   The set of uris to remove.
   5709     * @returns {number} The count of successfully closed tabs.
   5710     */
   5711    async closeTabsByURI(urisToClose) {
   5712      let tabsToRemove = [];
   5713      for (let tab of this.tabs) {
   5714        let currentURI = tab.linkedBrowser.currentURI;
   5715        // Find any URI that matches the current tab's URI
   5716        const matchedIndex = urisToClose.findIndex(uriToClose =>
   5717          uriToClose.equals(currentURI)
   5718        );
   5719 
   5720        if (matchedIndex > -1) {
   5721          tabsToRemove.push(tab);
   5722        }
   5723      }
   5724 
   5725      let closedCount = 0;
   5726 
   5727      if (tabsToRemove.length) {
   5728        const { beforeUnloadComplete, lastToClose } = this._startRemoveTabs(
   5729          tabsToRemove,
   5730          {
   5731            animate: false,
   5732            suppressWarnAboutClosingWindow: true,
   5733            skipPermitUnload: false,
   5734            skipRemoves: false,
   5735            skipSessionStore: false,
   5736          }
   5737        );
   5738 
   5739        // Wait for the beforeUnload handlers to complete.
   5740        await beforeUnloadComplete;
   5741 
   5742        closedCount = tabsToRemove.length - (lastToClose ? 1 : 0);
   5743 
   5744        // _startRemoveTabs doesn't close the last tab in the window
   5745        // for this use case, we simply close it
   5746        if (lastToClose) {
   5747          this.removeTab(lastToClose);
   5748          closedCount++;
   5749        }
   5750      }
   5751      return closedCount;
   5752    }
   5753 
   5754    async explicitUnloadTabs(tabs) {
   5755      let unloadBlocked = await this.runBeforeUnloadForTabs(tabs);
   5756      if (unloadBlocked) {
   5757        return;
   5758      }
   5759      let unloadSelectedTab = false;
   5760      let allTabsUnloaded = false;
   5761      if (tabs.some(tab => tab.selected)) {
   5762        // Unloading the currently selected tab.
   5763        // Need to select a different one before unloading.
   5764        // Avoid selecting any tab we're unloading now or
   5765        // any tab that is already unloaded.
   5766        unloadSelectedTab = true;
   5767        const tabsToExclude = tabs.concat(
   5768          this.tabContainer.allTabs.filter(tab => !tab.linkedPanel)
   5769        );
   5770        let newTab = this._findTabToBlurTo(this.selectedTab, tabsToExclude);
   5771        if (newTab) {
   5772          this.selectedTab = newTab;
   5773        } else {
   5774          allTabsUnloaded = true;
   5775          // all tabs are unloaded - show Firefox View if it's present, otherwise open a new tab
   5776          // Firefox View counts as present if its tab is already open, or if the button
   5777          // is visible, so as to not do this in private browsing mode or if the user
   5778          // has removed the button from their toolbar (bug 1946432, bug 1989429)
   5779          let firefoxViewAvailable =
   5780            FirefoxViewHandler.tab &&
   5781            FirefoxViewHandler.button?.checkVisibility({
   5782              checkVisibilityCSS: true,
   5783              visibilityProperty: true,
   5784            });
   5785          if (firefoxViewAvailable) {
   5786            FirefoxViewHandler.openTab("opentabs");
   5787          } else {
   5788            this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
   5789              skipAnimation: true,
   5790            });
   5791          }
   5792        }
   5793      }
   5794      let memoryUsageBeforeUnload = await getTotalMemoryUsage();
   5795      let timeBeforeUnload = performance.now();
   5796      let numberOfTabsUnloaded = 0;
   5797      await Promise.all(tabs.map(tab => this.prepareDiscardBrowser(tab)));
   5798 
   5799      for (let tab of tabs) {
   5800        numberOfTabsUnloaded += this.discardBrowser(tab, true) ? 1 : 0;
   5801      }
   5802      let timeElapsed = Math.floor(performance.now() - timeBeforeUnload);
   5803      Glean.browserEngagement.tabExplicitUnload.record({
   5804        unload_selected_tab: unloadSelectedTab,
   5805        all_tabs_unloaded: allTabsUnloaded,
   5806        tabs_unloaded: numberOfTabsUnloaded,
   5807        memory_before: memoryUsageBeforeUnload,
   5808        memory_after: await getTotalMemoryUsage(),
   5809        time_to_unload_in_ms: timeElapsed,
   5810      });
   5811    }
   5812 
   5813    /**
   5814     * Handles opening a new tab with mouse middleclick.
   5815     *
   5816     * @param node
   5817     * @param event
   5818     *        The click event
   5819     */
   5820    handleNewTabMiddleClick(node, event) {
   5821      // We should be using the disabled property here instead of the attribute,
   5822      // but some elements that this function is used with don't support it (e.g.
   5823      // menuitem).
   5824      if (node.hasAttribute("disabled")) {
   5825        return;
   5826      } // Do nothing
   5827 
   5828      if (event.button == 1) {
   5829        BrowserCommands.openTab({ event });
   5830        // Stop the propagation of the click event, to prevent the event from being
   5831        // handled more than once.
   5832        // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4
   5833        event.stopPropagation();
   5834        event.preventDefault();
   5835      }
   5836    }
   5837 
   5838    /**
   5839     * Finds the tab that we will blur to if we blur aTab.
   5840     *
   5841     * @param   {MozTabbrowserTab} aTab
   5842     *          The tab we would blur
   5843     * @param   {MozTabbrowserTab[]} [aExcludeTabs=[]]
   5844     *          Tabs to exclude from our search (i.e., because they are being
   5845     *          closed along with aTab)
   5846     */
   5847    _findTabToBlurTo(aTab, aExcludeTabs = []) {
   5848      if (!aTab.selected) {
   5849        return null;
   5850      }
   5851      if (FirefoxViewHandler.tab) {
   5852        aExcludeTabs.push(FirefoxViewHandler.tab);
   5853      }
   5854 
   5855      let excludeTabs = new Set(aExcludeTabs);
   5856 
   5857      // If this tab has a successor, it should be selectable, since
   5858      // hiding or closing a tab removes that tab as a successor.
   5859      if (aTab.successor && !excludeTabs.has(aTab.successor)) {
   5860        return aTab.successor;
   5861      }
   5862 
   5863      if (
   5864        aTab.owner?.visible &&
   5865        !excludeTabs.has(aTab.owner) &&
   5866        Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
   5867      ) {
   5868        return aTab.owner;
   5869      }
   5870 
   5871      // Try to find a remaining tab that comes after the given tab
   5872      let remainingTabs = Array.prototype.filter.call(
   5873        this.visibleTabs,
   5874        tab => !excludeTabs.has(tab)
   5875      );
   5876 
   5877      let tab = this.tabContainer.findNextTab(aTab, {
   5878        direction: 1,
   5879        filter: _tab => remainingTabs.includes(_tab),
   5880      });
   5881 
   5882      if (!tab) {
   5883        tab = this.tabContainer.findNextTab(aTab, {
   5884          direction: -1,
   5885          filter: _tab => remainingTabs.includes(_tab),
   5886        });
   5887      }
   5888 
   5889      if (tab) {
   5890        return tab;
   5891      }
   5892 
   5893      // If no qualifying visible tab was found, see if there is a tab in
   5894      // a collapsed tab group that could be selected.
   5895      let eligibleTabs = new Set(this.tabsInCollapsedTabGroups).difference(
   5896        excludeTabs
   5897      );
   5898 
   5899      tab = this.tabContainer.findNextTab(aTab, {
   5900        direction: 1,
   5901        filter: _tab => eligibleTabs.has(_tab),
   5902      });
   5903 
   5904      if (!tab) {
   5905        tab = this.tabContainer.findNextTab(aTab, {
   5906          direction: -1,
   5907          filter: _tab => eligibleTabs.has(_tab),
   5908        });
   5909      }
   5910 
   5911      return tab;
   5912    }
   5913 
   5914    _blurTab(aTab) {
   5915      this.selectedTab = this._findTabToBlurTo(aTab);
   5916    }
   5917 
   5918    /**
   5919     * @returns {boolean}
   5920     *   False if swapping isn't permitted, true otherwise.
   5921     */
   5922    swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
   5923      // Do not allow transfering a private tab to a non-private window
   5924      // and vice versa.
   5925      if (
   5926        PrivateBrowsingUtils.isWindowPrivate(window) !=
   5927        PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal)
   5928      ) {
   5929        return false;
   5930      }
   5931 
   5932      // Do not allow transfering a useRemoteSubframes tab to a
   5933      // non-useRemoteSubframes window and vice versa.
   5934      if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) {
   5935        return false;
   5936      }
   5937 
   5938      let ourBrowser = this.getBrowserForTab(aOurTab);
   5939      let otherBrowser = aOtherTab.linkedBrowser;
   5940 
   5941      // Can't swap between chrome and content processes.
   5942      if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
   5943        return false;
   5944      }
   5945 
   5946      // Keep the userContextId if set on other browser
   5947      if (otherBrowser.hasAttribute("usercontextid")) {
   5948        ourBrowser.setAttribute(
   5949          "usercontextid",
   5950          otherBrowser.getAttribute("usercontextid")
   5951        );
   5952      }
   5953 
   5954      // That's gBrowser for the other window, not the tab's browser!
   5955      var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
   5956      var isPending = aOtherTab.hasAttribute("pending");
   5957 
   5958      let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
   5959      let stateFlags = 0;
   5960      if (otherTabListener) {
   5961        stateFlags = otherTabListener.mStateFlags;
   5962      }
   5963 
   5964      // Expedite the removal of the icon if it was already scheduled.
   5965      if (aOtherTab._soundPlayingAttrRemovalTimer) {
   5966        clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
   5967        aOtherTab._soundPlayingAttrRemovalTimer = 0;
   5968        aOtherTab.removeAttribute("soundplaying");
   5969        remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
   5970      }
   5971 
   5972      // First, start teardown of the other browser.  Make sure to not
   5973      // fire the beforeunload event in the process.  Close the other
   5974      // window if this was its last tab.
   5975      if (
   5976        !remoteBrowser._beginRemoveTab(aOtherTab, {
   5977          adoptedByTab: aOurTab,
   5978          closeWindowWithLastTab: true,
   5979        })
   5980      ) {
   5981        return false;
   5982      }
   5983 
   5984      // If this is the last tab of the window, hide the window
   5985      // immediately without animation before the docshell swap, to avoid
   5986      // about:blank being painted.
   5987      let [closeWindow] = aOtherTab._endRemoveArgs;
   5988      if (closeWindow) {
   5989        let win = aOtherTab.ownerGlobal;
   5990        win.windowUtils.suppressAnimation(true);
   5991        // Only suppressing window animations isn't enough to avoid
   5992        // an empty content area being painted.
   5993        let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
   5994        baseWin.visibility = false;
   5995      }
   5996 
   5997      let modifiedAttrs = [];
   5998      if (aOtherTab.hasAttribute("muted")) {
   5999        aOurTab.toggleAttribute("muted", true);
   6000        aOurTab.muteReason = aOtherTab.muteReason;
   6001        // For non-lazy tabs, mute() must be called.
   6002        if (aOurTab.linkedPanel) {
   6003          ourBrowser.mute();
   6004        }
   6005        modifiedAttrs.push("muted");
   6006      }
   6007      if (aOtherTab.hasAttribute("discarded")) {
   6008        aOurTab.toggleAttribute("discarded", true);
   6009        modifiedAttrs.push("discarded");
   6010      }
   6011      if (aOtherTab.hasAttribute("undiscardable")) {
   6012        aOurTab.toggleAttribute("undiscardable", true);
   6013        modifiedAttrs.push("undiscardable");
   6014      }
   6015      if (aOtherTab.hasAttribute("soundplaying")) {
   6016        aOurTab.toggleAttribute("soundplaying", true);
   6017        modifiedAttrs.push("soundplaying");
   6018      }
   6019      if (aOtherTab.hasAttribute("usercontextid")) {
   6020        aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
   6021        modifiedAttrs.push("usercontextid");
   6022      }
   6023      if (aOtherTab.hasAttribute("sharing")) {
   6024        aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
   6025        modifiedAttrs.push("sharing");
   6026        aOurTab._sharingState = aOtherTab._sharingState;
   6027        webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
   6028      }
   6029      if (aOtherTab.hasAttribute("pictureinpicture")) {
   6030        aOurTab.toggleAttribute("pictureinpicture", true);
   6031        modifiedAttrs.push("pictureinpicture");
   6032 
   6033        let event = new CustomEvent("TabSwapPictureInPicture", {
   6034          detail: aOurTab,
   6035        });
   6036        aOtherTab.dispatchEvent(event);
   6037      }
   6038 
   6039      // Copy tab note-related properties of the tab.
   6040      aOurTab.hasTabNote = aOtherTab.hasTabNote;
   6041      aOurTab.canonicalUrl = aOtherTab.canonicalUrl;
   6042 
   6043      if (otherBrowser.isDistinctProductPageVisit) {
   6044        ourBrowser.isDistinctProductPageVisit = true;
   6045      }
   6046 
   6047      SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
   6048 
   6049      // Add a reference to the original registeredOpenURI to the closing
   6050      // tab so that events operating on the tab before close can reference it.
   6051      aOtherTab._originalRegisteredOpenURI = otherBrowser.registeredOpenURI;
   6052 
   6053      // If the other tab is pending (i.e. has not been restored, yet)
   6054      // then do not switch docShells but retrieve the other tab's state
   6055      // and apply it to our tab.
   6056      if (isPending) {
   6057        // Tag tab so that the extension framework can ignore tab events that
   6058        // are triggered amidst the tab/browser restoration process
   6059        // (TabHide, TabPinned, TabUnpinned, "muted" attribute changes, etc.).
   6060        aOurTab.initializingTab = true;
   6061        delete ourBrowser._cachedCurrentURI;
   6062        SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
   6063        delete aOurTab.initializingTab;
   6064 
   6065        // Make sure to unregister any open URIs.
   6066        this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
   6067      } else {
   6068        // Workarounds for bug 458697
   6069        // Icon might have been set on DOMLinkAdded, don't override that.
   6070        if (!ourBrowser.mIconURL && otherBrowser.mIconURL) {
   6071          this.setIcon(aOurTab, otherBrowser.mIconURL);
   6072        }
   6073        var isBusy = aOtherTab.hasAttribute("busy");
   6074        if (isBusy) {
   6075          aOurTab.setAttribute("busy", "true");
   6076          modifiedAttrs.push("busy");
   6077          if (aOurTab.selected) {
   6078            this._isBusy = true;
   6079          }
   6080        }
   6081 
   6082        this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
   6083      }
   6084 
   6085      // Unregister the previously opened URI
   6086      if (otherBrowser.registeredOpenURI) {
   6087        let userContextId = otherBrowser.getAttribute("usercontextid") || 0;
   6088        this.UrlbarProviderOpenTabs.unregisterOpenTab(
   6089          otherBrowser.registeredOpenURI.spec,
   6090          userContextId,
   6091          aOtherTab.group?.id,
   6092          PrivateBrowsingUtils.isWindowPrivate(window)
   6093        );
   6094        delete otherBrowser.registeredOpenURI;
   6095      }
   6096 
   6097      // Handle findbar data (if any)
   6098      let otherFindBar = aOtherTab._findBar;
   6099      if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
   6100        let oldValue = otherFindBar._findField.value;
   6101        let wasHidden = otherFindBar.hidden;
   6102        let ourFindBarPromise = this.getFindBar(aOurTab);
   6103        ourFindBarPromise.then(ourFindBar => {
   6104          if (!ourFindBar) {
   6105            return;
   6106          }
   6107          ourFindBar._findField.value = oldValue;
   6108          if (!wasHidden) {
   6109            ourFindBar.onFindCommand();
   6110          }
   6111        });
   6112      }
   6113 
   6114      // Finish tearing down the tab that's going away.
   6115      if (closeWindow) {
   6116        aOtherTab.ownerGlobal.close();
   6117      } else {
   6118        remoteBrowser._endRemoveTab(aOtherTab);
   6119      }
   6120 
   6121      this.setTabTitle(aOurTab);
   6122 
   6123      // If the tab was already selected (this happens in the scenario
   6124      // of replaceTabWithWindow), notify onLocationChange, etc.
   6125      if (aOurTab.selected) {
   6126        this.updateCurrentBrowser(true);
   6127      }
   6128 
   6129      if (modifiedAttrs.length) {
   6130        this._tabAttrModified(aOurTab, modifiedAttrs);
   6131      }
   6132 
   6133      return true;
   6134    }
   6135 
   6136    swapBrowsers(aOurTab, aOtherTab) {
   6137      let otherBrowser = aOtherTab.linkedBrowser;
   6138      let otherTabBrowser = otherBrowser.getTabBrowser();
   6139 
   6140      // We aren't closing the other tab so, we also need to swap its tablisteners.
   6141      let filter = otherTabBrowser._tabFilters.get(aOtherTab);
   6142      let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
   6143      otherBrowser.webProgress.removeProgressListener(filter);
   6144      filter.removeProgressListener(tabListener);
   6145 
   6146      // Perform the docshell swap through the common mechanism.
   6147      this._swapBrowserDocShells(aOurTab, otherBrowser);
   6148 
   6149      // Restore the listeners for the swapped in tab.
   6150      tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(
   6151        aOtherTab,
   6152        otherBrowser,
   6153        false,
   6154        false
   6155      );
   6156      otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
   6157 
   6158      const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
   6159      filter.addProgressListener(tabListener, notifyAll);
   6160      otherBrowser.webProgress.addProgressListener(filter, notifyAll);
   6161    }
   6162 
   6163    _swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) {
   6164      // aOurTab's browser needs to be inserted now if it hasn't already.
   6165      this._insertBrowser(aOurTab);
   6166 
   6167      // Unhook our progress listener
   6168      const filter = this._tabFilters.get(aOurTab);
   6169      let tabListener = this._tabListeners.get(aOurTab);
   6170      let ourBrowser = this.getBrowserForTab(aOurTab);
   6171      ourBrowser.webProgress.removeProgressListener(filter);
   6172      filter.removeProgressListener(tabListener);
   6173 
   6174      // Make sure to unregister any open URIs.
   6175      this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
   6176 
   6177      let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;
   6178 
   6179      // If switcher is active, it will intercept swap events and
   6180      // react as needed.
   6181      if (!this._switcher) {
   6182        aOtherBrowser.docShellIsActive =
   6183          this.shouldActivateDocShell(ourBrowser);
   6184      }
   6185 
   6186      let ourBrowserContainer =
   6187        ourBrowser.ownerDocument.getElementById("browser");
   6188      let otherBrowserContainer =
   6189        aOtherBrowser.ownerDocument.getElementById("browser");
   6190      let ourBrowserContainerWasHidden = ourBrowserContainer.hidden;
   6191      let otherBrowserContainerWasHidden = otherBrowserContainer.hidden;
   6192 
   6193      // #browser is hidden in Customize Mode; this breaks docshell swapping,
   6194      // so we need to toggle 'hidden' to make swapping work in this case.
   6195      ourBrowserContainer.hidden = otherBrowserContainer.hidden = false;
   6196 
   6197      // Swap the docshells
   6198      ourBrowser.swapDocShells(aOtherBrowser);
   6199 
   6200      ourBrowserContainer.hidden = ourBrowserContainerWasHidden;
   6201      otherBrowserContainer.hidden = otherBrowserContainerWasHidden;
   6202 
   6203      // Swap permanentKey properties.
   6204      let ourPermanentKey = ourBrowser.permanentKey;
   6205      ourBrowser.permanentKey = aOtherBrowser.permanentKey;
   6206      aOtherBrowser.permanentKey = ourPermanentKey;
   6207      aOurTab.permanentKey = ourBrowser.permanentKey;
   6208      if (remoteBrowser) {
   6209        let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
   6210        if (otherTab) {
   6211          otherTab.permanentKey = aOtherBrowser.permanentKey;
   6212        }
   6213      }
   6214 
   6215      // Restore the progress listener
   6216      tabListener = new TabProgressListener(
   6217        aOurTab,
   6218        ourBrowser,
   6219        false,
   6220        false,
   6221        aStateFlags
   6222      );
   6223      this._tabListeners.set(aOurTab, tabListener);
   6224 
   6225      const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
   6226      filter.addProgressListener(tabListener, notifyAll);
   6227      ourBrowser.webProgress.addProgressListener(filter, notifyAll);
   6228    }
   6229 
   6230    _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
   6231      // Swap the registeredOpenURI properties of the two browsers
   6232      let tmp = aOurBrowser.registeredOpenURI;
   6233      delete aOurBrowser.registeredOpenURI;
   6234      if (aOtherBrowser.registeredOpenURI) {
   6235        aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
   6236        delete aOtherBrowser.registeredOpenURI;
   6237      }
   6238      if (tmp) {
   6239        aOtherBrowser.registeredOpenURI = tmp;
   6240      }
   6241    }
   6242 
   6243    reloadMultiSelectedTabs() {
   6244      this.reloadTabs(this.selectedTabs);
   6245    }
   6246 
   6247    reloadTabs(tabs) {
   6248      for (let tab of tabs) {
   6249        try {
   6250          this.getBrowserForTab(tab).reload();
   6251        } catch (e) {
   6252          // ignore failure to reload so others will be reloaded
   6253        }
   6254      }
   6255    }
   6256 
   6257    reloadTab(aTab) {
   6258      let browser = this.getBrowserForTab(aTab);
   6259      // Reset temporary permissions on the current tab. This is done here
   6260      // because we only want to reset permissions on user reload.
   6261      SitePermissions.clearTemporaryBlockPermissions(browser);
   6262      // Also reset DOS mitigations for the basic auth prompt on reload.
   6263      delete browser.authPromptAbuseCounter;
   6264      gIdentityHandler.hidePopup();
   6265      gPermissionPanel.hidePopup();
   6266      browser.reload();
   6267    }
   6268 
   6269    addProgressListener(aListener) {
   6270      if (arguments.length != 1) {
   6271        console.error(
   6272          "gBrowser.addProgressListener was " +
   6273            "called with a second argument, " +
   6274            "which is not supported. See bug " +
   6275            "608628. Call stack: ",
   6276          new Error().stack
   6277        );
   6278      }
   6279 
   6280      this.mProgressListeners.push(aListener);
   6281    }
   6282 
   6283    removeProgressListener(aListener) {
   6284      this.mProgressListeners = this.mProgressListeners.filter(
   6285        l => l != aListener
   6286      );
   6287    }
   6288 
   6289    addTabsProgressListener(aListener) {
   6290      this.mTabsProgressListeners.push(aListener);
   6291    }
   6292 
   6293    removeTabsProgressListener(aListener) {
   6294      this.mTabsProgressListeners = this.mTabsProgressListeners.filter(
   6295        l => l != aListener
   6296      );
   6297    }
   6298 
   6299    getBrowserForTab(aTab) {
   6300      return aTab.linkedBrowser;
   6301    }
   6302 
   6303    showTab(aTab) {
   6304      if (!aTab.hidden || aTab == FirefoxViewHandler.tab) {
   6305        return;
   6306      }
   6307      aTab.removeAttribute("hidden");
   6308      this.tabContainer._invalidateCachedVisibleTabs();
   6309 
   6310      this.tabContainer._updateCloseButtons();
   6311      if (aTab.multiselected) {
   6312        this._updateMultiselectedTabCloseButtonTooltip();
   6313      }
   6314 
   6315      let event = document.createEvent("Events");
   6316      event.initEvent("TabShow", true, false);
   6317      aTab.dispatchEvent(event);
   6318      SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
   6319    }
   6320 
   6321    hideTab(aTab, aSource) {
   6322      if (
   6323        aTab.hidden ||
   6324        aTab.pinned ||
   6325        aTab.selected ||
   6326        aTab.closing ||
   6327        // Tabs that are sharing the screen, microphone or camera cannot be hidden.
   6328        aTab._sharingState?.webRTC?.sharing
   6329      ) {
   6330        return;
   6331      }
   6332      aTab.setAttribute("hidden", "true");
   6333      this.tabContainer._invalidateCachedVisibleTabs();
   6334 
   6335      this.tabContainer._updateCloseButtons();
   6336      if (aTab.multiselected) {
   6337        this._updateMultiselectedTabCloseButtonTooltip();
   6338      }
   6339 
   6340      // Splice this tab out of any lines of succession before any events are
   6341      // dispatched.
   6342      this.replaceInSuccession(aTab, aTab.successor);
   6343      this.setSuccessor(aTab, null);
   6344 
   6345      let event = document.createEvent("Events");
   6346      event.initEvent("TabHide", true, false);
   6347      aTab.dispatchEvent(event);
   6348      if (aSource) {
   6349        SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
   6350      }
   6351    }
   6352 
   6353    selectTabAtIndex(aIndex, aEvent) {
   6354      let tabs = this.visibleTabs;
   6355 
   6356      // count backwards for aIndex < 0
   6357      if (aIndex < 0) {
   6358        aIndex += tabs.length;
   6359        // clamp at index 0 if still negative.
   6360        if (aIndex < 0) {
   6361          aIndex = 0;
   6362        }
   6363      } else if (aIndex >= tabs.length) {
   6364        // clamp at right-most tab if out of range.
   6365        aIndex = tabs.length - 1;
   6366      }
   6367 
   6368      this.selectedTab = tabs[aIndex];
   6369 
   6370      if (aEvent) {
   6371        aEvent.preventDefault();
   6372        aEvent.stopPropagation();
   6373      }
   6374    }
   6375 
   6376    /**
   6377     * Moves a tab to a new browser window, unless it's already the only tab
   6378     * in the current window, in which case this will do nothing.
   6379     *
   6380     * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
   6381     */
   6382    replaceTabWithWindow(aTab, aOptions) {
   6383      if (this.tabs.length == 1) {
   6384        return null;
   6385      }
   6386      // TODO bug 1967925: Consider handling the case where aTab is a tab group
   6387      // and also the only tab group in its window.
   6388 
   6389      var options = "chrome,dialog=no,all";
   6390      for (var name in aOptions) {
   6391        options += "," + name + "=" + aOptions[name];
   6392      }
   6393 
   6394      if (PrivateBrowsingUtils.isWindowPrivate(window)) {
   6395        options += ",private=1";
   6396      }
   6397 
   6398      // Play the tab closing animation to give immediate feedback while
   6399      // waiting for the new window to appear.
   6400      if (!gReduceMotion && this.isTab(aTab)) {
   6401        aTab.style.maxWidth = ""; // ensure that fade-out transition happens
   6402        aTab.removeAttribute("fadein");
   6403      }
   6404 
   6405      // tell a new window to take the "dropped" tab
   6406      return window.openDialog(
   6407        AppConstants.BROWSER_CHROME_URL,
   6408        "_blank",
   6409        options,
   6410        aTab
   6411      );
   6412    }
   6413 
   6414    /**
   6415     * Move contextTab (or selected tabs in a mutli-select context)
   6416     * to a new browser window, unless it is (they are) already the only tab(s)
   6417     * in the current window, in which case this will do nothing.
   6418     */
   6419    replaceTabsWithWindow(contextTab, aOptions = {}) {
   6420      if (this.isTabGroupLabel(contextTab)) {
   6421        // TODO bug 1967937: Pass contextTab.group instead.
   6422        return this.replaceTabWithWindow(contextTab, aOptions);
   6423      }
   6424 
   6425      let tabs;
   6426      if (contextTab.multiselected) {
   6427        tabs = this.selectedTabs;
   6428      } else {
   6429        tabs = [contextTab];
   6430      }
   6431 
   6432      if (this.tabs.length == tabs.length) {
   6433        return null;
   6434      }
   6435 
   6436      if (tabs.length == 1) {
   6437        return this.replaceTabWithWindow(tabs[0], aOptions);
   6438      }
   6439 
   6440      // Play the closing animation for all selected tabs to give
   6441      // immediate feedback while waiting for the new window to appear.
   6442      if (!gReduceMotion) {
   6443        for (let tab of tabs) {
   6444          tab.style.maxWidth = ""; // ensure that fade-out transition happens
   6445          tab.removeAttribute("fadein");
   6446        }
   6447      }
   6448 
   6449      // Create a new window and make it adopt the tabs, preserving their relative order.
   6450      // The initial tab of the new window will be selected, so it should adopt the
   6451      // selected tab of the original window, if applicable, or else the first moving tab.
   6452      // This avoids tab-switches in the new window, preserving tab laziness.
   6453      // However, to avoid multiple tab-switches in the original window, the other tabs
   6454      // should be adopted before the selected one.
   6455      let { selectedTab } = gBrowser;
   6456      if (!tabs.includes(selectedTab)) {
   6457        selectedTab = tabs[0];
   6458      }
   6459 
   6460      let win = this.replaceTabWithWindow(selectedTab, aOptions);
   6461      win.addEventListener(
   6462        "before-initial-tab-adopted",
   6463        () => {
   6464          let tabIndex = 0;
   6465          for (let tab of tabs) {
   6466            if (tab !== selectedTab) {
   6467              const newTab = win.gBrowser.adoptTab(tab, { tabIndex });
   6468              if (!newTab) {
   6469                // The adoption failed. Restore "fadein" and don't increase the index.
   6470                tab.setAttribute("fadein", "true");
   6471                continue;
   6472              }
   6473            }
   6474 
   6475            ++tabIndex;
   6476          }
   6477          // Restore tab selection
   6478          let winVisibleTabs = win.gBrowser.visibleTabs;
   6479          let winTabLength = winVisibleTabs.length;
   6480          win.gBrowser.addRangeToMultiSelectedTabs(
   6481            winVisibleTabs[0],
   6482            winVisibleTabs[winTabLength - 1]
   6483          );
   6484          win.gBrowser.lockClearMultiSelectionOnce();
   6485        },
   6486        { once: true }
   6487      );
   6488      return win;
   6489    }
   6490 
   6491    /**
   6492     * Moves group to a new window.
   6493     *
   6494     * @param {MozTabbrowserTabGroup} group
   6495     *   The tab group to move.
   6496     */
   6497    replaceGroupWithWindow(group) {
   6498      return this.replaceTabWithWindow(group);
   6499    }
   6500 
   6501    /**
   6502     * @param {Element} element
   6503     * @returns {boolean}
   6504     *   `true` if element is a `<tab>`
   6505     */
   6506    isTab(element) {
   6507      return !!(element?.tagName == "tab");
   6508    }
   6509 
   6510    /**
   6511     * @param {Element} element
   6512     * @returns {boolean}
   6513     *   `true` if element is a `<tab-group>`
   6514     */
   6515    isTabGroup(element) {
   6516      return !!(element?.tagName == "tab-group");
   6517    }
   6518 
   6519    /**
   6520     * @param {Element} element
   6521     * @returns {boolean}
   6522     *   `true` if element is the `<label>` in a `<tab-group>`
   6523     */
   6524    isTabGroupLabel(element) {
   6525      return !!element?.classList?.contains("tab-group-label");
   6526    }
   6527 
   6528    /**
   6529     * @param {Element} element
   6530     * @returns {boolean}
   6531     *   `true` if element is a `<tab-split-view-wrapper>`
   6532     */
   6533    isSplitViewWrapper(element) {
   6534      return !!(element?.tagName == "tab-split-view-wrapper");
   6535    }
   6536 
   6537    _updateTabsAfterInsert() {
   6538      for (let i = 0; i < this.tabs.length; i++) {
   6539        this.tabs[i]._tPos = i;
   6540        this.tabs[i]._selected = false;
   6541      }
   6542 
   6543      // If we're in the midst of an async tab switch while calling
   6544      // moveTabTo, we can get into a case where _visuallySelected
   6545      // is set to true on two different tabs.
   6546      //
   6547      // What we want to do in moveTabTo is to remove logical selection
   6548      // from all tabs, and then re-add logical selection to selectedTab
   6549      // (and visual selection as well if we're not running with e10s, which
   6550      // setting _selected will do automatically).
   6551      //
   6552      // If we're running with e10s, then the visual selection will not
   6553      // be changed, which is fine, since if we weren't in the midst of a
   6554      // tab switch, the previously visually selected tab should still be
   6555      // correct, and if we are in the midst of a tab switch, then the async
   6556      // tab switcher will set the visually selected tab once the tab switch
   6557      // has completed.
   6558      this.selectedTab._selected = true;
   6559    }
   6560 
   6561    /**
   6562     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
   6563     *   The tab or tab group to move. Also accepts a tab group label as a
   6564     *   stand-in for its group.
   6565     * @param {object} [options]
   6566     * @param {number} [options.tabIndex]
   6567     *   The desired position, expressed as the index within the `tabs` array.
   6568     * @param {number} [options.elementIndex]
   6569     *   The desired position, expressed as the index within the
   6570     *   `MozTabbrowserTabs::dragAndDropElements` array.
   6571     * @param {boolean} [options.forceUngrouped=false]
   6572     *   Force `element` to move into position as a standalone tab, overriding
   6573     *   any possibility of entering a tab group. For example, setting `true`
   6574     *   ensures that a pinned tab will not accidentally be placed inside of
   6575     *   a tab group, since pinned tabs are presently not allowed in tab groups.
   6576     * @property {boolean} [options.isUserTriggered=false]
   6577     *   Should be true if there was an explicit action/request from the user
   6578     *   (as opposed to some action being taken internally or for technical
   6579     *   bookkeeping reasons alone) to move the tab. This causes telemetry
   6580     *   events to fire.
   6581     * @property {string} [options.telemetrySource="unknown"]
   6582     *   The system, surface, or control the user used to move the tab.
   6583     *   @see TabMetrics.METRIC_SOURCE for possible values.
   6584     *   Defaults to "unknown".
   6585     */
   6586    moveTabTo(
   6587      element,
   6588      {
   6589        elementIndex,
   6590        tabIndex,
   6591        forceUngrouped = false,
   6592        isUserTriggered = false,
   6593        telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN,
   6594      } = {}
   6595    ) {
   6596      if (typeof elementIndex == "number") {
   6597        tabIndex = this.#elementIndexToTabIndex(elementIndex);
   6598      }
   6599 
   6600      // Don't allow mixing pinned and unpinned tabs.
   6601      if (this.isTab(element) && element.pinned) {
   6602        tabIndex = Math.min(tabIndex, this.pinnedTabCount - 1);
   6603      } else {
   6604        tabIndex = Math.max(tabIndex, this.pinnedTabCount);
   6605      }
   6606 
   6607      // Return early if the tab is already in the right spot.
   6608      if (
   6609        this.isTab(element) &&
   6610        element._tPos == tabIndex &&
   6611        !(element.group && forceUngrouped)
   6612      ) {
   6613        return;
   6614      }
   6615 
   6616      // When asked to move a tab group label, we need to move the whole group
   6617      // instead.
   6618      if (this.isTabGroupLabel(element)) {
   6619        element = element.group;
   6620      }
   6621      if (this.isTabGroup(element)) {
   6622        forceUngrouped = true;
   6623      }
   6624 
   6625      this.#handleTabMove(
   6626        element,
   6627        () => {
   6628          let neighbor = this.tabs[tabIndex];
   6629          if (forceUngrouped && neighbor?.group) {
   6630            neighbor = neighbor.group;
   6631          }
   6632          if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
   6633            neighbor.after(element);
   6634          } else {
   6635            this.tabContainer.insertBefore(element, neighbor);
   6636          }
   6637        },
   6638        { isUserTriggered, telemetrySource }
   6639      );
   6640    }
   6641 
   6642    /**
   6643     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
   6644     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6645     * @param {TabMetricsContext} [metricsContext]
   6646     */
   6647    moveTabBefore(element, targetElement, metricsContext) {
   6648      this.#moveTabNextTo(element, targetElement, true, metricsContext);
   6649    }
   6650 
   6651    /**
   6652     * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements
   6653     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6654     * @param {TabMetricsContext} [metricsContext]
   6655     */
   6656    moveTabsBefore(elements, targetElement, metricsContext) {
   6657      this.#moveTabsNextTo(elements, targetElement, true, metricsContext);
   6658    }
   6659 
   6660    /**
   6661     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
   6662     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6663     * @param {TabMetricsContext} [metricsContext]
   6664     */
   6665    moveTabAfter(element, targetElement, metricsContext) {
   6666      this.#moveTabNextTo(element, targetElement, false, metricsContext);
   6667    }
   6668 
   6669    /**
   6670     * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements
   6671     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6672     * @param {TabMetricsContext} [metricsContext]
   6673     */
   6674    moveTabsAfter(elements, targetElement, metricsContext) {
   6675      this.#moveTabsNextTo(elements, targetElement, false, metricsContext);
   6676    }
   6677 
   6678    /**
   6679     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
   6680     *   The tab or tab group to move. Also accepts a tab group label as a
   6681     *   stand-in for its group.
   6682     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6683     * @param {boolean} [moveBefore=false]
   6684     * @param {TabMetricsContext} [metricsContext]
   6685     */
   6686    #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
   6687      if (this.isTabGroupLabel(targetElement)) {
   6688        targetElement = targetElement.group;
   6689        if (!moveBefore && !targetElement.collapsed) {
   6690          // Right after the tab group label = before the first tab in the tab group
   6691          targetElement = targetElement.tabs[0];
   6692          moveBefore = true;
   6693        }
   6694      }
   6695      if (this.isTabGroupLabel(element)) {
   6696        element = element.group;
   6697        if (targetElement?.group) {
   6698          targetElement = targetElement.group;
   6699        }
   6700      }
   6701 
   6702      // Don't allow mixing pinned and unpinned tabs.
   6703      if (element.pinned && !targetElement?.pinned) {
   6704        targetElement = this.tabs[this.pinnedTabCount - 1];
   6705        moveBefore = false;
   6706      } else if (!element.pinned && targetElement && targetElement.pinned) {
   6707        // If the caller asks to move an unpinned element next to a pinned
   6708        // tab, move the unpinned element to be the first unpinned element
   6709        // in the tab strip. Potential scenarios:
   6710        // 1. Moving an unpinned tab and the first unpinned tab is ungrouped:
   6711        //    move the unpinned tab right before the first unpinned tab.
   6712        // 2. Moving an unpinned tab and the first unpinned tab is grouped:
   6713        //    move the unpinned tab right before the tab group.
   6714        // 3. Moving a tab group and the first unpinned tab is ungrouped:
   6715        //    move the tab group right before the first unpinned tab.
   6716        // 4. Moving a tab group and the first unpinned tab is grouped:
   6717        //    move the tab group right before the first unpinned tab's tab group.
   6718        targetElement = this.tabs[this.pinnedTabCount];
   6719        if (targetElement.group) {
   6720          targetElement = targetElement.group;
   6721        }
   6722        moveBefore = true;
   6723      }
   6724 
   6725      let getContainer = () =>
   6726        element.pinned
   6727          ? this.tabContainer.pinnedTabsContainer
   6728          : this.tabContainer;
   6729 
   6730      this.#handleTabMove(
   6731        element,
   6732        () => {
   6733          if (moveBefore) {
   6734            getContainer().insertBefore(element, targetElement);
   6735          } else if (targetElement) {
   6736            targetElement.after(element);
   6737          } else {
   6738            getContainer().appendChild(element);
   6739          }
   6740        },
   6741        metricsContext
   6742      );
   6743    }
   6744 
   6745    /**
   6746     * @param {MozTabbrowserTab[]} elements
   6747     * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
   6748     * @param {boolean} [moveBefore=false]
   6749     * @param {TabMetricsContext} [metricsContext]
   6750     */
   6751    #moveTabsNextTo(
   6752      elements,
   6753      targetElement,
   6754      moveBefore = false,
   6755      metricsContext
   6756    ) {
   6757      this.#moveTabNextTo(
   6758        elements[0],
   6759        targetElement,
   6760        moveBefore,
   6761        metricsContext
   6762      );
   6763      for (let i = 1; i < elements.length; i++) {
   6764        this.#moveTabNextTo(
   6765          elements[i],
   6766          elements[i - 1],
   6767          false,
   6768          metricsContext
   6769        );
   6770      }
   6771    }
   6772 
   6773    /**
   6774     *
   6775     * @param {MozTabbrowserTab} aTab
   6776     * @param {MozTabSplitViewWrapper} aSplitViewWrapper
   6777     */
   6778    moveTabToSplitView(aTab, aSplitViewWrapper) {
   6779      if (!this.isTab(aTab)) {
   6780        throw new Error("Can only move a tab into a split view wrapper");
   6781      }
   6782      if (aTab.pinned) {
   6783        return;
   6784      }
   6785      if (
   6786        aTab.splitview &&
   6787        aTab.splitview.splitViewId === aSplitViewWrapper.splitViewId
   6788      ) {
   6789        return;
   6790      }
   6791 
   6792      this.#handleTabMove(aTab, () => aSplitViewWrapper.appendChild(aTab));
   6793      this.removeFromMultiSelectedTabs(aTab);
   6794      this.tabContainer._notifyBackgroundTab(aTab);
   6795    }
   6796 
   6797    /**
   6798     *
   6799     * @param {MozTabbrowserTab} aTab
   6800     * @param {MozTabbrowserTabGroup} aGroup
   6801     * @param {TabMetricsContext} [metricsContext]
   6802     */
   6803    moveTabToExistingGroup(aTab, aGroup, metricsContext) {
   6804      if (!this.isTab(aTab)) {
   6805        throw new Error("Can only move a tab into a tab group");
   6806      }
   6807      if (aTab.pinned) {
   6808        return;
   6809      }
   6810      if (aTab.group && aTab.group.id === aGroup.id) {
   6811        return;
   6812      }
   6813      if (aTab.splitview) {
   6814        let splitViewTabs = aTab.splitview.tabs;
   6815        this.#handleTabMove(
   6816          aTab.splitview,
   6817          () => aGroup.appendChild(aTab.splitview),
   6818          metricsContext
   6819        );
   6820        for (const splitViewTab of splitViewTabs) {
   6821          this.removeFromMultiSelectedTabs(splitViewTab);
   6822          this.tabContainer._notifyBackgroundTab(splitViewTab);
   6823        }
   6824      } else {
   6825        this.#handleTabMove(
   6826          aTab,
   6827          () => aGroup.appendChild(aTab),
   6828          metricsContext
   6829        );
   6830        this.removeFromMultiSelectedTabs(aTab);
   6831        this.tabContainer._notifyBackgroundTab(aTab);
   6832      }
   6833    }
   6834 
   6835    /**
   6836     *
   6837     * @param {MozSplitViewWrapper} aSplitView
   6838     * @param {MozTabbrowserTabGroup} aGroup
   6839     * @param {TabMetricsContext} [metricsContext]
   6840     */
   6841    moveSplitViewToExistingGroup(aSplitView, aGroup, metricsContext = null) {
   6842      if (!this.isSplitViewWrapper(aSplitView)) {
   6843        throw new Error("Can only move a split view into a tab group");
   6844      }
   6845      if (aSplitView.group && aSplitView.group.id === aGroup.id) {
   6846        return;
   6847      }
   6848 
   6849      let splitViewTabs = aSplitView.tabs;
   6850      this.#handleTabMove(
   6851        aSplitView,
   6852        () => aGroup.appendChild(aSplitView),
   6853        metricsContext
   6854      );
   6855      for (const splitViewTab of splitViewTabs) {
   6856        this.removeFromMultiSelectedTabs(splitViewTab);
   6857        this.tabContainer._notifyBackgroundTab(splitViewTab);
   6858      }
   6859    }
   6860 
   6861    /**
   6862     * @typedef {object} TabMoveState
   6863     * @property {number} tabIndex
   6864     * @property {number} [elementIndex]
   6865     * @property {string} [tabGroupId]
   6866     */
   6867 
   6868    /**
   6869     * @param {MozTabbrowserTab} tab
   6870     * @returns {TabMoveState|undefined}
   6871     */
   6872    #getTabMoveState(tab) {
   6873      if (!this.isTab(tab)) {
   6874        return undefined;
   6875      }
   6876 
   6877      let state = {
   6878        tabIndex: tab._tPos,
   6879      };
   6880      if (tab.visible) {
   6881        state.elementIndex = tab.elementIndex;
   6882      }
   6883      if (tab.group) {
   6884        state.tabGroupId = tab.group.id;
   6885      }
   6886      if (tab.splitview) {
   6887        state.splitViewId = tab.splitview.splitViewId;
   6888      }
   6889      return state;
   6890    }
   6891 
   6892    /**
   6893     * @param {MozTabbrowserTab} tab
   6894     * @param {TabMoveState} [previousTabState]
   6895     * @param {TabMoveState} [currentTabState]
   6896     * @param {TabMetricsContext} [metricsContext]
   6897     */
   6898    #notifyOnTabMove(tab, previousTabState, currentTabState, metricsContext) {
   6899      if (!this.isTab(tab) || !previousTabState || !currentTabState) {
   6900        return;
   6901      }
   6902 
   6903      let changedPosition =
   6904        previousTabState.tabIndex != currentTabState.tabIndex;
   6905      let changedTabGroup =
   6906        previousTabState.tabGroupId != currentTabState.tabGroupId;
   6907 
   6908      if (changedPosition || changedTabGroup) {
   6909        tab.dispatchEvent(
   6910          new CustomEvent("TabMove", {
   6911            bubbles: true,
   6912            detail: {
   6913              previousTabState,
   6914              currentTabState,
   6915              isUserTriggered: metricsContext?.isUserTriggered ?? false,
   6916              telemetrySource:
   6917                metricsContext?.telemetrySource ??
   6918                this.TabMetrics.METRIC_SOURCE.UNKNOWN,
   6919            },
   6920          })
   6921        );
   6922      }
   6923    }
   6924 
   6925    /**
   6926     * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabSplitViewWrapper} element
   6927     * @param {function():void} moveActionCallback
   6928     * @param {TabMetricsContext} [metricsContext]
   6929     */
   6930    #handleTabMove(element, moveActionCallback, metricsContext) {
   6931      let tabs;
   6932      if (this.isTab(element)) {
   6933        tabs = [element];
   6934      } else if (this.isTabGroup(element) || this.isSplitViewWrapper(element)) {
   6935        tabs = element.tabs;
   6936      } else {
   6937        throw new Error(
   6938          "Can only move a tab, tab group, or split view within the tab bar"
   6939        );
   6940      }
   6941 
   6942      let wasFocused = document.activeElement == this.selectedTab;
   6943      let previousTabStates = tabs.map(tab => this.#getTabMoveState(tab));
   6944 
   6945      moveActionCallback();
   6946 
   6947      // Clear tabs cache after moving nodes because the order of tabs may have
   6948      // changed.
   6949      this.tabContainer._invalidateCachedTabs();
   6950      this._lastRelatedTabMap = new WeakMap();
   6951      this._updateTabsAfterInsert();
   6952 
   6953      if (wasFocused) {
   6954        this.selectedTab.focus();
   6955      }
   6956 
   6957      // When a tab group with multiple tabs is moved forwards, emit TabMove in
   6958      // the reverse order, so that the index in previousTabState values are
   6959      // still accurate until the event is dispatched. If we were to start with
   6960      // the front tab, then logically that tab moves, and all following tabs
   6961      // would shift, which would invalidate the index in previousTabState.
   6962      let reverseEvents =
   6963        tabs.length > 1 && tabs[0]._tPos > previousTabStates[0].tabIndex;
   6964 
   6965      for (let i = 0; i < tabs.length; i++) {
   6966        let ii = reverseEvents ? tabs.length - i - 1 : i;
   6967        let tab = tabs[ii];
   6968        if (tab.selected) {
   6969          this.tabContainer._handleTabSelect(true);
   6970        }
   6971 
   6972        let currentTabState = this.#getTabMoveState(tab);
   6973        this.#notifyOnTabMove(
   6974          tab,
   6975          previousTabStates[ii],
   6976          currentTabState,
   6977          metricsContext
   6978        );
   6979      }
   6980 
   6981      let currentFirst = this.#getTabMoveState(tabs[0]);
   6982      if (
   6983        this.isTabGroup(element) &&
   6984        previousTabStates[0].tabIndex != currentFirst.tabIndex
   6985      ) {
   6986        let event = new CustomEvent("TabGroupMoved", { bubbles: true });
   6987        element.dispatchEvent(event);
   6988      }
   6989    }
   6990 
   6991    /**
   6992     * Adopts a tab from another browser window, and inserts it at the given index.
   6993     *
   6994     * @returns {object}
   6995     *    The new tab in the current window, null if the tab couldn't be adopted.
   6996     */
   6997    adoptTab(aTab, { elementIndex, tabIndex, selectTab = false } = {}) {
   6998      // Swap the dropped tab with a new one we create and then close
   6999      // it in the other window (making it seem to have moved between
   7000      // windows). We also ensure that the tab we create to swap into has
   7001      // the same remote type and process as the one we're swapping in.
   7002      // This makes sure we don't get a short-lived process for the new tab.
   7003      let linkedBrowser = aTab.linkedBrowser;
   7004      let createLazyBrowser = !aTab.linkedPanel;
   7005      let index;
   7006      let nextElement;
   7007      if (typeof elementIndex == "number") {
   7008        index = elementIndex;
   7009        nextElement = this.tabContainer.dragAndDropElements.at(elementIndex);
   7010      } else {
   7011        index = tabIndex;
   7012        nextElement = this.tabs.at(tabIndex);
   7013      }
   7014      let tabInGroup = !!aTab.group;
   7015      let params = {
   7016        eventDetail: { adoptedTab: aTab },
   7017        preferredRemoteType: linkedBrowser.remoteType,
   7018        initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
   7019        skipAnimation: true,
   7020        elementIndex,
   7021        tabIndex,
   7022        tabGroup: this.isTab(nextElement) && nextElement.group,
   7023        createLazyBrowser,
   7024      };
   7025 
   7026      // We want to explicitly set this param rather than carry it over to
   7027      // avoid situations like an unpinned tab being dragged between pinned
   7028      // tabs but not getting pinned as expected.
   7029      let numPinned = this.pinnedTabCount;
   7030      if (index < numPinned || (aTab.pinned && index == numPinned)) {
   7031        params.pinned = true;
   7032      }
   7033 
   7034      if (aTab.hasAttribute("usercontextid")) {
   7035        // new tab must have the same usercontextid as the old one
   7036        params.userContextId = aTab.getAttribute("usercontextid");
   7037      }
   7038      params.skipLoad = true;
   7039      let newTab = this.addWebTab("about:blank", params);
   7040 
   7041      aTab.container.tabDragAndDrop.finishAnimateTabMove();
   7042 
   7043      if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
   7044        // Swapping wasn't permitted. Bail out.
   7045        this.removeTab(newTab);
   7046        return null;
   7047      }
   7048 
   7049      if (selectTab) {
   7050        this.selectedTab = newTab;
   7051      }
   7052 
   7053      if (tabInGroup) {
   7054        Glean.tabgroup.tabInteractions.remove_other_window.add();
   7055      }
   7056 
   7057      return newTab;
   7058    }
   7059 
   7060    moveTabForward() {
   7061      let { selectedTab } = this;
   7062      let nextTab = this.tabContainer.findNextTab(selectedTab, {
   7063        direction: DIRECTION_FORWARD,
   7064        filter: tab => !tab.hidden && selectedTab.pinned == tab.pinned,
   7065      });
   7066      if (nextTab) {
   7067        this.#handleTabMove(selectedTab, () => {
   7068          if (!selectedTab.group && nextTab.group) {
   7069            if (nextTab.group.collapsed) {
   7070              // Skip over collapsed tab group.
   7071              nextTab.group.after(selectedTab);
   7072            } else {
   7073              // Enter first position of tab group.
   7074              nextTab.group.insertBefore(selectedTab, nextTab);
   7075            }
   7076          } else if (selectedTab.group != nextTab.group) {
   7077            // Standalone tab after tab group.
   7078            selectedTab.group.after(selectedTab);
   7079          } else {
   7080            nextTab.after(selectedTab);
   7081          }
   7082        });
   7083      } else if (selectedTab.group) {
   7084        // selectedTab is the last tab and is grouped.
   7085        // remove it from its group.
   7086        selectedTab.group.after(selectedTab);
   7087      }
   7088    }
   7089 
   7090    moveTabBackward() {
   7091      let { selectedTab } = this;
   7092 
   7093      let previousTab = this.tabContainer.findNextTab(selectedTab, {
   7094        direction: DIRECTION_BACKWARD,
   7095        filter: tab => !tab.hidden && selectedTab.pinned == tab.pinned,
   7096      });
   7097 
   7098      if (previousTab) {
   7099        this.#handleTabMove(selectedTab, () => {
   7100          if (!selectedTab.group && previousTab.group) {
   7101            if (previousTab.group.collapsed) {
   7102              // Skip over collapsed tab group.
   7103              previousTab.group.before(selectedTab);
   7104            } else {
   7105              // Enter last position of tab group.
   7106              previousTab.group.append(selectedTab);
   7107            }
   7108          } else if (selectedTab.group != previousTab.group) {
   7109            // Standalone tab before tab group.
   7110            selectedTab.group.before(selectedTab);
   7111          } else {
   7112            previousTab.before(selectedTab);
   7113          }
   7114        });
   7115      } else if (selectedTab.group) {
   7116        // selectedTab is the first tab and is grouped.
   7117        // remove it from its group.
   7118        selectedTab.group.before(selectedTab);
   7119      }
   7120    }
   7121 
   7122    moveTabToStart(aTab = this.selectedTab) {
   7123      this.moveTabTo(aTab, { tabIndex: 0, forceUngrouped: true });
   7124    }
   7125 
   7126    moveTabToEnd(aTab = this.selectedTab) {
   7127      this.moveTabTo(aTab, {
   7128        tabIndex: this.tabs.length - 1,
   7129        forceUngrouped: true,
   7130      });
   7131    }
   7132 
   7133    /**
   7134     * @param   aTab
   7135     *          Can be from a different window as well
   7136     * @param   aRestoreTabImmediately
   7137     *          Can defer loading of the tab contents
   7138     * @param   aOptions
   7139     *          The new index of the tab
   7140     */
   7141    duplicateTab(aTab, aRestoreTabImmediately, aOptions) {
   7142      let newTab = SessionStore.duplicateTab(
   7143        window,
   7144        aTab,
   7145        0,
   7146        aRestoreTabImmediately,
   7147        aOptions
   7148      );
   7149      if (aTab.group) {
   7150        Glean.tabgroup.tabInteractions.duplicate.add();
   7151      }
   7152      return newTab;
   7153    }
   7154 
   7155    /**
   7156     * Update accessible names of close buttons in the (multi) selected tabs
   7157     * collection with how many tabs they will close
   7158     */
   7159    _updateMultiselectedTabCloseButtonTooltip() {
   7160      const tabCount = gBrowser.selectedTabs.length;
   7161      gBrowser.selectedTabs.forEach(selectedTab => {
   7162        document.l10n.setArgs(selectedTab.querySelector(".tab-close-button"), {
   7163          tabCount,
   7164        });
   7165      });
   7166    }
   7167 
   7168    addToMultiSelectedTabs(aTab) {
   7169      if (aTab.multiselected) {
   7170        return;
   7171      }
   7172 
   7173      aTab.setAttribute("multiselected", "true");
   7174      aTab.setAttribute("aria-selected", "true");
   7175      this._multiSelectedTabsSet.add(aTab);
   7176      this._startMultiSelectChange();
   7177      if (this._multiSelectChangeRemovals.has(aTab)) {
   7178        this._multiSelectChangeRemovals.delete(aTab);
   7179      } else {
   7180        this._multiSelectChangeAdditions.add(aTab);
   7181      }
   7182 
   7183      this._updateMultiselectedTabCloseButtonTooltip();
   7184    }
   7185 
   7186    /**
   7187     * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
   7188     */
   7189    addRangeToMultiSelectedTabs(aTab1, aTab2) {
   7190      if (aTab1 == aTab2) {
   7191        return;
   7192      }
   7193 
   7194      const tabs = this.visibleTabs;
   7195      const indexOfTab1 = tabs.indexOf(aTab1);
   7196      const indexOfTab2 = tabs.indexOf(aTab2);
   7197 
   7198      const [lowerIndex, higherIndex] =
   7199        indexOfTab1 < indexOfTab2
   7200          ? [Math.max(0, indexOfTab1), indexOfTab2]
   7201          : [Math.max(0, indexOfTab2), indexOfTab1];
   7202 
   7203      for (let i = lowerIndex; i <= higherIndex; i++) {
   7204        this.addToMultiSelectedTabs(tabs[i]);
   7205      }
   7206 
   7207      this._updateMultiselectedTabCloseButtonTooltip();
   7208    }
   7209 
   7210    removeFromMultiSelectedTabs(aTab) {
   7211      if (!aTab.multiselected) {
   7212        return;
   7213      }
   7214      aTab.removeAttribute("multiselected");
   7215      aTab.removeAttribute("aria-selected");
   7216      this._multiSelectedTabsSet.delete(aTab);
   7217      this._startMultiSelectChange();
   7218      if (this._multiSelectChangeAdditions.has(aTab)) {
   7219        this._multiSelectChangeAdditions.delete(aTab);
   7220      } else {
   7221        this._multiSelectChangeRemovals.add(aTab);
   7222      }
   7223      // Update labels for Close buttons of the remaining multiselected tabs:
   7224      this._updateMultiselectedTabCloseButtonTooltip();
   7225      // Update the label for the Close button of the tab being removed
   7226      // from the multiselection:
   7227      document.l10n.setArgs(aTab.querySelector(".tab-close-button"), {
   7228        tabCount: 1,
   7229      });
   7230    }
   7231 
   7232    clearMultiSelectedTabs() {
   7233      if (this._clearMultiSelectionLocked) {
   7234        if (this._clearMultiSelectionLockedOnce) {
   7235          this._clearMultiSelectionLockedOnce = false;
   7236          this._clearMultiSelectionLocked = false;
   7237        }
   7238        return;
   7239      }
   7240 
   7241      if (this.multiSelectedTabsCount < 1) {
   7242        return;
   7243      }
   7244 
   7245      for (let tab of this.selectedTabs) {
   7246        this.removeFromMultiSelectedTabs(tab);
   7247      }
   7248      this._lastMultiSelectedTabRef = null;
   7249    }
   7250 
   7251    selectAllTabs() {
   7252      let visibleTabs = this.visibleTabs;
   7253      gBrowser.addRangeToMultiSelectedTabs(
   7254        visibleTabs[0],
   7255        visibleTabs[visibleTabs.length - 1]
   7256      );
   7257    }
   7258 
   7259    allTabsSelected() {
   7260      return (
   7261        this.visibleTabs.length == 1 ||
   7262        this.visibleTabs.every(t => t.multiselected)
   7263      );
   7264    }
   7265 
   7266    lockClearMultiSelectionOnce() {
   7267      this._clearMultiSelectionLockedOnce = true;
   7268      this._clearMultiSelectionLocked = true;
   7269    }
   7270 
   7271    unlockClearMultiSelection() {
   7272      this._clearMultiSelectionLockedOnce = false;
   7273      this._clearMultiSelectionLocked = false;
   7274    }
   7275 
   7276    /**
   7277     * Remove a tab from the multiselection if it's the only one left there.
   7278     *
   7279     * In fact, some scenario may lead to only one single tab multi-selected,
   7280     * this is something to avoid (Chrome does the same)
   7281     * Consider 4 tabs A,B,C,D with A having the focus
   7282     * 1. select C with Ctrl
   7283     * 2. Right-click on B and "Close Tabs to The Right"
   7284     *
   7285     * Expected result
   7286     * C and D closing
   7287     * A being the only multi-selected tab, selection should be cleared
   7288     *
   7289     *
   7290     * Single selected tab could even happen with a none-focused tab.
   7291     * For exemple with the menu "Close other tabs", it could happen
   7292     * with a multi-selected pinned tab.
   7293     * For illustration, consider 4 tabs A,B,C,D with B active
   7294     * 1. pin A and Ctrl-select it
   7295     * 2. Ctrl-select C
   7296     * 3. right-click on D and click "Close Other Tabs"
   7297     *
   7298     * Expected result
   7299     * B and C closing
   7300     * A[pinned] being the only multi-selected tab, selection should be cleared.
   7301     */
   7302    _avoidSingleSelectedTab() {
   7303      if (this.multiSelectedTabsCount == 1) {
   7304        this.clearMultiSelectedTabs();
   7305      }
   7306    }
   7307 
   7308    _switchToNextMultiSelectedTab() {
   7309      this._clearMultiSelectionLocked = true;
   7310 
   7311      // Guarantee that _clearMultiSelectionLocked lock gets released.
   7312      try {
   7313        let lastMultiSelectedTab = this.lastMultiSelectedTab;
   7314        if (!lastMultiSelectedTab.selected) {
   7315          this.selectedTab = lastMultiSelectedTab;
   7316        } else {
   7317          let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(
   7318            this._multiSelectedTabsSet
   7319          ).filter(this._mayTabBeMultiselected);
   7320          this.selectedTab = selectedTabs.at(-1);
   7321        }
   7322      } catch (e) {
   7323        console.error(e);
   7324      }
   7325 
   7326      this._clearMultiSelectionLocked = false;
   7327    }
   7328 
   7329    set selectedTabs(tabs) {
   7330      this.clearMultiSelectedTabs();
   7331      this.selectedTab = tabs[0];
   7332      if (tabs.length > 1) {
   7333        for (let tab of tabs) {
   7334          this.addToMultiSelectedTabs(tab);
   7335        }
   7336      }
   7337    }
   7338 
   7339    get selectedTabs() {
   7340      let { selectedTab, _multiSelectedTabsSet } = this;
   7341      let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(
   7342        _multiSelectedTabsSet
   7343      ).filter(this._mayTabBeMultiselected);
   7344      if (
   7345        (!_multiSelectedTabsSet.has(selectedTab) &&
   7346          this._mayTabBeMultiselected(selectedTab)) ||
   7347        !tabs.length
   7348      ) {
   7349        tabs.push(selectedTab);
   7350      }
   7351      return tabs.sort((a, b) => a._tPos > b._tPos);
   7352    }
   7353 
   7354    get multiSelectedTabsCount() {
   7355      return ChromeUtils.nondeterministicGetWeakSetKeys(
   7356        this._multiSelectedTabsSet
   7357      ).filter(this._mayTabBeMultiselected).length;
   7358    }
   7359 
   7360    get lastMultiSelectedTab() {
   7361      let tab = this._lastMultiSelectedTabRef
   7362        ? this._lastMultiSelectedTabRef.get()
   7363        : null;
   7364      if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) {
   7365        return tab;
   7366      }
   7367      let selectedTab = this.selectedTab;
   7368      this.lastMultiSelectedTab = selectedTab;
   7369      return selectedTab;
   7370    }
   7371 
   7372    set lastMultiSelectedTab(aTab) {
   7373      this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
   7374    }
   7375 
   7376    _mayTabBeMultiselected(aTab) {
   7377      return aTab.visible;
   7378    }
   7379 
   7380    _startMultiSelectChange() {
   7381      if (!this._multiSelectChangeStarted) {
   7382        this._multiSelectChangeStarted = true;
   7383        Promise.resolve().then(() => this._endMultiSelectChange());
   7384      }
   7385    }
   7386 
   7387    _endMultiSelectChange() {
   7388      let noticeable = false;
   7389      let { selectedTab } = this;
   7390      if (this._multiSelectChangeAdditions.size) {
   7391        if (!selectedTab.multiselected) {
   7392          this.addToMultiSelectedTabs(selectedTab);
   7393        }
   7394        noticeable = true;
   7395      }
   7396      if (this._multiSelectChangeRemovals.size) {
   7397        if (this._multiSelectChangeRemovals.has(selectedTab)) {
   7398          this._switchToNextMultiSelectedTab();
   7399        }
   7400        this._avoidSingleSelectedTab();
   7401        noticeable = true;
   7402      }
   7403      this._multiSelectChangeStarted = false;
   7404      if (noticeable || this._multiSelectChangeSelected) {
   7405        this._multiSelectChangeSelected = false;
   7406        this._multiSelectChangeAdditions.clear();
   7407        this._multiSelectChangeRemovals.clear();
   7408        this.dispatchEvent(
   7409          new CustomEvent("TabMultiSelect", { bubbles: true })
   7410        );
   7411      }
   7412    }
   7413 
   7414    toggleMuteAudioOnMultiSelectedTabs(aTab) {
   7415      let tabMuted = aTab.linkedBrowser.audioMuted;
   7416      let tabsToToggle = this.selectedTabs.filter(
   7417        tab => tab.linkedBrowser.audioMuted == tabMuted
   7418      );
   7419      for (let tab of tabsToToggle) {
   7420        tab.toggleMuteAudio();
   7421      }
   7422    }
   7423 
   7424    resumeDelayedMediaOnMultiSelectedTabs() {
   7425      for (let tab of this.selectedTabs) {
   7426        tab.resumeDelayedMedia();
   7427      }
   7428    }
   7429 
   7430    pinMultiSelectedTabs() {
   7431      for (let tab of this.selectedTabs) {
   7432        this.pinTab(tab);
   7433      }
   7434    }
   7435 
   7436    unpinMultiSelectedTabs() {
   7437      // The selectedTabs getter returns the tabs
   7438      // in visual order. We need to unpin in reverse
   7439      // order to maintain visual order.
   7440      let selectedTabs = this.selectedTabs;
   7441      for (let i = selectedTabs.length - 1; i >= 0; i--) {
   7442        let tab = selectedTabs[i];
   7443        this.unpinTab(tab);
   7444      }
   7445    }
   7446 
   7447    activateBrowserForPrintPreview(aBrowser) {
   7448      this._printPreviewBrowsers.add(aBrowser);
   7449      if (this._switcher) {
   7450        this._switcher.activateBrowserForPrintPreview(aBrowser);
   7451      }
   7452      aBrowser.docShellIsActive = true;
   7453    }
   7454 
   7455    deactivatePrintPreviewBrowsers() {
   7456      let browsers = this._printPreviewBrowsers;
   7457      this._printPreviewBrowsers = new Set();
   7458      for (let browser of browsers) {
   7459        browser.docShellIsActive = this.shouldActivateDocShell(browser);
   7460      }
   7461    }
   7462 
   7463    /**
   7464     * Returns true if a given browser's docshell should be active.
   7465     */
   7466    shouldActivateDocShell(aBrowser) {
   7467      if (this._switcher) {
   7468        return this._switcher.shouldActivateDocShell(aBrowser);
   7469      }
   7470      return (
   7471        (aBrowser == this.selectedBrowser && !document.hidden) ||
   7472        this._printPreviewBrowsers.has(aBrowser) ||
   7473        this.PictureInPicture.isOriginatingBrowser(aBrowser)
   7474      );
   7475    }
   7476 
   7477    _getSwitcher() {
   7478      if (!this._switcher) {
   7479        this._switcher = new this.AsyncTabSwitcher(this);
   7480      }
   7481      return this._switcher;
   7482    }
   7483 
   7484    warmupTab(aTab) {
   7485      if (gMultiProcessBrowser) {
   7486        this._getSwitcher().warmupTab(aTab);
   7487      }
   7488    }
   7489 
   7490    /**
   7491     * _maybeRequestReplyFromRemoteContent may call
   7492     * aEvent.requestReplyFromRemoteContent if necessary.
   7493     *
   7494     * @param aEvent    The handling event.
   7495     * @return          true if the handler should wait a reply event.
   7496     *                  false if the handle can handle the immediately.
   7497     */
   7498    _maybeRequestReplyFromRemoteContent(aEvent) {
   7499      if (aEvent.defaultPrevented) {
   7500        return false;
   7501      }
   7502      // If the event target is a remote browser, and the event has not been
   7503      // handled by the remote content yet, we should wait a reply event
   7504      // from the content.
   7505      if (aEvent.isWaitingReplyFromRemoteContent) {
   7506        return true; // Somebody called requestReplyFromRemoteContent already.
   7507      }
   7508      if (
   7509        !aEvent.isReplyEventFromRemoteContent &&
   7510        aEvent.target?.isRemoteBrowser === true
   7511      ) {
   7512        aEvent.requestReplyFromRemoteContent();
   7513        return true;
   7514      }
   7515      return false;
   7516    }
   7517 
   7518    _handleKeyDownEvent(aEvent) {
   7519      if (!aEvent.isTrusted) {
   7520        // Don't let untrusted events mess with tabs.
   7521        return;
   7522      }
   7523 
   7524      // Skip this only if something has explicitly cancelled it.
   7525      if (aEvent.defaultCancelled) {
   7526        return;
   7527      }
   7528 
   7529      // Skip if chrome code has cancelled this:
   7530      if (aEvent.defaultPreventedByChrome) {
   7531        return;
   7532      }
   7533 
   7534      // Don't check if the event was already consumed because tab
   7535      // navigation should always work for better user experience.
   7536 
   7537      switch (ShortcutUtils.getSystemActionForEvent(aEvent)) {
   7538        case ShortcutUtils.TOGGLE_CARET_BROWSING:
   7539          this._maybeRequestReplyFromRemoteContent(aEvent);
   7540          return;
   7541        case ShortcutUtils.MOVE_TAB_BACKWARD:
   7542          this.moveTabBackward();
   7543          aEvent.preventDefault();
   7544          return;
   7545        case ShortcutUtils.MOVE_TAB_FORWARD:
   7546          this.moveTabForward();
   7547          aEvent.preventDefault();
   7548          return;
   7549        case ShortcutUtils.CLOSE_TAB:
   7550          if (gBrowser.multiSelectedTabsCount) {
   7551            gBrowser.removeMultiSelectedTabs();
   7552          } else if (!this.selectedTab.pinned) {
   7553            this.removeCurrentTab({ animate: true });
   7554          }
   7555          aEvent.preventDefault();
   7556      }
   7557    }
   7558 
   7559    toggleCaretBrowsing() {
   7560      const kPrefShortcutEnabled =
   7561        "accessibility.browsewithcaret_shortcut.enabled";
   7562      const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
   7563      const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
   7564 
   7565      var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled);
   7566      if (!isEnabled || this._awaitingToggleCaretBrowsingPrompt) {
   7567        return;
   7568      }
   7569 
   7570      // Toggle browse with caret mode
   7571      var browseWithCaretOn = Services.prefs.getBoolPref(
   7572        kPrefCaretBrowsingOn,
   7573        false
   7574      );
   7575      var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true);
   7576      if (warn && !browseWithCaretOn) {
   7577        var checkValue = { value: false };
   7578        var promptService = Services.prompt;
   7579 
   7580        try {
   7581          this._awaitingToggleCaretBrowsingPrompt = true;
   7582          const [title, message, checkbox] =
   7583            this.tabLocalization.formatValuesSync([
   7584              "tabbrowser-confirm-caretbrowsing-title",
   7585              "tabbrowser-confirm-caretbrowsing-message",
   7586              "tabbrowser-confirm-caretbrowsing-checkbox",
   7587            ]);
   7588          var buttonPressed = promptService.confirmEx(
   7589            window,
   7590            title,
   7591            message,
   7592            // Make "No" the default:
   7593            promptService.STD_YES_NO_BUTTONS |
   7594              promptService.BUTTON_POS_1_DEFAULT,
   7595            null,
   7596            null,
   7597            null,
   7598            checkbox,
   7599            checkValue
   7600          );
   7601        } catch (ex) {
   7602          return;
   7603        } finally {
   7604          this._awaitingToggleCaretBrowsingPrompt = false;
   7605        }
   7606        if (buttonPressed != 0) {
   7607          if (checkValue.value) {
   7608            try {
   7609              Services.prefs.setBoolPref(kPrefShortcutEnabled, false);
   7610            } catch (ex) {}
   7611          }
   7612          return;
   7613        }
   7614        if (checkValue.value) {
   7615          try {
   7616            Services.prefs.setBoolPref(kPrefWarnOnEnable, false);
   7617          } catch (ex) {}
   7618        }
   7619      }
   7620 
   7621      // Toggle the pref
   7622      try {
   7623        Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
   7624      } catch (ex) {}
   7625    }
   7626 
   7627    _handleKeyPressEvent(aEvent) {
   7628      if (!aEvent.isTrusted) {
   7629        // Don't let untrusted events mess with tabs.
   7630        return;
   7631      }
   7632 
   7633      // Skip this only if something has explicitly cancelled it.
   7634      if (aEvent.defaultCancelled) {
   7635        return;
   7636      }
   7637 
   7638      // Skip if chrome code has cancelled this:
   7639      if (aEvent.defaultPreventedByChrome) {
   7640        return;
   7641      }
   7642 
   7643      switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) {
   7644        case ShortcutUtils.TOGGLE_CARET_BROWSING:
   7645          if (
   7646            aEvent.defaultPrevented ||
   7647            this._maybeRequestReplyFromRemoteContent(aEvent)
   7648          ) {
   7649            break;
   7650          }
   7651          this.toggleCaretBrowsing();
   7652          break;
   7653 
   7654        case ShortcutUtils.NEXT_TAB:
   7655          if (AppConstants.platform == "macosx") {
   7656            this.tabContainer.advanceSelectedTab(DIRECTION_FORWARD, true);
   7657            aEvent.preventDefault();
   7658          }
   7659          break;
   7660        case ShortcutUtils.PREVIOUS_TAB:
   7661          if (AppConstants.platform == "macosx") {
   7662            this.tabContainer.advanceSelectedTab(DIRECTION_BACKWARD, true);
   7663            aEvent.preventDefault();
   7664          }
   7665          break;
   7666      }
   7667    }
   7668 
   7669    /**
   7670     *
   7671     * @param {MozTabbrowserTab} tab
   7672     */
   7673    #isFirstOrLastInTabGroup(tab) {
   7674      if (tab.group) {
   7675        let groupTabs = tab.group.tabs;
   7676        if (groupTabs.at(0) == tab || groupTabs.at(-1) == tab) {
   7677          return true;
   7678        }
   7679      }
   7680      return false;
   7681    }
   7682 
   7683    getTabPids(tab) {
   7684      if (!tab?.linkedBrowser) {
   7685        return [];
   7686      }
   7687 
   7688      // Get the PIDs of the content process and remote subframe processes
   7689      let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
   7690        tab.linkedBrowser,
   7691        gFissionBrowser
   7692      );
   7693      let pids = contentPid ? [contentPid] : [];
   7694      return pids.concat(framePids.sort());
   7695    }
   7696 
   7697    /**
   7698     * @param {MozTabbrowserTab} tab
   7699     * @param {boolean} [includeLabel=true]
   7700     *   Include the tab's title/full label in the tooltip. Defaults to true,
   7701     *   Can be disabled for contexts where including the title in the tooltip
   7702     *   string would be duplicative would already available information,
   7703     *   e.g. accessibility descriptions.
   7704     * @returns {string}
   7705     */
   7706    getTabTooltip(tab, includeLabel = true) {
   7707      let labelArray = [];
   7708      if (includeLabel) {
   7709        labelArray.push(tab._fullLabel || tab.getAttribute("label"));
   7710      }
   7711      if (this.showPidAndActiveness) {
   7712        const pids = this.getTabPids(tab);
   7713        let debugStringArray = [];
   7714        if (pids.length) {
   7715          let pidLabel = pids.length > 1 ? "pids" : "pid";
   7716          debugStringArray.push(`(${pidLabel} ${pids.join(", ")})`);
   7717        }
   7718 
   7719        if (tab.linkedBrowser.docShellIsActive) {
   7720          debugStringArray.push("[A]");
   7721        }
   7722 
   7723        if (debugStringArray.length) {
   7724          labelArray.push(debugStringArray.join(" "));
   7725        }
   7726      }
   7727 
   7728      // Add a line to the tooltip with additional tab context (e.g. container
   7729      let containerName = tab.userContextId
   7730        ? ContextualIdentityService.getUserContextLabel(tab.userContextId)
   7731        : "";
   7732      let tabGroupName = this.#isFirstOrLastInTabGroup(tab)
   7733        ? tab.group.name ||
   7734          this.tabLocalization.formatValueSync("tab-group-name-default")
   7735        : "";
   7736 
   7737      if (containerName || tabGroupName) {
   7738        let tabContextString;
   7739        if (containerName && tabGroupName) {
   7740          tabContextString = this.tabLocalization.formatValueSync(
   7741            "tabbrowser-tab-tooltip-tab-group-container",
   7742            {
   7743              tabGroupName,
   7744              containerName,
   7745            }
   7746          );
   7747        } else if (tabGroupName) {
   7748          tabContextString = this.tabLocalization.formatValueSync(
   7749            "tabbrowser-tab-tooltip-tab-group",
   7750            {
   7751              tabGroupName,
   7752            }
   7753          );
   7754        } else {
   7755          tabContextString = this.tabLocalization.formatValueSync(
   7756            "tabbrowser-tab-tooltip-container",
   7757            {
   7758              containerName,
   7759            }
   7760          );
   7761        }
   7762        labelArray.push(tabContextString);
   7763      }
   7764 
   7765      if (tab.soundPlaying) {
   7766        let audioPlayingString = this.tabLocalization.formatValueSync(
   7767          "tabbrowser-tab-audio-playing-description"
   7768        );
   7769        labelArray.push(audioPlayingString);
   7770      }
   7771      return labelArray.join("\n");
   7772    }
   7773 
   7774    createTooltip(event) {
   7775      event.stopPropagation();
   7776      let tab = event.target.triggerNode?.closest("tab");
   7777      if (!tab) {
   7778        if (event.target.triggerNode?.getRootNode()?.host?.closest("tab")) {
   7779          // Check if triggerNode is within shadowRoot of moz-button
   7780          tab = event.target.triggerNode?.getRootNode().host.closest("tab");
   7781        } else {
   7782          event.preventDefault();
   7783          return;
   7784        }
   7785      }
   7786 
   7787      const tooltip = event.target;
   7788      tooltip.removeAttribute("data-l10n-id");
   7789 
   7790      const tabCount = this.selectedTabs.includes(tab)
   7791        ? this.selectedTabs.length
   7792        : 1;
   7793      if (tab._overPlayingIcon || tab._overAudioButton) {
   7794        let l10nId;
   7795        const l10nArgs = { tabCount };
   7796        if (tab.selected) {
   7797          l10nId = tab.linkedBrowser.audioMuted
   7798            ? "tabbrowser-unmute-tab-audio-tooltip"
   7799            : "tabbrowser-mute-tab-audio-tooltip";
   7800          const keyElem = document.getElementById("key_toggleMute");
   7801          l10nArgs.shortcut = ShortcutUtils.prettifyShortcut(keyElem);
   7802        } else if (tab.hasAttribute("activemedia-blocked")) {
   7803          l10nId = "tabbrowser-unblock-tab-audio-tooltip";
   7804        } else {
   7805          l10nId = tab.linkedBrowser.audioMuted
   7806            ? "tabbrowser-unmute-tab-audio-background-tooltip"
   7807            : "tabbrowser-mute-tab-audio-background-tooltip";
   7808        }
   7809        tooltip.label = "";
   7810        document.l10n.setAttributes(tooltip, l10nId, l10nArgs);
   7811      } else {
   7812        // Prevent the tooltip from appearing if card preview is enabled, but
   7813        // only if the user is not hovering over the media play icon or the
   7814        // close button
   7815        if (this._showTabCardPreview) {
   7816          event.preventDefault();
   7817          return;
   7818        }
   7819        tooltip.label = this.getTabTooltip(tab, true);
   7820      }
   7821    }
   7822 
   7823    handleEvent(aEvent) {
   7824      switch (aEvent.type) {
   7825        case "keydown":
   7826          this._handleKeyDownEvent(aEvent);
   7827          break;
   7828        case "keypress":
   7829          this._handleKeyPressEvent(aEvent);
   7830          break;
   7831        case "framefocusrequested": {
   7832          let tab = this.getTabForBrowser(aEvent.target);
   7833          if (!tab || tab == this.selectedTab) {
   7834            // Let the focus manager try to do its thing by not calling
   7835            // preventDefault(). It will still raise the window if appropriate.
   7836            break;
   7837          }
   7838          this.selectedTab = tab;
   7839          window.focus();
   7840          aEvent.preventDefault();
   7841          break;
   7842        }
   7843        case "visibilitychange": {
   7844          const inactive = document.hidden;
   7845          if (!this._switcher) {
   7846            for (const browser of this.selectedBrowsers) {
   7847              browser.preserveLayers(inactive);
   7848              browser.docShellIsActive = !inactive;
   7849            }
   7850          }
   7851          break;
   7852        }
   7853        case "TabGroupCollapse":
   7854          aEvent.target.tabs.forEach(tab => {
   7855            this.removeFromMultiSelectedTabs(tab);
   7856          });
   7857          break;
   7858        case "TabGroupCreateByUser":
   7859          this.tabGroupMenu.openCreateModal(aEvent.target);
   7860          break;
   7861        case "TabGrouped": {
   7862          let tab = aEvent.detail;
   7863          let uri =
   7864            tab.linkedBrowser?.registeredOpenURI ||
   7865            tab._originalRegisteredOpenURI;
   7866          if (uri) {
   7867            this.UrlbarProviderOpenTabs.unregisterOpenTab(
   7868              uri.spec,
   7869              tab.userContextId,
   7870              null,
   7871              PrivateBrowsingUtils.isWindowPrivate(window)
   7872            );
   7873            this.UrlbarProviderOpenTabs.registerOpenTab(
   7874              uri.spec,
   7875              tab.userContextId,
   7876              tab.group?.id,
   7877              PrivateBrowsingUtils.isWindowPrivate(window)
   7878            );
   7879          }
   7880          break;
   7881        }
   7882        case "TabUngrouped": {
   7883          let tab = aEvent.detail;
   7884          let uri =
   7885            tab.linkedBrowser?.registeredOpenURI ||
   7886            tab._originalRegisteredOpenURI;
   7887          if (uri) {
   7888            // By the time the tab makes it to us it is already ungrouped, but
   7889            // the original group is preserved in the event target.
   7890            let originalGroup = aEvent.target;
   7891            this.UrlbarProviderOpenTabs.unregisterOpenTab(
   7892              uri.spec,
   7893              tab.userContextId,
   7894              originalGroup.id,
   7895              PrivateBrowsingUtils.isWindowPrivate(window)
   7896            );
   7897            this.UrlbarProviderOpenTabs.registerOpenTab(
   7898              uri.spec,
   7899              tab.userContextId,
   7900              null,
   7901              PrivateBrowsingUtils.isWindowPrivate(window)
   7902            );
   7903          }
   7904          break;
   7905        }
   7906        case "TabSplitViewActivate":
   7907          this.#activeSplitView = aEvent.detail.splitview;
   7908          break;
   7909        case "TabSplitViewDeactivate":
   7910          if (this.#activeSplitView === aEvent.detail.splitview) {
   7911            this.#activeSplitView = null;
   7912          }
   7913          break;
   7914        case "activate":
   7915        // Intentional fallthrough
   7916        case "deactivate":
   7917          this.selectedTab.updateLastSeenActive();
   7918          break;
   7919      }
   7920    }
   7921 
   7922    observe(aSubject, aTopic) {
   7923      switch (aTopic) {
   7924        case "contextual-identity-updated": {
   7925          let identity = aSubject.wrappedJSObject;
   7926          for (let tab of this.tabs) {
   7927            if (tab.getAttribute("usercontextid") == identity.userContextId) {
   7928              ContextualIdentityService.setTabStyle(tab);
   7929            }
   7930          }
   7931          break;
   7932        }
   7933        case "intl:app-locales-changed": {
   7934          this.#populateTitleCache();
   7935          this.updateTitlebar();
   7936          break;
   7937        }
   7938      }
   7939    }
   7940 
   7941    refreshBlocked(actor, browser, data) {
   7942      // The data object is expected to contain the following properties:
   7943      //  - URI (string)
   7944      //     The URI that a page is attempting to refresh or redirect to.
   7945      //  - delay (int)
   7946      //     The delay (in milliseconds) before the page was going to
   7947      //     reload or redirect.
   7948      //  - sameURI (bool)
   7949      //     true if we're refreshing the page. false if we're redirecting.
   7950 
   7951      let notificationBox = this.getNotificationBox(browser);
   7952      let notification =
   7953        notificationBox.getNotificationWithValue("refresh-blocked");
   7954 
   7955      let l10nId = data.sameURI
   7956        ? "refresh-blocked-refresh-label"
   7957        : "refresh-blocked-redirect-label";
   7958      if (notification) {
   7959        notification.label = { "l10n-id": l10nId };
   7960      } else {
   7961        const buttons = [
   7962          {
   7963            "l10n-id": "refresh-blocked-allow",
   7964            callback() {
   7965              actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
   7966            },
   7967          },
   7968        ];
   7969 
   7970        notificationBox.appendNotification(
   7971          "refresh-blocked",
   7972          {
   7973            label: { "l10n-id": l10nId },
   7974            image: "chrome://browser/skin/notification-icons/popup.svg",
   7975            priority: notificationBox.PRIORITY_INFO_MEDIUM,
   7976          },
   7977          buttons
   7978        );
   7979      }
   7980    }
   7981 
   7982    _generateUniquePanelID() {
   7983      if (!this._uniquePanelIDCounter) {
   7984        this._uniquePanelIDCounter = 0;
   7985      }
   7986 
   7987      let outerID = window.docShell.outerWindowID;
   7988 
   7989      // We want panel IDs to be globally unique, that's why we include the
   7990      // window ID. We switched to a monotonic counter as Date.now() lead
   7991      // to random failures because of colliding IDs.
   7992      return "panel-" + outerID + "-" + ++this._uniquePanelIDCounter;
   7993    }
   7994 
   7995    destroy() {
   7996      this.tabContainer.destroy();
   7997      Services.obs.removeObserver(this, "contextual-identity-updated");
   7998      Services.obs.removeObserver(this, "intl:app-locales-changed");
   7999 
   8000      for (let tab of this.tabs) {
   8001        let browser = tab.linkedBrowser;
   8002        if (browser.registeredOpenURI) {
   8003          let userContextId = browser.getAttribute("usercontextid") || 0;
   8004          this.UrlbarProviderOpenTabs.unregisterOpenTab(
   8005            browser.registeredOpenURI.spec,
   8006            userContextId,
   8007            tab.group?.id,
   8008            PrivateBrowsingUtils.isWindowPrivate(window)
   8009          );
   8010          delete browser.registeredOpenURI;
   8011        }
   8012 
   8013        let filter = this._tabFilters.get(tab);
   8014        if (filter) {
   8015          browser.webProgress.removeProgressListener(filter);
   8016 
   8017          let listener = this._tabListeners.get(tab);
   8018          if (listener) {
   8019            filter.removeProgressListener(listener);
   8020            listener.destroy();
   8021          }
   8022 
   8023          this._tabFilters.delete(tab);
   8024          this._tabListeners.delete(tab);
   8025        }
   8026      }
   8027 
   8028      document.removeEventListener("keydown", this, { mozSystemGroup: true });
   8029      if (AppConstants.platform == "macosx") {
   8030        document.removeEventListener("keypress", this, {
   8031          mozSystemGroup: true,
   8032        });
   8033      }
   8034      document.removeEventListener("visibilitychange", this);
   8035      window.removeEventListener("framefocusrequested", this);
   8036      window.removeEventListener("activate", this);
   8037      window.removeEventListener("deactivate", this);
   8038 
   8039      if (gMultiProcessBrowser) {
   8040        if (this._switcher) {
   8041          this._switcher.destroy();
   8042        }
   8043      }
   8044    }
   8045 
   8046    _setupEventListeners() {
   8047      this.tabpanels.addEventListener("select", event => {
   8048        if (event.target == this.tabpanels) {
   8049          this.updateCurrentBrowser();
   8050        }
   8051      });
   8052 
   8053      this.addEventListener("DOMWindowClose", event => {
   8054        let browser = event.target;
   8055        if (!browser.isRemoteBrowser) {
   8056          if (!event.isTrusted) {
   8057            // If the browser is not remote, then we expect the event to be trusted.
   8058            // In the remote case, the DOMWindowClose event is captured in content,
   8059            // a message is sent to the parent, and another DOMWindowClose event
   8060            // is re-dispatched on the actual browser node. In that case, the event
   8061            // won't  be marked as trusted, since it's synthesized by JavaScript.
   8062            return;
   8063          }
   8064          // In the parent-process browser case, it's possible that the browser
   8065          // that fired DOMWindowClose is actually a child of another browser. We
   8066          // want to find the top-most browser to determine whether or not this is
   8067          // for a tab or not. The chromeEventHandler will be the top-most browser.
   8068          browser = event.target.docShell.chromeEventHandler;
   8069        }
   8070 
   8071        if (this.tabs.length == 1) {
   8072          // We already did PermitUnload in the content process
   8073          // for this tab (the only one in the window). So we don't
   8074          // need to do it again for any tabs.
   8075          window.skipNextCanClose = true;
   8076          // In the parent-process browser case, the nsCloseEvent will actually take
   8077          // care of tearing down the window, but we need to do this ourselves in the
   8078          // content-process browser case. Doing so in both cases doesn't appear to
   8079          // hurt.
   8080          window.close();
   8081          return;
   8082        }
   8083 
   8084        let tab = this.getTabForBrowser(browser);
   8085        if (tab) {
   8086          // Skip running PermitUnload since it already happened in
   8087          // the content process.
   8088          this.removeTab(tab, { skipPermitUnload: true });
   8089          // If we don't preventDefault on the DOMWindowClose event, then
   8090          // in the parent-process browser case, we're telling the platform
   8091          // to close the entire window. Calling preventDefault is our way of
   8092          // saying we took care of this close request by closing the tab.
   8093          event.preventDefault();
   8094        }
   8095      });
   8096 
   8097      this.addEventListener("pagetitlechanged", event => {
   8098        let browser = event.target;
   8099        let tab = this.getTabForBrowser(browser);
   8100        if (!tab || tab.hasAttribute("pending")) {
   8101          return;
   8102        }
   8103 
   8104        // Ignore empty title changes on internal pages. This prevents the title
   8105        // from changing while Fluent is populating the (initially-empty) title
   8106        // element.
   8107        if (
   8108          !browser.contentTitle &&
   8109          browser.contentPrincipal.isSystemPrincipal
   8110        ) {
   8111          return;
   8112        }
   8113 
   8114        let titleChanged = this.setTabTitle(tab);
   8115        if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
   8116          tab.setAttribute("titlechanged", "true");
   8117        }
   8118      });
   8119 
   8120      this.addEventListener(
   8121        "DOMWillOpenModalDialog",
   8122        event => {
   8123          if (!event.isTrusted) {
   8124            return;
   8125          }
   8126 
   8127          let targetIsWindow = Window.isInstance(event.target);
   8128 
   8129          // We're about to open a modal dialog, so figure out for which tab:
   8130          // If this is a same-process modal dialog, then we're given its DOM
   8131          // window as the event's target. For remote dialogs, we're given the
   8132          // browser, but that's in the originalTarget and not the target,
   8133          // because it's across the tabbrowser's XBL boundary.
   8134          let tabForEvent = targetIsWindow
   8135            ? this.getTabForBrowser(event.target.docShell.chromeEventHandler)
   8136            : this.getTabForBrowser(event.originalTarget);
   8137 
   8138          // Focus window for beforeunload dialog so it is seen but don't
   8139          // steal focus from other applications.
   8140          if (
   8141            event.detail &&
   8142            event.detail.tabPrompt &&
   8143            event.detail.inPermitUnload &&
   8144            Services.focus.activeWindow
   8145          ) {
   8146            window.focus();
   8147          }
   8148 
   8149          // Don't need to act if the tab is already selected or if there isn't
   8150          // a tab for the event (e.g. for the webextensions options_ui remote
   8151          // browsers embedded in the "about:addons" page):
   8152          if (!tabForEvent || tabForEvent.selected) {
   8153            return;
   8154          }
   8155 
   8156          // We always switch tabs for beforeunload tab-modal prompts.
   8157          if (
   8158            event.detail &&
   8159            event.detail.tabPrompt &&
   8160            !event.detail.inPermitUnload
   8161          ) {
   8162            let docPrincipal = targetIsWindow
   8163              ? event.target.document.nodePrincipal
   8164              : null;
   8165            // At least one of these should/will be non-null:
   8166            let promptPrincipal =
   8167              event.detail.promptPrincipal ||
   8168              docPrincipal ||
   8169              tabForEvent.linkedBrowser.contentPrincipal;
   8170 
   8171            // For null principals, we bail immediately and don't show the checkbox:
   8172            if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
   8173              tabForEvent.attention = true;
   8174              return;
   8175            }
   8176 
   8177            // For non-system/expanded principals without permission, we bail and show the checkbox.
   8178            if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
   8179              let permission = Services.perms.testPermissionFromPrincipal(
   8180                promptPrincipal,
   8181                "focus-tab-by-prompt"
   8182              );
   8183              if (permission != Services.perms.ALLOW_ACTION) {
   8184                // Tell the prompt box we want to show the user a checkbox:
   8185                let tabPrompt = this.getTabDialogBox(tabForEvent.linkedBrowser);
   8186                tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
   8187                  promptPrincipal
   8188                );
   8189                tabForEvent.attention = true;
   8190                return;
   8191              }
   8192            }
   8193            // ... so system and expanded principals, as well as permitted "normal"
   8194            // URI-based principals, always get to steal focus for the tab when prompting.
   8195          }
   8196 
   8197          // If permissions/origins dictate so, bring tab to the front.
   8198          this.selectedTab = tabForEvent;
   8199        },
   8200        true
   8201      );
   8202 
   8203      // When cancelling beforeunload tabmodal dialogs, reset the URL bar to
   8204      // avoid spoofing risks.
   8205      this.addEventListener(
   8206        "DOMModalDialogClosed",
   8207        event => {
   8208          if (
   8209            event.detail?.promptType != "beforeunload" ||
   8210            event.detail.areLeaving ||
   8211            event.target.nodeName != "browser"
   8212          ) {
   8213            return;
   8214          }
   8215          event.target.userTypedValue = null;
   8216          if (event.target == this.selectedBrowser) {
   8217            gURLBar.setURI();
   8218          }
   8219        },
   8220        true
   8221      );
   8222 
   8223      let onTabCrashed = event => {
   8224        if (!event.isTrusted) {
   8225          return;
   8226        }
   8227 
   8228        let browser = event.originalTarget;
   8229 
   8230        if (!event.isTopFrame) {
   8231          TabCrashHandler.onSubFrameCrash(browser, event.childID);
   8232          return;
   8233        }
   8234 
   8235        // Preloaded browsers do not actually have any tabs. If one crashes,
   8236        // it should be released and removed.
   8237        if (browser === this.preloadedBrowser) {
   8238          NewTabPagePreloading.removePreloadedBrowser(window);
   8239          return;
   8240        }
   8241 
   8242        let isRestartRequiredCrash =
   8243          event.type == "oop-browser-buildid-mismatch";
   8244 
   8245        let icon = browser.mIconURL;
   8246        let tab = this.getTabForBrowser(browser);
   8247 
   8248        if (this.selectedBrowser == browser) {
   8249          TabCrashHandler.onSelectedBrowserCrash(
   8250            browser,
   8251            isRestartRequiredCrash
   8252          );
   8253        } else {
   8254          TabCrashHandler.onBackgroundBrowserCrash(
   8255            browser,
   8256            isRestartRequiredCrash
   8257          );
   8258        }
   8259 
   8260        tab.removeAttribute("soundplaying");
   8261        this.setIcon(tab, icon);
   8262      };
   8263 
   8264      this.addEventListener("oop-browser-crashed", onTabCrashed);
   8265      this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed);
   8266 
   8267      this.addEventListener("DOMAudioPlaybackStarted", event => {
   8268        var tab = this.getTabFromAudioEvent(event);
   8269        if (!tab) {
   8270          return;
   8271        }
   8272 
   8273        clearTimeout(tab._soundPlayingAttrRemovalTimer);
   8274        tab._soundPlayingAttrRemovalTimer = 0;
   8275 
   8276        let modifiedAttrs = [];
   8277        if (tab.hasAttribute("soundplaying-scheduledremoval")) {
   8278          tab.removeAttribute("soundplaying-scheduledremoval");
   8279          modifiedAttrs.push("soundplaying-scheduledremoval");
   8280        }
   8281 
   8282        if (!tab.hasAttribute("soundplaying")) {
   8283          tab.toggleAttribute("soundplaying", true);
   8284          modifiedAttrs.push("soundplaying");
   8285        }
   8286 
   8287        if (modifiedAttrs.length) {
   8288          // Flush style so that the opacity takes effect immediately, in
   8289          // case the media is stopped before the style flushes naturally.
   8290          getComputedStyle(tab).opacity;
   8291        }
   8292 
   8293        this._tabAttrModified(tab, modifiedAttrs);
   8294      });
   8295 
   8296      this.addEventListener("DOMAudioPlaybackStopped", event => {
   8297        var tab = this.getTabFromAudioEvent(event);
   8298        if (!tab) {
   8299          return;
   8300        }
   8301 
   8302        if (tab.hasAttribute("soundplaying")) {
   8303          let removalDelay = Services.prefs.getIntPref(
   8304            "browser.tabs.delayHidingAudioPlayingIconMS"
   8305          );
   8306 
   8307          tab.style.setProperty(
   8308            "--soundplaying-removal-delay",
   8309            `${removalDelay - 300}ms`
   8310          );
   8311          tab.toggleAttribute("soundplaying-scheduledremoval", true);
   8312          this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
   8313 
   8314          tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
   8315            tab.removeAttribute("soundplaying-scheduledremoval");
   8316            tab.removeAttribute("soundplaying");
   8317            this._tabAttrModified(tab, [
   8318              "soundplaying",
   8319              "soundplaying-scheduledremoval",
   8320            ]);
   8321          }, removalDelay);
   8322        }
   8323      });
   8324 
   8325      this.addEventListener("DOMAudioPlaybackBlockStarted", event => {
   8326        var tab = this.getTabFromAudioEvent(event);
   8327        if (!tab) {
   8328          return;
   8329        }
   8330 
   8331        if (!tab.hasAttribute("activemedia-blocked")) {
   8332          tab.setAttribute("activemedia-blocked", true);
   8333          this._tabAttrModified(tab, ["activemedia-blocked"]);
   8334        }
   8335      });
   8336 
   8337      this.addEventListener("DOMAudioPlaybackBlockStopped", event => {
   8338        var tab = this.getTabFromAudioEvent(event);
   8339        if (!tab) {
   8340          return;
   8341        }
   8342 
   8343        if (tab.hasAttribute("activemedia-blocked")) {
   8344          tab.removeAttribute("activemedia-blocked");
   8345          this._tabAttrModified(tab, ["activemedia-blocked"]);
   8346        }
   8347      });
   8348 
   8349      this.addEventListener("GloballyAutoplayBlocked", event => {
   8350        let browser = event.originalTarget;
   8351        let tab = this.getTabForBrowser(browser);
   8352        if (!tab) {
   8353          return;
   8354        }
   8355 
   8356        SitePermissions.setForPrincipal(
   8357          browser.contentPrincipal,
   8358          "autoplay-media",
   8359          SitePermissions.BLOCK,
   8360          SitePermissions.SCOPE_GLOBAL,
   8361          browser
   8362        );
   8363      });
   8364 
   8365      let tabContextFTLInserter = () => {
   8366        this.translateTabContextMenu();
   8367        this.tabContainer.removeEventListener(
   8368          "contextmenu",
   8369          tabContextFTLInserter,
   8370          true
   8371        );
   8372        this.tabContainer.removeEventListener(
   8373          "mouseover",
   8374          tabContextFTLInserter
   8375        );
   8376        this.tabContainer.removeEventListener(
   8377          "focus",
   8378          tabContextFTLInserter,
   8379          true
   8380        );
   8381      };
   8382      this.tabContainer.addEventListener(
   8383        "contextmenu",
   8384        tabContextFTLInserter,
   8385        true
   8386      );
   8387      this.tabContainer.addEventListener("mouseover", tabContextFTLInserter);
   8388      this.tabContainer.addEventListener("focus", tabContextFTLInserter, true);
   8389 
   8390      // Fired when Gecko has decided a <browser> element will change
   8391      // remoteness. This allows persisting some state on this element across
   8392      // process switches.
   8393      this.addEventListener("WillChangeBrowserRemoteness", event => {
   8394        let browser = event.originalTarget;
   8395        let tab = this.getTabForBrowser(browser);
   8396        if (!tab) {
   8397          return;
   8398        }
   8399 
   8400        // Dispatch the `BeforeTabRemotenessChange` event, allowing other code
   8401        // to react to this tab's process switch.
   8402        let evt = document.createEvent("Events");
   8403        evt.initEvent("BeforeTabRemotenessChange", true, false);
   8404        tab.dispatchEvent(evt);
   8405 
   8406        // Unhook our progress listener.
   8407        let filter = this._tabFilters.get(tab);
   8408        let oldListener = this._tabListeners.get(tab);
   8409        browser.webProgress.removeProgressListener(filter);
   8410        filter.removeProgressListener(oldListener);
   8411        let stateFlags = oldListener.mStateFlags;
   8412        let requestCount = oldListener.mRequestCount;
   8413 
   8414        // We'll be creating a new listener, so destroy the old one.
   8415        oldListener.destroy();
   8416 
   8417        let oldDroppedLinkHandler = browser.droppedLinkHandler;
   8418        let oldUserTypedValue = browser.userTypedValue;
   8419        let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();
   8420 
   8421        let didChange = () => {
   8422          browser.userTypedValue = oldUserTypedValue;
   8423          if (hadStartedLoad) {
   8424            browser.urlbarChangeTracker.startedLoad();
   8425          }
   8426 
   8427          browser.droppedLinkHandler = oldDroppedLinkHandler;
   8428 
   8429          // This shouldn't really be necessary, however, this has the side effect
   8430          // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
   8431          // frames, which the tab switcher depends on.
   8432          //
   8433          // eslint-disable-next-line no-self-assign
   8434          browser.docShellIsActive = browser.docShellIsActive;
   8435 
   8436          // Create a new tab progress listener for the new browser we just
   8437          // injected, since tab progress listeners have logic for handling the
   8438          // initial about:blank load
   8439          let listener = new TabProgressListener(
   8440            tab,
   8441            browser,
   8442            false,
   8443            false,
   8444            stateFlags,
   8445            requestCount
   8446          );
   8447          this._tabListeners.set(tab, listener);
   8448          filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
   8449 
   8450          // Restore the progress listener.
   8451          browser.webProgress.addProgressListener(
   8452            filter,
   8453            Ci.nsIWebProgress.NOTIFY_ALL
   8454          );
   8455 
   8456          let cbEvent = browser.getContentBlockingEvents();
   8457          // Include the true final argument to indicate that this event is
   8458          // simulated (instead of being observed by the webProgressListener).
   8459          this._callProgressListeners(
   8460            browser,
   8461            "onContentBlockingEvent",
   8462            [browser.webProgress, null, cbEvent, true],
   8463            true,
   8464            false
   8465          );
   8466 
   8467          if (browser.isRemoteBrowser) {
   8468            // Switching the browser to be remote will connect to a new child
   8469            // process so the browser can no longer be considered to be
   8470            // crashed.
   8471            tab.removeAttribute("crashed");
   8472          }
   8473 
   8474          if (this.isFindBarInitialized(tab)) {
   8475            this.getCachedFindBar(tab).browser = browser;
   8476          }
   8477 
   8478          evt = document.createEvent("Events");
   8479          evt.initEvent("TabRemotenessChange", true, false);
   8480          tab.dispatchEvent(evt);
   8481        };
   8482        browser.addEventListener("DidChangeBrowserRemoteness", didChange, {
   8483          once: true,
   8484        });
   8485      });
   8486 
   8487      this.addEventListener("pageinfo", event => {
   8488        let browser = event.originalTarget;
   8489        let tab = this.getTabForBrowser(browser);
   8490        if (!tab) {
   8491          return;
   8492        }
   8493        const { url, description, previewImageURL } = event.detail;
   8494        this.setPageInfo(tab, url, description, previewImageURL);
   8495      });
   8496 
   8497      this.splitViewCommandSet.addEventListener("command", event => {
   8498        switch (event.target.id) {
   8499          case "splitViewCmd_separateTabs":
   8500            this.#activeSplitView.unsplitTabs();
   8501            break;
   8502          case "splitViewCmd_reverseTabs":
   8503            this.#activeSplitView.reverseTabs();
   8504            break;
   8505          case "splitViewCmd_closeTabs":
   8506            this.#activeSplitView.close();
   8507            break;
   8508        }
   8509      });
   8510    }
   8511 
   8512    translateTabContextMenu() {
   8513      if (this._tabContextMenuTranslated) {
   8514        return;
   8515      }
   8516      MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl");
   8517      // Un-lazify the l10n-ids now that the FTL file has been inserted.
   8518      document
   8519        .getElementById("tabContextMenu")
   8520        .querySelectorAll("[data-lazy-l10n-id]")
   8521        .forEach(el => {
   8522          el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
   8523          el.removeAttribute("data-lazy-l10n-id");
   8524        });
   8525      this._tabContextMenuTranslated = true;
   8526    }
   8527 
   8528    setSuccessor(aTab, successorTab) {
   8529      if (aTab.ownerGlobal != window) {
   8530        throw new Error("Cannot set the successor of another window's tab");
   8531      }
   8532      if (successorTab == aTab) {
   8533        successorTab = null;
   8534      }
   8535      if (successorTab && successorTab.ownerGlobal != window) {
   8536        throw new Error("Cannot set the successor to another window's tab");
   8537      }
   8538      if (aTab.successor) {
   8539        aTab.successor.predecessors.delete(aTab);
   8540      }
   8541      aTab.successor = successorTab;
   8542      if (successorTab) {
   8543        if (!successorTab.predecessors) {
   8544          successorTab.predecessors = new Set();
   8545        }
   8546        successorTab.predecessors.add(aTab);
   8547      }
   8548    }
   8549 
   8550    /**
   8551     * For all tabs with aTab as a successor, set the successor to aOtherTab
   8552     * instead.
   8553     */
   8554    replaceInSuccession(aTab, aOtherTab) {
   8555      if (aTab.predecessors) {
   8556        for (const predecessor of Array.from(aTab.predecessors)) {
   8557          this.setSuccessor(predecessor, aOtherTab);
   8558        }
   8559      }
   8560    }
   8561 
   8562    /**
   8563     * Get the triggering principal for the last navigation in the session history.
   8564     */
   8565    _getTriggeringPrincipalFromHistory(aBrowser) {
   8566      let sessionHistory = aBrowser?.browsingContext?.sessionHistory;
   8567      if (
   8568        !sessionHistory ||
   8569        !sessionHistory.index ||
   8570        sessionHistory.count == 0
   8571      ) {
   8572        return undefined;
   8573      }
   8574      let currentEntry = sessionHistory.getEntryAtIndex(sessionHistory.index);
   8575      let triggeringPrincipal = currentEntry?.triggeringPrincipal;
   8576      return triggeringPrincipal;
   8577    }
   8578 
   8579    clearRelatedTabs() {
   8580      this._lastRelatedTabMap = new WeakMap();
   8581    }
   8582  };
   8583 
   8584  /**
   8585   * A web progress listener object definition for a given tab.
   8586   */
   8587  class TabProgressListener {
   8588    constructor(
   8589      aTab,
   8590      aBrowser,
   8591      aStartsBlank,
   8592      aWasPreloadedBrowser,
   8593      aOrigStateFlags,
   8594      aOrigRequestCount
   8595    ) {
   8596      let stateFlags = aOrigStateFlags || 0;
   8597      // Initialize mStateFlags to non-zero e.g. when creating a progress
   8598      // listener for preloaded browsers as there was no progress listener
   8599      // around when the content started loading. If the content didn't
   8600      // quite finish loading yet, mStateFlags will very soon be overridden
   8601      // with the correct value and end up at STATE_STOP again.
   8602      if (aWasPreloadedBrowser) {
   8603        stateFlags =
   8604          Ci.nsIWebProgressListener.STATE_STOP |
   8605          Ci.nsIWebProgressListener.STATE_IS_REQUEST;
   8606      }
   8607 
   8608      this.mTab = aTab;
   8609      this.mBrowser = aBrowser;
   8610      this.mBlank = aStartsBlank;
   8611 
   8612      // cache flags for correct status UI update after tab switching
   8613      this.mStateFlags = stateFlags;
   8614      this.mStatus = 0;
   8615      this.mMessage = "";
   8616      this.mTotalProgress = 0;
   8617 
   8618      // count of open requests (should always be 0 or 1)
   8619      this.mRequestCount = aOrigRequestCount || 0;
   8620    }
   8621 
   8622    destroy() {
   8623      delete this.mTab;
   8624      delete this.mBrowser;
   8625    }
   8626 
   8627    _callProgressListeners(...args) {
   8628      args.unshift(this.mBrowser);
   8629      return gBrowser._callProgressListeners.apply(gBrowser, args);
   8630    }
   8631 
   8632    _shouldShowProgress(aRequest) {
   8633      if (this.mBlank) {
   8634        return false;
   8635      }
   8636 
   8637      // Don't show progress indicators in tabs for about: URIs
   8638      // pointing to local resources.
   8639      if (
   8640        aRequest instanceof Ci.nsIChannel &&
   8641        aRequest.originalURI.schemeIs("about")
   8642      ) {
   8643        return false;
   8644      }
   8645 
   8646      return true;
   8647    }
   8648 
   8649    _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
   8650      if (!this.mBlank || !aWebProgress.isTopLevel) {
   8651        return false;
   8652      }
   8653 
   8654      // If the state has STATE_STOP, and no requests were in flight, then this
   8655      // must be the initial "stop" for the initial about:blank document.
   8656      if (
   8657        aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
   8658        this.mRequestCount == 0 &&
   8659        !aLocation
   8660      ) {
   8661        return true;
   8662      }
   8663 
   8664      let location = aLocation ? aLocation.spec : "";
   8665      return location == "about:blank";
   8666    }
   8667 
   8668    onProgressChange(
   8669      aWebProgress,
   8670      aRequest,
   8671      aCurSelfProgress,
   8672      aMaxSelfProgress,
   8673      aCurTotalProgress,
   8674      aMaxTotalProgress
   8675    ) {
   8676      this.mTotalProgress = aMaxTotalProgress
   8677        ? aCurTotalProgress / aMaxTotalProgress
   8678        : 0;
   8679 
   8680      if (!this._shouldShowProgress(aRequest)) {
   8681        return;
   8682      }
   8683 
   8684      if (this.mTotalProgress && this.mTab.hasAttribute("busy")) {
   8685        this.mTab.setAttribute("progress", "true");
   8686        gBrowser._tabAttrModified(this.mTab, ["progress"]);
   8687      }
   8688 
   8689      this._callProgressListeners("onProgressChange", [
   8690        aWebProgress,
   8691        aRequest,
   8692        aCurSelfProgress,
   8693        aMaxSelfProgress,
   8694        aCurTotalProgress,
   8695        aMaxTotalProgress,
   8696      ]);
   8697    }
   8698 
   8699    onProgressChange64(
   8700      aWebProgress,
   8701      aRequest,
   8702      aCurSelfProgress,
   8703      aMaxSelfProgress,
   8704      aCurTotalProgress,
   8705      aMaxTotalProgress
   8706    ) {
   8707      return this.onProgressChange(
   8708        aWebProgress,
   8709        aRequest,
   8710        aCurSelfProgress,
   8711        aMaxSelfProgress,
   8712        aCurTotalProgress,
   8713        aMaxTotalProgress
   8714      );
   8715    }
   8716 
   8717    /* eslint-disable complexity */
   8718    onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
   8719      if (!aRequest) {
   8720        return;
   8721      }
   8722 
   8723      let location, originalLocation;
   8724      try {
   8725        aRequest.QueryInterface(Ci.nsIChannel);
   8726        location = aRequest.URI;
   8727        originalLocation = aRequest.originalURI;
   8728      } catch (ex) {}
   8729 
   8730      let ignoreBlank = this._isForInitialAboutBlank(
   8731        aWebProgress,
   8732        aStateFlags,
   8733        location
   8734      );
   8735 
   8736      const { STATE_START, STATE_STOP, STATE_IS_NETWORK } =
   8737        Ci.nsIWebProgressListener;
   8738 
   8739      // If we were ignoring some messages about the initial about:blank, and we
   8740      // got the STATE_STOP for it, we'll want to pay attention to those messages
   8741      // from here forward. Similarly, if we conclude that this state change
   8742      // is one that we shouldn't be ignoring, then stop ignoring.
   8743      if (
   8744        (ignoreBlank &&
   8745          aStateFlags & STATE_STOP &&
   8746          aStateFlags & STATE_IS_NETWORK) ||
   8747        (!ignoreBlank && this.mBlank)
   8748      ) {
   8749        this.mBlank = false;
   8750      }
   8751 
   8752      if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) {
   8753        this.mRequestCount++;
   8754 
   8755        if (aWebProgress.isTopLevel) {
   8756          // Need to use originalLocation rather than location because things
   8757          // like about:home and about:privatebrowsing arrive with nsIRequest
   8758          // pointing to their resolved jar: or file: URIs.
   8759          if (
   8760            !(
   8761              originalLocation &&
   8762              gInitialPages.includes(originalLocation.spec) &&
   8763              originalLocation != "about:blank" &&
   8764              this.mBrowser.initialPageLoadedFromUserAction !=
   8765                originalLocation.spec &&
   8766              this.mBrowser.currentURI &&
   8767              this.mBrowser.currentURI.spec == "about:blank"
   8768            )
   8769          ) {
   8770            // Indicating that we started a load will allow the location
   8771            // bar to be cleared when the load finishes.
   8772            // In order to not overwrite user-typed content, we avoid it
   8773            // (see if condition above) in a very specific case:
   8774            // If the load is of an 'initial' page (e.g. about:privatebrowsing,
   8775            // about:newtab, etc.), was not explicitly typed in the location
   8776            // bar by the user, is not about:blank (because about:blank can be
   8777            // loaded by websites under their principal), and the current
   8778            // page in the browser is about:blank (indicating it is a newly
   8779            // created or re-created browser, e.g. because it just switched
   8780            // remoteness or is a new tab/window).
   8781            this.mBrowser.urlbarChangeTracker.startedLoad();
   8782 
   8783            // To improve the user experience and perceived performance when
   8784            // opening links in new tabs, we show the url and tab title sooner,
   8785            // but only if it's safe (from a phishing point of view) to do so,
   8786            // thus there's no session history and the load starts from a
   8787            // non-web-controlled blank page.
   8788            if (
   8789              this.mBrowser.browsingContext.sessionHistory?.count === 0 &&
   8790              BrowserUIUtils.checkEmptyPageOrigin(
   8791                this.mBrowser,
   8792                originalLocation
   8793              )
   8794            ) {
   8795              gBrowser.setInitialTabTitle(this.mTab, originalLocation.spec, {
   8796                isURL: true,
   8797              });
   8798 
   8799              this.mBrowser.browsingContext.nonWebControlledBlankURI =
   8800                originalLocation;
   8801              if (this.mTab.selected && !gBrowser.userTypedValue) {
   8802                gURLBar.setURI();
   8803              }
   8804            }
   8805          }
   8806          delete this.mBrowser.initialPageLoadedFromUserAction;
   8807          // If the browser is loading it must not be crashed anymore
   8808          this.mTab.removeAttribute("crashed");
   8809        }
   8810 
   8811        if (this._shouldShowProgress(aRequest)) {
   8812          if (
   8813            !(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) &&
   8814            aWebProgress &&
   8815            aWebProgress.isTopLevel
   8816          ) {
   8817            this.mTab.setAttribute("busy", "true");
   8818            gBrowser._tabAttrModified(this.mTab, ["busy"]);
   8819            this.mTab._notselectedsinceload = !this.mTab.selected;
   8820          }
   8821 
   8822          if (this.mTab.selected) {
   8823            gBrowser._isBusy = true;
   8824          }
   8825        }
   8826      } else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) {
   8827        // since we (try to) only handle STATE_STOP of the last request,
   8828        // the count of open requests should now be 0
   8829        this.mRequestCount = 0;
   8830 
   8831        let modifiedAttrs = [];
   8832        if (this.mTab.hasAttribute("busy")) {
   8833          this.mTab.removeAttribute("busy");
   8834          modifiedAttrs.push("busy");
   8835 
   8836          // Only animate the "burst" indicating the page has loaded if
   8837          // the top-level page is the one that finished loading.
   8838          if (
   8839            aWebProgress.isTopLevel &&
   8840            !aWebProgress.isLoadingDocument &&
   8841            Components.isSuccessCode(aStatus) &&
   8842            !gBrowser.tabAnimationsInProgress &&
   8843            !gReduceMotion
   8844          ) {
   8845            if (this.mTab._notselectedsinceload) {
   8846              this.mTab.setAttribute("notselectedsinceload", "true");
   8847            } else {
   8848              this.mTab.removeAttribute("notselectedsinceload");
   8849            }
   8850 
   8851            this.mTab.setAttribute("bursting", "true");
   8852          }
   8853        }
   8854 
   8855        if (this.mTab.hasAttribute("progress")) {
   8856          this.mTab.removeAttribute("progress");
   8857          modifiedAttrs.push("progress");
   8858        }
   8859 
   8860        if (aWebProgress.isTopLevel) {
   8861          let isSuccessful = Components.isSuccessCode(aStatus);
   8862          if (!isSuccessful && !this.mTab.isEmpty) {
   8863            // Restore the current document's location in case the
   8864            // request was stopped (possibly from a content script)
   8865            // before the location changed.
   8866 
   8867            this.mBrowser.userTypedValue = null;
   8868            // When SHIP is enabled and a load gets cancelled due to another one
   8869            // starting, the error is NS_BINDING_CANCELLED_OLD_LOAD.
   8870            // When these prefs are not enabled, the error is different and
   8871            // that's why we still want to look at the isNavigating flag.
   8872            // We could add a workaround and make sure that in the alternative
   8873            // codepaths we would also omit the same error, but considering
   8874            // how we will be enabling fission by default soon, we can keep
   8875            // using isNavigating for now, and remove it when SHIP is enabled
   8876            // by default.
   8877            // Bug 1725716 has been filed to consider removing isNavigating
   8878            // field alltogether.
   8879            let isNavigating = this.mBrowser.isNavigating;
   8880            if (
   8881              this.mTab.selected &&
   8882              aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD &&
   8883              !isNavigating
   8884            ) {
   8885              gURLBar.setURI();
   8886            }
   8887          } else if (isSuccessful) {
   8888            this.mBrowser.urlbarChangeTracker.finishedLoad();
   8889          }
   8890        }
   8891 
   8892        // If we don't already have an icon for this tab then clear the tab's
   8893        // icon. Don't do this on the initial about:blank load to prevent
   8894        // flickering. Don't clear the icon if we already set it from one of the
   8895        // known defaults. Note we use the original URL since about:newtab
   8896        // redirects to a prerendered page.
   8897        const shouldRemoveFavicon =
   8898          !this.mBrowser.mIconURL &&
   8899          !ignoreBlank &&
   8900          !(originalLocation.spec in FAVICON_DEFAULTS);
   8901        if (shouldRemoveFavicon && this.mTab.hasAttribute("image")) {
   8902          this.mTab.removeAttribute("image");
   8903          modifiedAttrs.push("image");
   8904        } else if (!shouldRemoveFavicon) {
   8905          // Bug 1804166: Allow new tabs to set the favicon correctly if the
   8906          // new tabs behavior is set to open a blank page
   8907          // This is a no-op unless this.mBrowser._documentURI is in
   8908          // FAVICON_DEFAULTS.
   8909          gBrowser.setDefaultIcon(this.mTab, this.mBrowser._documentURI);
   8910        }
   8911 
   8912        // For keyword URIs clear the user typed value since they will be changed into real URIs
   8913        if (location.scheme == "keyword") {
   8914          this.mBrowser.userTypedValue = null;
   8915        }
   8916 
   8917        if (this.mTab.selected) {
   8918          gBrowser._isBusy = false;
   8919        }
   8920 
   8921        if (modifiedAttrs.length) {
   8922          gBrowser._tabAttrModified(this.mTab, modifiedAttrs);
   8923        }
   8924      }
   8925 
   8926      if (ignoreBlank) {
   8927        this._callProgressListeners(
   8928          "onUpdateCurrentBrowser",
   8929          [aStateFlags, aStatus, "", 0],
   8930          true,
   8931          false
   8932        );
   8933      } else {
   8934        this._callProgressListeners(
   8935          "onStateChange",
   8936          [aWebProgress, aRequest, aStateFlags, aStatus],
   8937          true,
   8938          false
   8939        );
   8940      }
   8941 
   8942      this._callProgressListeners(
   8943        "onStateChange",
   8944        [aWebProgress, aRequest, aStateFlags, aStatus],
   8945        false
   8946      );
   8947 
   8948      if (aStateFlags & (STATE_START | STATE_STOP)) {
   8949        // reset cached temporary values at beginning and end
   8950        this.mMessage = "";
   8951        this.mTotalProgress = 0;
   8952      }
   8953      this.mStateFlags = aStateFlags;
   8954      this.mStatus = aStatus;
   8955    }
   8956    /* eslint-enable complexity */
   8957 
   8958    onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
   8959      // OnLocationChange is called for both the top-level content
   8960      // and the subframes.
   8961      let topLevel = aWebProgress.isTopLevel;
   8962 
   8963      let isSameDocument = !!(
   8964        aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
   8965      );
   8966      if (topLevel) {
   8967        let isReload = !!(
   8968          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
   8969        );
   8970        let isErrorPage = !!(
   8971          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE
   8972        );
   8973 
   8974        // We need to clear the typed value
   8975        // if the document failed to load, to make sure the urlbar reflects the
   8976        // failed URI (particularly for SSL errors). However, don't clear the value
   8977        // if the error page's URI is about:blank, because that causes complete
   8978        // loss of urlbar contents for invalid URI errors (see bug 867957).
   8979        // Another reason to clear the userTypedValue is if this was an anchor
   8980        // navigation initiated by the user.
   8981        // Finally, we do insert the URL if this is a same-document navigation
   8982        // and the user cleared the URL manually.
   8983        if (
   8984          this.mBrowser.didStartLoadSinceLastUserTyping() ||
   8985          (isErrorPage && aLocation.spec != "about:blank") ||
   8986          (isSameDocument && this.mBrowser.isNavigating) ||
   8987          (isSameDocument && !this.mBrowser.userTypedValue)
   8988        ) {
   8989          this.mBrowser.userTypedValue = null;
   8990        }
   8991 
   8992        // If the tab has been set to "busy" outside the stateChange
   8993        // handler below (e.g. by sessionStore.navigateAndRestore), and
   8994        // the load results in an error page, it's possible that there
   8995        // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
   8996        // attribute being removed. In this case we should remove the
   8997        // attribute here.
   8998        if (isErrorPage && this.mTab.hasAttribute("busy")) {
   8999          this.mTab.removeAttribute("busy");
   9000          gBrowser._tabAttrModified(this.mTab, ["busy"]);
   9001        }
   9002 
   9003        if (!isSameDocument) {
   9004          // If the browser was playing audio, we should remove the playing state.
   9005          if (this.mTab.hasAttribute("soundplaying")) {
   9006            clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
   9007            this.mTab._soundPlayingAttrRemovalTimer = 0;
   9008            this.mTab.removeAttribute("soundplaying");
   9009            gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
   9010          }
   9011 
   9012          // If the browser was previously muted, we should restore the muted state.
   9013          if (this.mTab.hasAttribute("muted")) {
   9014            this.mTab.linkedBrowser.mute();
   9015          }
   9016 
   9017          if (gBrowser.isFindBarInitialized(this.mTab)) {
   9018            let findBar = gBrowser.getCachedFindBar(this.mTab);
   9019 
   9020            // Close the Find toolbar if we're in old-style TAF mode
   9021            if (findBar.findMode != findBar.FIND_NORMAL) {
   9022              findBar.close();
   9023            }
   9024          }
   9025 
   9026          // Note that we're not updating for same-document loads, despite
   9027          // the `title` argument to `history.pushState/replaceState`. For
   9028          // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653
   9029          // and https://github.com/whatwg/html/issues/2174
   9030          if (!isReload) {
   9031            gBrowser.setTabTitle(this.mTab);
   9032          }
   9033 
   9034          // Don't clear the favicon if this tab is in the pending
   9035          // state, as SessionStore will have set the icon for us even
   9036          // though we're pointed at an about:blank. Also don't clear it
   9037          // if the tab is in customize mode, to keep the one set by
   9038          // gCustomizeMode.setTab (bug 1551239). Also don't clear it
   9039          // if onLocationChange was triggered by a pushState or a
   9040          // replaceState (bug 550565) or a hash change (bug 408415).
   9041          if (
   9042            !this.mTab.hasAttribute("pending") &&
   9043            !this.mTab.hasAttribute("customizemode") &&
   9044            aWebProgress.isLoadingDocument
   9045          ) {
   9046            // Removing the tab's image here causes flickering, wait until the
   9047            // load is complete.
   9048            this.mBrowser.mIconURL = null;
   9049          }
   9050 
   9051          if (!isReload && aWebProgress.isLoadingDocument) {
   9052            let triggerer = gBrowser._getTriggeringPrincipalFromHistory(
   9053              this.mBrowser
   9054            );
   9055            // Typing a url, searching or clicking a bookmark will load a new
   9056            // document that is no longer tied to a navigation from the previous
   9057            // content and will have a system principal as the triggerer.
   9058            if (triggerer && triggerer.isSystemPrincipal) {
   9059              // Reset the related tab map so that the next tab opened will be related
   9060              // to this new document and not to tabs opened by the previous one.
   9061              gBrowser.clearRelatedTabs();
   9062            }
   9063          }
   9064 
   9065          if (
   9066            aRequest instanceof Ci.nsIChannel &&
   9067            !isBlankPageURL(aRequest.originalURI.spec)
   9068          ) {
   9069            this.mBrowser.originalURI = aRequest.originalURI;
   9070          }
   9071 
   9072          if (!this._allowTransparentBrowser) {
   9073            this.mBrowser.toggleAttribute(
   9074              "transparent",
   9075              AIWindow.isAIWindowActive(window) &&
   9076                AIWindow.isAIWindowContentPage(aLocation)
   9077            );
   9078          }
   9079        }
   9080 
   9081        let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
   9082        if (this.mBrowser.registeredOpenURI) {
   9083          let uri = this.mBrowser.registeredOpenURI;
   9084          gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
   9085            uri.spec,
   9086            userContextId,
   9087            this.mTab.group?.id,
   9088            PrivateBrowsingUtils.isWindowPrivate(window)
   9089          );
   9090          delete this.mBrowser.registeredOpenURI;
   9091        }
   9092        if (!isBlankPageURL(aLocation.spec)) {
   9093          gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
   9094            aLocation.spec,
   9095            userContextId,
   9096            this.mTab.group?.id,
   9097            PrivateBrowsingUtils.isWindowPrivate(window)
   9098          );
   9099          this.mBrowser.registeredOpenURI = aLocation;
   9100        }
   9101 
   9102        if (this.mTab != gBrowser.selectedTab) {
   9103          let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab);
   9104          if (tabCacheIndex != -1) {
   9105            gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
   9106            gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
   9107          }
   9108        }
   9109      }
   9110 
   9111      if (!this.mBlank || this.mBrowser.hasContentOpener) {
   9112        this._callProgressListeners("onLocationChange", [
   9113          aWebProgress,
   9114          aRequest,
   9115          aLocation,
   9116          aFlags,
   9117        ]);
   9118        if (topLevel && !isSameDocument) {
   9119          // Include the true final argument to indicate that this event is
   9120          // simulated (instead of being observed by the webProgressListener).
   9121          this._callProgressListeners("onContentBlockingEvent", [
   9122            aWebProgress,
   9123            null,
   9124            0,
   9125            true,
   9126          ]);
   9127        }
   9128      }
   9129 
   9130      if (topLevel) {
   9131        this.mBrowser.lastURI = aLocation;
   9132        this.mBrowser.lastLocationChange = Date.now();
   9133      }
   9134    }
   9135 
   9136    onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
   9137      if (this.mBlank) {
   9138        return;
   9139      }
   9140 
   9141      this._callProgressListeners("onStatusChange", [
   9142        aWebProgress,
   9143        aRequest,
   9144        aStatus,
   9145        aMessage,
   9146      ]);
   9147 
   9148      this.mMessage = aMessage;
   9149    }
   9150 
   9151    onSecurityChange(aWebProgress, aRequest, aState) {
   9152      this._callProgressListeners("onSecurityChange", [
   9153        aWebProgress,
   9154        aRequest,
   9155        aState,
   9156      ]);
   9157    }
   9158 
   9159    onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
   9160      this._callProgressListeners("onContentBlockingEvent", [
   9161        aWebProgress,
   9162        aRequest,
   9163        aEvent,
   9164      ]);
   9165    }
   9166 
   9167    onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
   9168      return this._callProgressListeners("onRefreshAttempted", [
   9169        aWebProgress,
   9170        aURI,
   9171        aDelay,
   9172        aSameURI,
   9173      ]);
   9174    }
   9175  }
   9176  TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
   9177    "nsIWebProgressListener",
   9178    "nsIWebProgressListener2",
   9179    "nsISupportsWeakReference",
   9180  ]);
   9181 
   9182  let URILoadingWrapper = {
   9183    _normalizeLoadURIOptions(browser, loadURIOptions) {
   9184      if (!loadURIOptions.triggeringPrincipal) {
   9185        throw new Error("Must load with a triggering Principal");
   9186      }
   9187 
   9188      if (
   9189        loadURIOptions.userContextId &&
   9190        loadURIOptions.userContextId != browser.getAttribute("usercontextid")
   9191      ) {
   9192        throw new Error("Cannot load with mismatched userContextId");
   9193      }
   9194 
   9195      loadURIOptions.loadFlags |= loadURIOptions.flags | LOAD_FLAGS_NONE;
   9196      delete loadURIOptions.flags;
   9197      loadURIOptions.hasValidUserGestureActivation ??=
   9198        document.hasValidTransientUserGestureActivation;
   9199    },
   9200 
   9201    _loadFlagsToFixupFlags(browser, loadFlags) {
   9202      // Attempt to perform URI fixup to see if we can handle this URI in chrome.
   9203      let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
   9204      if (loadFlags & LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
   9205        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   9206      }
   9207      if (loadFlags & LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
   9208        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
   9209      }
   9210      if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
   9211        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   9212      }
   9213      return fixupFlags;
   9214    },
   9215 
   9216    _fixupURIString(browser, uriString, loadURIOptions) {
   9217      let fixupFlags = this._loadFlagsToFixupFlags(
   9218        browser,
   9219        loadURIOptions.loadFlags
   9220      );
   9221 
   9222      // XXXgijs: If we switch to loading the URI we return from this method,
   9223      // rather than redoing fixup in docshell (see bug 1815509), we need to
   9224      // ensure that the loadURIOptions have the fixup flag removed here for
   9225      // loads where `uriString` already parses if just passed immediately
   9226      // to `newURI`.
   9227      // Right now this happens in nsDocShellLoadState code.
   9228      try {
   9229        let fixupInfo = Services.uriFixup.getFixupURIInfo(
   9230          uriString,
   9231          fixupFlags
   9232        );
   9233        return fixupInfo.preferredURI;
   9234      } catch (e) {
   9235        // getFixupURIInfo may throw. Just return null, our caller will deal.
   9236      }
   9237      return null;
   9238    },
   9239 
   9240    /**
   9241     * Handles URIs when we want to deal with them in chrome code rather than pass
   9242     * them down to a content browser. This can avoid unnecessary process switching
   9243     * for the browser.
   9244     *
   9245     * @param aBrowser the browser that is attempting to load the URI
   9246     * @param aUri the nsIURI that is being loaded
   9247     * @returns true if the URI is handled, otherwise false
   9248     */
   9249    _handleUriInChrome(aBrowser, aUri) {
   9250      if (aUri.scheme == "file") {
   9251        try {
   9252          let mimeType = Cc["@mozilla.org/mime;1"]
   9253            .getService(Ci.nsIMIMEService)
   9254            .getTypeFromURI(aUri);
   9255          if (mimeType == "application/x-xpinstall") {
   9256            let systemPrincipal =
   9257              Services.scriptSecurityManager.getSystemPrincipal();
   9258            AddonManager.getInstallForURL(aUri.spec, {
   9259              telemetryInfo: { source: "file-url" },
   9260            }).then(install => {
   9261              AddonManager.installAddonFromWebpage(
   9262                mimeType,
   9263                aBrowser,
   9264                systemPrincipal,
   9265                install
   9266              );
   9267            });
   9268            return true;
   9269          }
   9270        } catch (e) {
   9271          return false;
   9272        }
   9273      }
   9274 
   9275      return false;
   9276    },
   9277 
   9278    _updateTriggerMetadataForLoad(
   9279      browser,
   9280      uriString,
   9281      { loadFlags, globalHistoryOptions }
   9282    ) {
   9283      if (globalHistoryOptions?.triggeringSponsoredURL) {
   9284        try {
   9285          // Browser may access URL after fixing it up, then store the URL into DB.
   9286          // To match with it, fix the link up explicitly.
   9287          const triggeringSponsoredURL = Services.uriFixup.getFixupURIInfo(
   9288            globalHistoryOptions.triggeringSponsoredURL,
   9289            this._loadFlagsToFixupFlags(browser, loadFlags)
   9290          ).fixedURI.spec;
   9291          browser.setAttribute(
   9292            "triggeringSponsoredURL",
   9293            triggeringSponsoredURL
   9294          );
   9295          const time =
   9296            globalHistoryOptions.triggeringSponsoredURLVisitTimeMS ||
   9297            Date.now();
   9298          browser.setAttribute("triggeringSponsoredURLVisitTimeMS", time);
   9299          browser.setAttribute(
   9300            "triggeringSource",
   9301            globalHistoryOptions.triggeringSource
   9302          );
   9303        } catch (e) {}
   9304      }
   9305 
   9306      if (globalHistoryOptions?.triggeringSearchEngine) {
   9307        browser.setAttribute(
   9308          "triggeringSearchEngine",
   9309          globalHistoryOptions.triggeringSearchEngine
   9310        );
   9311        browser.setAttribute("triggeringSearchEngineURL", uriString);
   9312      } else {
   9313        browser.removeAttribute("triggeringSearchEngine");
   9314        browser.removeAttribute("triggeringSearchEngineURL");
   9315      }
   9316    },
   9317 
   9318    // Both of these are used to override functions on browser-custom-element.
   9319    fixupAndLoadURIString(browser, uriString, loadURIOptions = {}) {
   9320      this._internalMaybeFixupLoadURI(browser, uriString, null, loadURIOptions);
   9321    },
   9322    loadURI(browser, uri, loadURIOptions = {}) {
   9323      this._internalMaybeFixupLoadURI(browser, "", uri, loadURIOptions);
   9324    },
   9325 
   9326    // A shared function used by both remote and non-remote browsers to
   9327    // load a string URI or redirect it to the correct process.
   9328    _internalMaybeFixupLoadURI(browser, uriString, uri, loadURIOptions) {
   9329      this._normalizeLoadURIOptions(browser, loadURIOptions);
   9330      // Some callers pass undefined/null when calling
   9331      // loadURI/fixupAndLoadURIString. Just load about:blank instead:
   9332      if (!uriString && !uri) {
   9333        uri = Services.io.newURI("about:blank");
   9334      }
   9335 
   9336      // We need a URI in frontend code for checking various things. Ideally
   9337      // we would then also pass that URI to webnav/browsingcontext code
   9338      // for loading, but we historically haven't. Changing this would alter
   9339      // fixup scenarios in some non-obvious cases.
   9340      let startedWithURI = !!uri;
   9341      if (!uri) {
   9342        // Note: this may return null if we can't make a URI out of the input.
   9343        uri = this._fixupURIString(browser, uriString, loadURIOptions);
   9344      }
   9345 
   9346      if (uri && this._handleUriInChrome(browser, uri)) {
   9347        // If we've handled the URI in chrome, then just return here.
   9348        return;
   9349      }
   9350 
   9351      this._updateTriggerMetadataForLoad(
   9352        browser,
   9353        uriString || uri.spec,
   9354        loadURIOptions
   9355      );
   9356 
   9357      if (loadURIOptions.isCaptivePortalTab) {
   9358        browser.browsingContext.isCaptivePortalTab = true;
   9359      }
   9360 
   9361      // XXX(nika): Is `browser.isNavigating` necessary anymore?
   9362      // XXX(gijs): Unsure. But it mirrors docShell.isNavigating, but in the parent process
   9363      // (and therefore imperfectly so).
   9364      browser.isNavigating = true;
   9365 
   9366      try {
   9367        // Should more generally prefer loadURI here - see bug 1815509.
   9368        if (startedWithURI) {
   9369          browser.webNavigation.loadURI(uri, loadURIOptions);
   9370        } else {
   9371          browser.webNavigation.fixupAndLoadURIString(
   9372            uriString,
   9373            loadURIOptions
   9374          );
   9375        }
   9376      } finally {
   9377        browser.isNavigating = false;
   9378      }
   9379    },
   9380  };
   9381 } // end private scope for gBrowser
   9382 
   9383 var StatusPanel = {
   9384  // This is useful for debugging (set to `true` in the interesting state for
   9385  // the panel to remain in that state).
   9386  _frozen: false,
   9387 
   9388  get panel() {
   9389    delete this.panel;
   9390    this.panel = document.getElementById("statuspanel");
   9391    this.panel.addEventListener(
   9392      "transitionend",
   9393      this._onTransitionEnd.bind(this)
   9394    );
   9395    this.panel.addEventListener(
   9396      "transitioncancel",
   9397      this._onTransitionEnd.bind(this)
   9398    );
   9399    return this.panel;
   9400  },
   9401 
   9402  get isVisible() {
   9403    return !this.panel.hasAttribute("inactive");
   9404  },
   9405 
   9406  update() {
   9407    if (BrowserHandler.kiosk || this._frozen) {
   9408      return;
   9409    }
   9410    let text;
   9411    let type;
   9412    let types = ["overLink"];
   9413    if (XULBrowserWindow.busyUI) {
   9414      types.push("status");
   9415    }
   9416    types.push("letterboxingStatus");
   9417    types.push("defaultStatus");
   9418    for (type of types) {
   9419      if ((text = XULBrowserWindow[type])) {
   9420        break;
   9421      }
   9422    }
   9423 
   9424    // If it's a long data: URI that uses base64 encoding, truncate to
   9425    // a reasonable length rather than trying to display the entire thing.
   9426    // We can't shorten arbitrary URIs like this, as bidi etc might mean
   9427    // we need the trailing characters for display. But a base64-encoded
   9428    // data-URI is plain ASCII, so this is OK for status panel display.
   9429    // (See bug 1484071.)
   9430    let textCropped = false;
   9431    if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) {
   9432      text = text.substring(0, 500) + "\u2026";
   9433      textCropped = true;
   9434    }
   9435 
   9436    if (this._labelElement.value != text || (text && !this.isVisible)) {
   9437      this.panel.setAttribute("previoustype", this.panel.getAttribute("type"));
   9438      this.panel.setAttribute("type", type);
   9439 
   9440      this._label = text;
   9441      this._labelElement.setAttribute(
   9442        "crop",
   9443        type == "overLink" && !textCropped ? "center" : "end"
   9444      );
   9445    }
   9446  },
   9447 
   9448  get _labelElement() {
   9449    delete this._labelElement;
   9450    return (this._labelElement = document.getElementById("statuspanel-label"));
   9451  },
   9452 
   9453  set _label(val) {
   9454    if (!this.isVisible) {
   9455      this.panel.removeAttribute("mirror");
   9456      this.panel.removeAttribute("sizelimit");
   9457    }
   9458 
   9459    if (
   9460      this.panel.getAttribute("type") == "status" &&
   9461      this.panel.getAttribute("previoustype") == "status"
   9462    ) {
   9463      // Before updating the label, set the panel's current width as its
   9464      // min-width to let the panel grow but not shrink and prevent
   9465      // unnecessary flicker while loading pages. We only care about the
   9466      // panel's width once it has been painted, so we can do this
   9467      // without flushing layout.
   9468      this.panel.style.minWidth =
   9469        window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px";
   9470    } else {
   9471      this.panel.style.minWidth = "";
   9472    }
   9473 
   9474    if (val) {
   9475      this._labelElement.value = val;
   9476      if (this.panel.hidden) {
   9477        this.panel.hidden = false;
   9478        // This ensures that the "inactive" attribute removal triggers a
   9479        // transition.
   9480        getComputedStyle(this.panel).display;
   9481      }
   9482      this.panel.removeAttribute("inactive");
   9483      MousePosTracker.addListener(this);
   9484    } else {
   9485      this.panel.setAttribute("inactive", "true");
   9486      MousePosTracker.removeListener(this);
   9487    }
   9488  },
   9489 
   9490  _onTransitionEnd() {
   9491    if (!this.isVisible) {
   9492      this.panel.hidden = true;
   9493    }
   9494  },
   9495 
   9496  getMouseTargetRect() {
   9497    let container = this.panel.parentNode;
   9498    let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
   9499    let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);
   9500 
   9501    return {
   9502      top: panelRect.top,
   9503      bottom: panelRect.bottom,
   9504      left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left,
   9505      right: RTL_UI
   9506        ? containerRect.right
   9507        : containerRect.left + panelRect.width,
   9508    };
   9509  },
   9510 
   9511  onMouseEnter() {
   9512    this._mirror();
   9513  },
   9514 
   9515  onMouseLeave() {
   9516    this._mirror();
   9517  },
   9518 
   9519  _mirror() {
   9520    if (this._frozen) {
   9521      return;
   9522    }
   9523    if (this.panel.hasAttribute("mirror")) {
   9524      this.panel.removeAttribute("mirror");
   9525    } else {
   9526      this.panel.setAttribute("mirror", "true");
   9527    }
   9528 
   9529    if (!this.panel.hasAttribute("sizelimit")) {
   9530      this.panel.setAttribute("sizelimit", "true");
   9531    }
   9532  },
   9533 };
   9534 
   9535 var TabBarVisibility = {
   9536  _initialUpdateDone: false,
   9537 
   9538  update(force = false) {
   9539    let isPopup = !window.toolbar.visible;
   9540    let isTaskbarTab = document.documentElement.hasAttribute("taskbartab");
   9541    let isSingleTabWindow = isPopup || isTaskbarTab;
   9542 
   9543    let hasVerticalTabs =
   9544      !isSingleTabWindow &&
   9545      Services.prefs.getBoolPref("sidebar.verticalTabs", false);
   9546 
   9547    // When `gBrowser` has not been initialized, we're opening a new window and
   9548    // assume only a single tab is loading.
   9549    let hasSingleTab = !gBrowser || gBrowser.visibleTabs.length == 1;
   9550 
   9551    // To prevent tabs being lost, hiding the tabs toolbar should only work
   9552    // when only a single tab is visible or tabs are displayed elsewhere.
   9553    let hideTabsToolbar =
   9554      (isSingleTabWindow && hasSingleTab) || hasVerticalTabs;
   9555 
   9556    // We only want a non-customized titlebar for popups. It should not be the
   9557    // case, but if a popup window contains more than one tab we re-enable
   9558    // titlebar customization and display tabs.
   9559    CustomTitlebar.allowedBy("non-popup", !(isPopup && hasSingleTab));
   9560 
   9561    // Update the browser chrome.
   9562 
   9563    let tabsToolbar = document.getElementById("TabsToolbar");
   9564    let navbar = document.getElementById("nav-bar");
   9565 
   9566    gNavToolbox.toggleAttribute("tabs-hidden", hideTabsToolbar);
   9567    // Should the nav-bar look and function like a titlebar?
   9568    navbar.classList.toggle(
   9569      "browser-titlebar",
   9570      CustomTitlebar.enabled && hideTabsToolbar
   9571    );
   9572 
   9573    document
   9574      .getElementById("browser")
   9575      .classList.toggle(
   9576        "browser-toolbox-background",
   9577        CustomTitlebar.enabled && hasVerticalTabs
   9578      );
   9579 
   9580    if (
   9581      hideTabsToolbar == tabsToolbar.collapsed &&
   9582      !force &&
   9583      this._initialUpdateDone
   9584    ) {
   9585      // No further updates needed, `TabsToolbar` already matches the expected
   9586      // visibilty.
   9587      return;
   9588    }
   9589    this._initialUpdateDone = true;
   9590 
   9591    tabsToolbar.collapsed = hideTabsToolbar;
   9592 
   9593    // Stylize close menu items based on tab visibility. When a window will only
   9594    // ever have a single tab, only show the option to close the tab, and
   9595    // simplify the text since we don't need to disambiguate from closing the window.
   9596    document.getElementById("menu_closeWindow").hidden = hideTabsToolbar;
   9597    document.l10n.setAttributes(
   9598      document.getElementById("menu_close"),
   9599      hideTabsToolbar
   9600        ? "tabbrowser-menuitem-close"
   9601        : "tabbrowser-menuitem-close-tab"
   9602    );
   9603  },
   9604 };
   9605 
   9606 var TabContextMenu = {
   9607  contextTab: null,
   9608  _updateToggleMuteMenuItems(aTab, aConditionFn) {
   9609    ["muted", "soundplaying"].forEach(attr => {
   9610      if (!aConditionFn || aConditionFn(attr)) {
   9611        if (aTab.hasAttribute(attr)) {
   9612          aTab.toggleMuteMenuItem.setAttribute(attr, "true");
   9613          aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
   9614        } else {
   9615          aTab.toggleMuteMenuItem.removeAttribute(attr);
   9616          aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
   9617        }
   9618      }
   9619    });
   9620  },
   9621  // eslint-disable-next-line complexity
   9622  updateContextMenu(aPopupMenu) {
   9623    let triggerTab =
   9624      aPopupMenu.triggerNode &&
   9625      (aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab"));
   9626    this.contextTab = triggerTab || gBrowser.selectedTab;
   9627    this.contextTab.addEventListener("TabAttrModified", this);
   9628    aPopupMenu.addEventListener("popuphidden", this);
   9629 
   9630    this.multiselected = this.contextTab.multiselected;
   9631    this.contextTabs = this.multiselected
   9632      ? gBrowser.selectedTabs
   9633      : [this.contextTab];
   9634 
   9635    let splitViews = new Set();
   9636    // bug1973996: This call is not guaranteed to complete
   9637    // before the saved groups menu is populated
   9638    for (let tab of this.contextTabs) {
   9639      gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
   9640 
   9641      // Add unique split views for count info below
   9642      if (tab.splitview) {
   9643        splitViews.add(tab.splitview);
   9644      }
   9645    }
   9646 
   9647    let disabled = gBrowser.tabs.length == 1;
   9648    let tabCountInfo = JSON.stringify({
   9649      tabCount: this.contextTabs.length,
   9650    });
   9651    let splitViewCountInfo = JSON.stringify({
   9652      splitViewCount: splitViews.size,
   9653    });
   9654 
   9655    var menuItems = aPopupMenu.getElementsByAttribute(
   9656      "tbattr",
   9657      "tabbrowser-multiple"
   9658    );
   9659    for (let menuItem of menuItems) {
   9660      menuItem.disabled = disabled;
   9661    }
   9662 
   9663    disabled = gBrowser.visibleTabs.length == 1;
   9664    menuItems = aPopupMenu.getElementsByAttribute(
   9665      "tbattr",
   9666      "tabbrowser-multiple-visible"
   9667    );
   9668    for (let menuItem of menuItems) {
   9669      menuItem.disabled = disabled;
   9670    }
   9671 
   9672    let contextNewTabButton = document.getElementById("context_openANewTab");
   9673    // update context menu item strings for vertical tabs
   9674    document.l10n.setAttributes(
   9675      contextNewTabButton,
   9676      gBrowser.tabContainer?.verticalMode
   9677        ? "tab-context-new-tab-open-vertical"
   9678        : "tab-context-new-tab-open"
   9679    );
   9680 
   9681    // Session store
   9682    let closedCount = SessionStore.getLastClosedTabCount(window);
   9683    document
   9684      .getElementById("History:UndoCloseTab")
   9685      .toggleAttribute("disabled", closedCount == 0);
   9686    document.l10n.setArgs(document.getElementById("context_undoCloseTab"), {
   9687      tabCount: closedCount,
   9688    });
   9689 
   9690    // Show/hide fullscreen context menu items and set the
   9691    // autohide item's checked state to mirror the autohide pref.
   9692    showFullScreenViewContextMenuItems(aPopupMenu);
   9693 
   9694    // #context_moveTabToNewGroup is a simplified context menu item that only
   9695    // appears if there are no existing tab groups available to move the tab to.
   9696    let contextMoveTabToNewGroup = document.getElementById(
   9697      "context_moveTabToNewGroup"
   9698    );
   9699    let contextMoveTabToGroup = document.getElementById(
   9700      "context_moveTabToGroup"
   9701    );
   9702    let contextUngroupTab = document.getElementById("context_ungroupTab");
   9703    let contextMoveSplitViewToNewGroup = document.getElementById(
   9704      "context_moveSplitViewToNewGroup"
   9705    );
   9706    let contextUngroupSplitView = document.getElementById(
   9707      "context_ungroupSplitView"
   9708    );
   9709    let isAllSplitViewTabs = this.contextTabs.every(
   9710      contextTab => contextTab.splitview
   9711    );
   9712 
   9713    if (gBrowser._tabGroupsEnabled) {
   9714      let selectedGroupCount = new Set(
   9715        // The filter removes the "null" group for ungrouped tabs.
   9716        this.contextTabs.map(t => t.group).filter(g => g)
   9717      ).size;
   9718 
   9719      let openGroupsToMoveTo = gBrowser.getAllTabGroups({
   9720        sortByLastSeenActive: true,
   9721      });
   9722 
   9723      // Determine whether or not the "current" tab group should appear in the
   9724      // "move tab to group" context menu.
   9725      if (selectedGroupCount == 1) {
   9726        let groupToFilter = this.contextTabs[0].group;
   9727        if (groupToFilter && this.contextTabs.every(t => t.group)) {
   9728          openGroupsToMoveTo = openGroupsToMoveTo.filter(
   9729            group => group !== groupToFilter
   9730          );
   9731        }
   9732      }
   9733 
   9734      // Populate the saved groups context menu
   9735      // Only enable in non-private windows, or if at least one of the tabs is
   9736      // considered saveable
   9737      let savedGroupsToMoveTo = [];
   9738      if (
   9739        !PrivateBrowsingUtils.isWindowPrivate(window) &&
   9740        SessionStore.shouldSaveTabsToGroup(this.contextTabs)
   9741      ) {
   9742        savedGroupsToMoveTo = SessionStore.getSavedTabGroups();
   9743      }
   9744 
   9745      if (!openGroupsToMoveTo.length && !savedGroupsToMoveTo.length) {
   9746        if (isAllSplitViewTabs) {
   9747          contextMoveTabToGroup.hidden = true;
   9748          contextMoveTabToNewGroup.hidden = true;
   9749          contextMoveSplitViewToNewGroup.hidden = false;
   9750          contextMoveSplitViewToNewGroup.setAttribute(
   9751            "data-l10n-args",
   9752            splitViewCountInfo
   9753          );
   9754        } else {
   9755          contextMoveTabToGroup.hidden = true;
   9756          contextMoveSplitViewToNewGroup.hidden = true;
   9757          contextMoveTabToNewGroup.hidden = false;
   9758          contextMoveTabToNewGroup.setAttribute("data-l10n-args", tabCountInfo);
   9759        }
   9760      } else {
   9761        if (isAllSplitViewTabs) {
   9762          contextMoveTabToNewGroup.hidden = true;
   9763          contextMoveSplitViewToNewGroup.hidden = true;
   9764          contextMoveTabToGroup.hidden = false;
   9765          contextMoveTabToGroup.setAttribute(
   9766            "data-l10n-id",
   9767            "tab-context-move-split-view-to-group"
   9768          );
   9769          contextMoveTabToGroup.setAttribute(
   9770            "data-l10n-args",
   9771            splitViewCountInfo
   9772          );
   9773        } else {
   9774          contextMoveTabToNewGroup.hidden = true;
   9775          contextMoveSplitViewToNewGroup.hidden = true;
   9776          contextMoveTabToGroup.hidden = false;
   9777          contextMoveTabToGroup.setAttribute(
   9778            "data-l10n-id",
   9779            "tab-context-move-tab-to-group"
   9780          );
   9781          contextMoveTabToGroup.setAttribute("data-l10n-args", tabCountInfo);
   9782        }
   9783 
   9784        const openGroupsMenu = contextMoveTabToGroup.querySelector("menupopup");
   9785        openGroupsMenu
   9786          .querySelectorAll("[tab-group-id]")
   9787          .forEach(el => el.remove());
   9788        const upperSeparator = openGroupsMenu.querySelector(
   9789          `#open-tab-groups-separator-upper`
   9790        );
   9791        const lowerSeparator = openGroupsMenu.querySelector(
   9792          `#open-tab-groups-separator-lower`
   9793        );
   9794 
   9795        lowerSeparator.hidden = !openGroupsToMoveTo.length;
   9796 
   9797        openGroupsToMoveTo.toReversed().forEach(group => {
   9798          let item = this._createTabGroupMenuItem(group, false);
   9799          upperSeparator.after(item);
   9800        });
   9801 
   9802        const savedGroupsMenu = contextMoveTabToGroup.querySelector(
   9803          "#context_moveTabToSavedGroup"
   9804        );
   9805        const savedGroupsMenuPopup = savedGroupsMenu.querySelector("menupopup");
   9806 
   9807        savedGroupsMenuPopup
   9808          .querySelectorAll("[tab-group-id]")
   9809          .forEach(el => el.remove());
   9810        if (savedGroupsToMoveTo.length) {
   9811          savedGroupsMenu.disabled = false;
   9812 
   9813          savedGroupsToMoveTo.forEach(group => {
   9814            let item = this._createTabGroupMenuItem(group, true);
   9815            savedGroupsMenuPopup.appendChild(item);
   9816          });
   9817        } else {
   9818          savedGroupsMenu.disabled = true;
   9819        }
   9820      }
   9821 
   9822      let groupInfo = JSON.stringify({
   9823        groupCount: selectedGroupCount,
   9824      });
   9825      if (isAllSplitViewTabs) {
   9826        contextUngroupSplitView.hidden = !selectedGroupCount;
   9827        contextUngroupTab.hidden = true;
   9828        contextUngroupSplitView.setAttribute("data-l10n-args", groupInfo);
   9829      } else {
   9830        contextUngroupTab.hidden = !selectedGroupCount;
   9831        contextUngroupSplitView.hidden = true;
   9832        contextUngroupTab.setAttribute("data-l10n-args", groupInfo);
   9833      }
   9834    } else {
   9835      contextMoveTabToNewGroup.hidden = true;
   9836      contextMoveTabToGroup.hidden = true;
   9837      contextUngroupTab.hidden = true;
   9838      contextMoveSplitViewToNewGroup.hidden = true;
   9839      contextUngroupSplitView.hidden = true;
   9840    }
   9841 
   9842    let contextAddNote = document.getElementById("context_addNote");
   9843    let contextUpdateNote = document.getElementById("context_updateNote");
   9844    if (gBrowser._tabNotesEnabled) {
   9845      // Tab notes behaviour is disabled if a user has a selection of tabs that
   9846      // contains more than one canonical URL.
   9847      let multiselectingDiverseUrls =
   9848        this.multiselected &&
   9849        !this.contextTabs.every(
   9850          t => t.canonicalUrl === this.contextTabs[0].canonicalUrl
   9851        );
   9852 
   9853      contextAddNote.disabled =
   9854        multiselectingDiverseUrls || !this.TabNotes.isEligible(this.contextTab);
   9855      contextUpdateNote.disabled = multiselectingDiverseUrls;
   9856 
   9857      this.TabNotes.has(this.contextTab).then(hasNote => {
   9858        contextAddNote.hidden = hasNote;
   9859        contextUpdateNote.hidden = !hasNote;
   9860      });
   9861    } else {
   9862      contextAddNote.hidden = true;
   9863      contextUpdateNote.hidden = true;
   9864    }
   9865 
   9866    // Split View
   9867    let splitViewEnabled = Services.prefs.getBoolPref(
   9868      "browser.tabs.splitView.enabled",
   9869      false
   9870    );
   9871    let contextMoveTabToNewSplitView = document.getElementById(
   9872      "context_moveTabToSplitView"
   9873    );
   9874    let contextSeparateSplitView = document.getElementById(
   9875      "context_separateSplitView"
   9876    );
   9877    let hasSplitViewTab = this.contextTabs.some(tab => tab.splitview);
   9878    contextMoveTabToNewSplitView.hidden = !splitViewEnabled || hasSplitViewTab;
   9879    contextSeparateSplitView.hidden = !splitViewEnabled || !hasSplitViewTab;
   9880    if (splitViewEnabled) {
   9881      contextMoveTabToNewSplitView.removeAttribute("data-l10n-id");
   9882      contextMoveTabToNewSplitView.setAttribute(
   9883        "data-l10n-id",
   9884        this.contextTabs.length < 2
   9885          ? "tab-context-add-split-view"
   9886          : "tab-context-open-in-split-view"
   9887      );
   9888 
   9889      let pinnedTabs = this.contextTabs.filter(t => t.pinned);
   9890      contextMoveTabToNewSplitView.disabled =
   9891        this.contextTabs.length > 2 || pinnedTabs.length;
   9892    }
   9893 
   9894    // Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
   9895    document.getElementById("context_reloadTab").hidden = this.multiselected;
   9896    document.getElementById("context_reloadSelectedTabs").hidden =
   9897      !this.multiselected;
   9898    let unloadTabItem = document.getElementById("context_unloadTab");
   9899    if (gBrowser._unloadTabInContextMenu) {
   9900      // linkedPanel is false if the tab is already unloaded
   9901      // Cannot unload about: pages, etc., so skip browsers that are not remote
   9902      let unloadableTabs = this.contextTabs.filter(
   9903        t => t.linkedPanel && t.linkedBrowser?.isRemoteBrowser
   9904      );
   9905      unloadTabItem.hidden = unloadableTabs.length === 0;
   9906      unloadTabItem.setAttribute(
   9907        "data-l10n-args",
   9908        JSON.stringify({ tabCount: unloadableTabs.length })
   9909      );
   9910    } else {
   9911      unloadTabItem.hidden = true;
   9912    }
   9913 
   9914    // Show Play Tab menu item if the tab has attribute activemedia-blocked
   9915    document.getElementById("context_playTab").hidden = !(
   9916      this.contextTab.activeMediaBlocked && !this.multiselected
   9917    );
   9918    document.getElementById("context_playSelectedTabs").hidden = !(
   9919      this.contextTab.activeMediaBlocked && this.multiselected
   9920    );
   9921 
   9922    // Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
   9923    let contextPinTab = document.getElementById("context_pinTab");
   9924    contextPinTab.hidden = this.contextTab.pinned || this.multiselected;
   9925    let contextUnpinTab = document.getElementById("context_unpinTab");
   9926    contextUnpinTab.hidden = !this.contextTab.pinned || this.multiselected;
   9927    let contextPinSelectedTabs = document.getElementById(
   9928      "context_pinSelectedTabs"
   9929    );
   9930    contextPinSelectedTabs.hidden =
   9931      this.contextTab.pinned || !this.multiselected;
   9932    let contextUnpinSelectedTabs = document.getElementById(
   9933      "context_unpinSelectedTabs"
   9934    );
   9935    contextUnpinSelectedTabs.hidden =
   9936      !this.contextTab.pinned || !this.multiselected;
   9937 
   9938    // Build Ask Chat items
   9939    // GenAI is missing. tor-browser#44045.
   9940 
   9941    // Move Tab items
   9942    let contextMoveTabOptions = document.getElementById(
   9943      "context_moveTabOptions"
   9944    );
   9945    // gBrowser.visibleTabs excludes tabs in collapsed groups,
   9946    // which we want to include in calculations for Move Tab items
   9947    let visibleOrCollapsedTabs = gBrowser.tabs.filter(
   9948      t => t.isOpen && !t.hidden
   9949    );
   9950    let allTabsSelected = visibleOrCollapsedTabs.every(t => t.multiselected);
   9951    contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
   9952    contextMoveTabOptions.disabled = this.contextTab.hidden || allTabsSelected;
   9953    let selectedTabs = gBrowser.selectedTabs;
   9954    let contextMoveTabToEnd = document.getElementById("context_moveToEnd");
   9955    let allSelectedTabsAdjacent = selectedTabs.every(
   9956      (element, index, array) => {
   9957        return array.length > index + 1
   9958          ? element._tPos + 1 == array[index + 1]._tPos
   9959          : true;
   9960      }
   9961    );
   9962 
   9963    let lastVisibleTab = visibleOrCollapsedTabs.at(-1);
   9964    let lastTabToMove = this.contextTabs.at(-1);
   9965 
   9966    let isLastPinnedTab = false;
   9967    if (lastTabToMove.pinned) {
   9968      let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
   9969      isLastPinnedTab = !sibling || !sibling.pinned;
   9970    }
   9971    contextMoveTabToEnd.disabled =
   9972      (lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
   9973      !lastTabToMove.group &&
   9974      allSelectedTabsAdjacent;
   9975    let contextMoveTabToStart = document.getElementById("context_moveToStart");
   9976    let isFirstTab =
   9977      !this.contextTabs[0].group &&
   9978      (this.contextTabs[0] == visibleOrCollapsedTabs[0] ||
   9979        this.contextTabs[0] == visibleOrCollapsedTabs[gBrowser.pinnedTabCount]);
   9980    contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
   9981 
   9982    document.getElementById("context_openTabInWindow").disabled =
   9983      this.contextTab.hasAttribute("customizemode");
   9984 
   9985    // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
   9986    document.getElementById("context_duplicateTab").hidden = this.multiselected;
   9987    document.getElementById("context_duplicateTabs").hidden =
   9988      !this.multiselected;
   9989 
   9990    let closeTabsToTheStartItem = document.getElementById(
   9991      "context_closeTabsToTheStart"
   9992    );
   9993 
   9994    // update context menu item strings for vertical tabs
   9995    document.l10n.setAttributes(
   9996      closeTabsToTheStartItem,
   9997      gBrowser.tabContainer?.verticalMode
   9998        ? "close-tabs-to-the-start-vertical"
   9999        : "close-tabs-to-the-start"
  10000    );
  10001 
  10002    let closeTabsToTheEndItem = document.getElementById(
  10003      "context_closeTabsToTheEnd"
  10004    );
  10005 
  10006    // update context menu item strings for vertical tabs
  10007    document.l10n.setAttributes(
  10008      closeTabsToTheEndItem,
  10009      gBrowser.tabContainer?.verticalMode
  10010        ? "close-tabs-to-the-end-vertical"
  10011        : "close-tabs-to-the-end"
  10012    );
  10013 
  10014    // Disable "Close Tabs to the Left/Right" if there are no tabs
  10015    // preceding/following it.
  10016    let noTabsToStart = !gBrowser._getTabsToTheStartFrom(this.contextTab)
  10017      .length;
  10018    closeTabsToTheStartItem.disabled = noTabsToStart;
  10019 
  10020    let noTabsToEnd = !gBrowser._getTabsToTheEndFrom(this.contextTab).length;
  10021    closeTabsToTheEndItem.disabled = noTabsToEnd;
  10022 
  10023    // Disable "Close other Tabs" if there are no unpinned tabs.
  10024    let unpinnedTabsToClose = this.multiselected
  10025      ? gBrowser.openTabs.filter(
  10026          t => !t.multiselected && !t.pinned && !t.hidden
  10027        ).length
  10028      : gBrowser.openTabs.filter(
  10029          t => t != this.contextTab && !t.pinned && !t.hidden
  10030        ).length;
  10031    let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
  10032    closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;
  10033 
  10034    // Update the close item with how many tabs will close.
  10035    document
  10036      .getElementById("context_closeTab")
  10037      .setAttribute("data-l10n-args", tabCountInfo);
  10038 
  10039    let closeDuplicateEnabled = Services.prefs.getBoolPref(
  10040      "browser.tabs.context.close-duplicate.enabled"
  10041    );
  10042    let closeDuplicateTabsItem = document.getElementById(
  10043      "context_closeDuplicateTabs"
  10044    );
  10045    closeDuplicateTabsItem.hidden = !closeDuplicateEnabled;
  10046    closeDuplicateTabsItem.disabled =
  10047      !closeDuplicateEnabled ||
  10048      !gBrowser.getDuplicateTabsToClose(this.contextTab).length;
  10049 
  10050    // Disable "Close Multiple Tabs" if all sub menuitems are disabled
  10051    document.getElementById("context_closeTabOptions").disabled =
  10052      closeTabsToTheStartItem.disabled &&
  10053      closeTabsToTheEndItem.disabled &&
  10054      closeOtherTabsItem.disabled;
  10055 
  10056    // Hide "Bookmark Tab…" for multiselection.
  10057    // Update its state if visible.
  10058    let bookmarkTab = document.getElementById("context_bookmarkTab");
  10059    bookmarkTab.hidden = this.multiselected;
  10060 
  10061    // Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
  10062    let bookmarkMultiSelectedTabs = document.getElementById(
  10063      "context_bookmarkSelectedTabs"
  10064    );
  10065    bookmarkMultiSelectedTabs.hidden = !this.multiselected;
  10066 
  10067    let toggleMute = document.getElementById("context_toggleMuteTab");
  10068    let toggleMultiSelectMute = document.getElementById(
  10069      "context_toggleMuteSelectedTabs"
  10070    );
  10071 
  10072    // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
  10073    toggleMute.hidden = this.multiselected;
  10074    toggleMultiSelectMute.hidden = !this.multiselected;
  10075 
  10076    const isMuted = this.contextTab.hasAttribute("muted");
  10077    document.l10n.setAttributes(
  10078      toggleMute,
  10079      isMuted ? "tabbrowser-context-unmute-tab" : "tabbrowser-context-mute-tab"
  10080    );
  10081    document.l10n.setAttributes(
  10082      toggleMultiSelectMute,
  10083      isMuted
  10084        ? "tabbrowser-context-unmute-selected-tabs"
  10085        : "tabbrowser-context-mute-selected-tabs"
  10086    );
  10087 
  10088    this.contextTab.toggleMuteMenuItem = toggleMute;
  10089    this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
  10090    this._updateToggleMuteMenuItems(this.contextTab);
  10091 
  10092    let selectAllTabs = document.getElementById("context_selectAllTabs");
  10093    selectAllTabs.disabled = gBrowser.allTabsSelected();
  10094 
  10095    gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
  10096 
  10097    let reopenInContainer = document.getElementById(
  10098      "context_reopenInContainer"
  10099    );
  10100    reopenInContainer.hidden =
  10101      !Services.prefs.getBoolPref("privacy.userContext.enabled", false) ||
  10102      PrivateBrowsingUtils.isWindowPrivate(window);
  10103    reopenInContainer.disabled = this.contextTab.hidden;
  10104 
  10105    SharingUtils.updateShareURLMenuItem(
  10106      this.contextTab.linkedBrowser,
  10107      document.getElementById("context_moveTabOptions")
  10108    );
  10109  },
  10110 
  10111  _createTabGroupMenuItem(group, isSaved) {
  10112    let item = document.createXULElement("menuitem");
  10113    item.setAttribute("tab-group-id", group.id);
  10114 
  10115    // Open groups have labels, and saved groups have names
  10116    let label = group.label ?? group.name;
  10117    if (label) {
  10118      item.setAttribute("label", label);
  10119    } else {
  10120      document.l10n.setAttributes(item, "tab-context-unnamed-group");
  10121    }
  10122 
  10123    item.classList.add("menuitem-iconic", "tab-group-icon");
  10124    if (isSaved) {
  10125      item.classList.add("tab-group-icon-closed");
  10126    }
  10127 
  10128    item.style.setProperty(
  10129      "--tab-group-color",
  10130      `var(--tab-group-color-${group.color})`
  10131    );
  10132    item.style.setProperty(
  10133      "--tab-group-color-invert",
  10134      `var(--tab-group-color-${group.color}-invert)`
  10135    );
  10136    item.style.setProperty(
  10137      "--tab-group-color-pale",
  10138      `var(--tab-group-color-${group.color}-pale)`
  10139    );
  10140 
  10141    return item;
  10142  },
  10143 
  10144  handleEvent(aEvent) {
  10145    switch (aEvent.type) {
  10146      case "popuphidden":
  10147        if (aEvent.target.id == "tabContextMenu") {
  10148          this.contextTab.removeEventListener("TabAttrModified", this);
  10149          this.contextTab = null;
  10150          this.contextTabs = null;
  10151        }
  10152        break;
  10153      case "TabAttrModified": {
  10154        let tab = aEvent.target;
  10155        this._updateToggleMuteMenuItems(tab, attr =>
  10156          aEvent.detail.changed.includes(attr)
  10157        );
  10158        break;
  10159      }
  10160    }
  10161  },
  10162 
  10163  createReopenInContainerMenu(event) {
  10164    createUserContextMenu(event, {
  10165      isContextMenu: true,
  10166      excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
  10167    });
  10168  },
  10169  duplicateSelectedTabs() {
  10170    let newIndex = this.contextTabs.at(-1)._tPos + 1;
  10171    for (let tab of this.contextTabs) {
  10172      let newTab = SessionStore.duplicateTab(window, tab);
  10173      if (tab.group) {
  10174        Glean.tabgroup.tabInteractions.duplicate.add();
  10175      }
  10176      gBrowser.moveTabTo(newTab, { tabIndex: newIndex++ });
  10177    }
  10178  },
  10179  reopenInContainer(event) {
  10180    let userContextId = parseInt(
  10181      event.target.getAttribute("data-usercontextid")
  10182    );
  10183 
  10184    for (let tab of this.contextTabs) {
  10185      if (tab.getAttribute("usercontextid") == userContextId) {
  10186        continue;
  10187      }
  10188 
  10189      /* Create a triggering principal that is able to load the new tab
  10190         For content principals that are about: chrome: or resource: we need system to load them.
  10191         Anything other than system principal needs to have the new userContextId.
  10192      */
  10193      let triggeringPrincipal;
  10194 
  10195      if (tab.linkedPanel) {
  10196        triggeringPrincipal = tab.linkedBrowser.contentPrincipal;
  10197      } else {
  10198        // For lazy tab browsers, get the original principal
  10199        // from SessionStore
  10200        let tabState = JSON.parse(SessionStore.getTabState(tab));
  10201        try {
  10202          triggeringPrincipal = E10SUtils.deserializePrincipal(
  10203            tabState.triggeringPrincipal_base64
  10204          );
  10205        } catch (ex) {
  10206          continue;
  10207        }
  10208      }
  10209 
  10210      if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) {
  10211        // Ensure that we have a null principal if we couldn't
  10212        // deserialize it (for lazy tab browsers) ...
  10213        // This won't always work however is safe to use.
  10214        triggeringPrincipal =
  10215          Services.scriptSecurityManager.createNullPrincipal({ userContextId });
  10216      } else if (triggeringPrincipal.isContentPrincipal) {
  10217        triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
  10218          triggeringPrincipal,
  10219          {
  10220            userContextId,
  10221          }
  10222        );
  10223      }
  10224 
  10225      let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, {
  10226        userContextId,
  10227        pinned: tab.pinned,
  10228        tabIndex: tab._tPos + 1,
  10229        triggeringPrincipal,
  10230      });
  10231 
  10232      Glean.containers.tabAssignedContainer.record({
  10233        from_container_id: tab.getAttribute("usercontextid"),
  10234        to_container_id: userContextId,
  10235      });
  10236 
  10237      if (gBrowser.selectedTab == tab) {
  10238        gBrowser.selectedTab = newTab;
  10239      }
  10240      if (tab.muted && !newTab.muted) {
  10241        newTab.toggleMuteAudio(tab.muteReason);
  10242      }
  10243    }
  10244  },
  10245 
  10246  closeContextTabs() {
  10247    if (this.contextTab.multiselected) {
  10248      gBrowser.removeMultiSelectedTabs(
  10249        gBrowser.TabMetrics.userTriggeredContext(
  10250          gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP
  10251        )
  10252      );
  10253    } else {
  10254      gBrowser.removeTab(this.contextTab, {
  10255        animate: true,
  10256        ...gBrowser.TabMetrics.userTriggeredContext(
  10257          gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP
  10258        ),
  10259      });
  10260    }
  10261  },
  10262 
  10263  explicitUnloadTabs() {
  10264    gBrowser.explicitUnloadTabs(this.contextTabs);
  10265  },
  10266 
  10267  moveTabsToNewGroup() {
  10268    let insertBefore = this.contextTab;
  10269    if (insertBefore._tPos < gBrowser.pinnedTabCount) {
  10270      insertBefore = gBrowser.tabs[gBrowser.pinnedTabCount];
  10271    } else if (this.contextTab.group) {
  10272      insertBefore = this.contextTab.group;
  10273    } else if (this.contextTab.splitview) {
  10274      insertBefore = this.contextTab.splitview;
  10275    }
  10276    gBrowser.addTabGroup(this.contextTabs, {
  10277      insertBefore,
  10278      isUserTriggered: true,
  10279      telemetryUserCreateSource: "tab_menu",
  10280    });
  10281    gBrowser.selectedTab = this.contextTabs[0];
  10282 
  10283    // When using the tab context menu to create a group from the all tabs
  10284    // panel, make sure we close that panel so that it doesn't obscure the tab
  10285    // group creation panel.
  10286    gTabsPanel.hideAllTabsPanel();
  10287  },
  10288 
  10289  moveSplitViewToNewGroup() {
  10290    let insertBefore = this.contextTab;
  10291    if (insertBefore._tPos < gBrowser.pinnedTabCount) {
  10292      insertBefore = gBrowser.tabs[gBrowser.pinnedTabCount];
  10293    } else if (this.contextTab.group) {
  10294      insertBefore = this.contextTab.group;
  10295    } else if (this.contextTab.splitview) {
  10296      insertBefore = this.contextTab.splitview;
  10297    }
  10298    let tabsAndSplitViews = [];
  10299    for (const contextTab of this.contextTabs) {
  10300      if (contextTab.splitView) {
  10301        if (!tabsAndSplitViews.includes(contextTab.splitView)) {
  10302          tabsAndSplitViews.push(contextTab.splitView);
  10303        }
  10304      } else {
  10305        tabsAndSplitViews.push(contextTab);
  10306      }
  10307    }
  10308    gBrowser.addTabGroup(tabsAndSplitViews, {
  10309      insertBefore,
  10310      isUserTriggered: true,
  10311      telemetryUserCreateSource: "tab_menu",
  10312    });
  10313    gBrowser.selectedTab = this.contextTabs[0];
  10314 
  10315    // When using the tab context menu to create a group from the all tabs
  10316    // panel, make sure we close that panel so that it doesn't obscure the tab
  10317    // group creation panel.
  10318    gTabsPanel.hideAllTabsPanel();
  10319  },
  10320 
  10321  /**
  10322   * @param {MozTabbrowserTabGroup} group
  10323   */
  10324  moveTabsToGroup(group) {
  10325    group.addTabs(
  10326      this.contextTabs,
  10327      gBrowser.TabMetrics.userTriggeredContext(
  10328        gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU
  10329      )
  10330    );
  10331    group.ownerGlobal.focus();
  10332  },
  10333 
  10334  addTabsToSavedGroup(groupId) {
  10335    SessionStore.addTabsToSavedGroup(
  10336      groupId,
  10337      this.contextTabs,
  10338      gBrowser.TabMetrics.userTriggeredContext(
  10339        gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU
  10340      )
  10341    );
  10342    this.closeContextTabs();
  10343  },
  10344 
  10345  ungroupTabs() {
  10346    for (let i = this.contextTabs.length - 1; i >= 0; i--) {
  10347      gBrowser.ungroupTab(this.contextTabs[i]);
  10348    }
  10349  },
  10350 
  10351  ungroupSplitViews() {
  10352    let splitViews = new Set();
  10353    for (const tab of this.contextTabs) {
  10354      if (!splitViews.has(tab.splitview)) {
  10355        splitViews.add(tab.splitview);
  10356        gBrowser.ungroupSplitView(tab.splitview);
  10357      }
  10358    }
  10359  },
  10360 
  10361  moveTabsToSplitView() {
  10362    let insertBefore = this.contextTabs.includes(gBrowser.selectedTab)
  10363      ? gBrowser.selectedTab
  10364      : this.contextTabs[0];
  10365    let tabsToAdd = this.contextTabs;
  10366 
  10367    // Ensure selected tab is always first in split view
  10368    const selectedTabIndex = tabsToAdd.indexOf(gBrowser.selectedTab);
  10369    if (selectedTabIndex > -1 && selectedTabIndex != 0) {
  10370      const [removed] = tabsToAdd.splice(selectedTabIndex, 1);
  10371      tabsToAdd.unshift(removed);
  10372    }
  10373 
  10374    let newTab = null;
  10375    if (this.contextTabs.length < 2) {
  10376      // Open new tab to split with context tab
  10377      newTab = gBrowser.addTrustedTab("about:opentabs");
  10378      tabsToAdd = [this.contextTabs[0], newTab];
  10379    }
  10380 
  10381    gBrowser.addTabSplitView(tabsToAdd, {
  10382      insertBefore,
  10383    });
  10384 
  10385    if (newTab) {
  10386      gBrowser.selectedTab = newTab;
  10387    }
  10388  },
  10389 
  10390  unsplitTabs() {
  10391    const splitviews = new Set(
  10392      this.contextTabs.map(tab => tab.splitview).filter(Boolean)
  10393    );
  10394    splitviews.forEach(splitview => gBrowser.unsplitTabs(splitview));
  10395  },
  10396 
  10397  addNewBadge() {
  10398    let badgeNewMenuItems = document.querySelectorAll(
  10399      "#tabContextMenu menuitem.badge-new"
  10400    );
  10401 
  10402    badgeNewMenuItems.forEach(badgedMenuItem => {
  10403      badgedMenuItem.setAttribute(
  10404        "badge",
  10405        gBrowser.tabLocalization.formatValueSync("tab-context-badge-new")
  10406      );
  10407    });
  10408  },
  10409 
  10410  deleteTabNotes() {
  10411    for (let tab of this.contextTabs) {
  10412      this.TabNotes.delete(tab, {
  10413        telemetrySource: this.TabNotes.TELEMETRY_SOURCE.TAB_CONTEXT_MENU,
  10414      });
  10415    }
  10416  },
  10417 };
  10418 
  10419 ChromeUtils.defineESModuleGetters(TabContextMenu, {
  10420  // GenAI.sys.mjs is missing. tor-browser#44045.
  10421  TabNotes: "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs",
  10422 });