tor-browser

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

browser-sidebar.js (82132B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * SidebarController handles logic such as toggling sidebar panels,
      7 * dynamically adding menubar menu items for the View -> Sidebar menu,
      8 * and provides APIs for sidebar extensions, etc.
      9 */
     10 
     11 const { DeferredTask } = ChromeUtils.importESModule(
     12  "resource://gre/modules/DeferredTask.sys.mjs"
     13 );
     14 
     15 const toolsNameMap = {
     16  viewGenaiChatSidebar: "aichat",
     17  viewGenaiPageAssistSidebar: "aipageassist",
     18  viewGenaiSmartAssistSidebar: "aismartassist",
     19  viewTabsSidebar: "syncedtabs",
     20  viewHistorySidebar: "history",
     21  viewBookmarksSidebar: "bookmarks",
     22  viewCPMSidebar: "passwords",
     23 };
     24 const EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS = 1000;
     25 const LAUNCHER_SPLITTER_WIDTH = 4;
     26 
     27 var SidebarController = {
     28  makeSidebar({ elementId, ...rest }, commandID) {
     29    const sidebar = {
     30      get sourceL10nEl() {
     31        return document.getElementById(elementId);
     32      },
     33      get title() {
     34        let element = document.getElementById(elementId);
     35        return element?.getAttribute("label");
     36      },
     37      ...rest,
     38    };
     39 
     40    const toolID = toolsNameMap[commandID];
     41    if (toolID) {
     42      XPCOMUtils.defineLazyPreferenceGetter(
     43        sidebar,
     44        "attention",
     45        `sidebar.notification.badge.${toolID}`,
     46        false,
     47        (_pref, _prev) => this.handleToolBadges(toolID)
     48      );
     49      sidebar.attention;
     50    }
     51 
     52    return sidebar;
     53  },
     54 
     55  registerPrefSidebar(pref, commandID, config) {
     56    const sidebar = this.makeSidebar(config, commandID);
     57    this._sidebars.set(commandID, sidebar);
     58 
     59    let switcherMenuitem;
     60    const updateMenus = visible => {
     61      // Hide the sidebar if it is open and should not be visible,
     62      // and unset the current command and lastOpenedId so they do not
     63      // re-open the next time the sidebar does.
     64      if (!visible && this._state.command == commandID) {
     65        this._state.command = "";
     66        this.lastOpenedId = null;
     67        this.hide();
     68      }
     69 
     70      // Update visibility of View -> Sidebar menu item.
     71      const viewItem = document.getElementById(sidebar.menuId);
     72      if (viewItem) {
     73        viewItem.hidden = !visible;
     74      }
     75 
     76      let menuItem = document.getElementById(config.elementId);
     77      // Add/remove switcher menu item.
     78      if (visible && !menuItem) {
     79        switcherMenuitem = this.createMenuItem(commandID, sidebar);
     80        switcherMenuitem.setAttribute("id", config.elementId);
     81        switcherMenuitem.removeAttribute("type");
     82        const separator = this._switcherPanel.querySelector("menuseparator");
     83        separator.parentNode.insertBefore(switcherMenuitem, separator);
     84      } else {
     85        switcherMenuitem?.remove();
     86      }
     87 
     88      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
     89    };
     90 
     91    // Detect pref changes and handle initial state.
     92    XPCOMUtils.defineLazyPreferenceGetter(
     93      sidebar,
     94      "visible",
     95      pref,
     96      false,
     97      (_pref, _prev, val) => updateMenus(val)
     98    );
     99    this.promiseInitialized.then(() => updateMenus(sidebar.visible));
    100  },
    101 
    102  isAIWindow() {
    103    return this.AIWindow.isAIWindowActive(window);
    104  },
    105 
    106  get sidebars() {
    107    if (this._sidebars) {
    108      return this._sidebars;
    109    }
    110 
    111    return this.generateSidebarsMap();
    112  },
    113 
    114  generateSidebarsMap() {
    115    this._sidebars = new Map([
    116      [
    117        "viewHistorySidebar",
    118        this.makeSidebar({
    119          name: "history",
    120          elementId: "sidebar-switcher-history",
    121          // sidebar-history.html requires the "firefoxview" component and
    122          // requires more work. Stick to historySidebar.xhtml for ESR 140.
    123          // See tor-browser#44108.
    124          url: "chrome://browser/content/places/historySidebar.xhtml",
    125          menuId: "menu_historySidebar",
    126          triggerButtonId: "appMenuViewHistorySidebar",
    127          keyId: "key_gotoHistory",
    128          menuL10nId: "menu-view-history-button",
    129          revampL10nId: "sidebar-menu-history-label",
    130          iconUrl: "chrome://browser/skin/history.svg",
    131          contextMenuId: this.sidebarRevampEnabled
    132            ? "sidebar-history-context-menu"
    133            : undefined,
    134          gleanEvent: Glean.history.sidebarToggle,
    135          gleanClickEvent: Glean.sidebar.historyIconClick,
    136          recordSidebarVersion: true,
    137          // In permanent private browsing, the history panel can be opened, but
    138          // we hide the sidebar button to control this. tor-browser#43902.
    139          visible: !PrivateBrowsingUtils.permanentPrivateBrowsing,
    140        }),
    141      ],
    142      [
    143        "viewTabsSidebar",
    144        this.makeSidebar({
    145          name: "syncedtabs",
    146          elementId: "sidebar-switcher-tabs",
    147          url: this.sidebarRevampEnabled
    148            ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html"
    149            : "chrome://browser/content/syncedtabs/sidebar.xhtml",
    150          menuId: "menu_tabsSidebar",
    151          classAttribute: "sync-ui-item",
    152          menuL10nId: "menu-view-synced-tabs-sidebar",
    153          revampL10nId: "sidebar-menu-synced-tabs-label",
    154          iconUrl: "chrome://browser/skin/synced-tabs.svg",
    155          contextMenuId: this.sidebarRevampEnabled
    156            ? "sidebar-synced-tabs-context-menu"
    157            : undefined,
    158          gleanClickEvent: Glean.sidebar.syncedTabsIconClick,
    159          // firefoxview is disabled. tor-browser#42037 and tor-browser#43902.
    160          // See bugzilla bug 1983505.
    161          // NOTE: The menuId and elementId menu items (sidebar switchers)
    162          // should be hidden via the `sync-ui-item` class, which will *one*
    163          // time hide the menu items via gSync.init.
    164          // #sidebar-switcher-tabs is already in the initial browser DOM,
    165          // and #menu_tabsSidebar is created during SidebarController.init,
    166          // which seems to run prior to gSync.init.
    167          visible: false,
    168        }),
    169      ],
    170      [
    171        "viewBookmarksSidebar",
    172        this.makeSidebar({
    173          name: "bookmarks",
    174          elementId: "sidebar-switcher-bookmarks",
    175          url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
    176          menuId: "menu_bookmarksSidebar",
    177          keyId: "viewBookmarksSidebarKb",
    178          menuL10nId: "menu-view-bookmarks",
    179          revampL10nId: "sidebar-menu-bookmarks-label",
    180          iconUrl: "chrome://browser/skin/bookmark-hollow.svg",
    181          disabled: true,
    182          gleanEvent: Glean.bookmarks.sidebarToggle,
    183          gleanClickEvent: Glean.sidebar.bookmarksIconClick,
    184          recordSidebarVersion: true,
    185        }),
    186      ],
    187    ]);
    188 
    189    if (!this.isAIWindow()) {
    190      this.registerPrefSidebar(
    191        "browser.ml.chat.enabled",
    192        "viewGenaiChatSidebar",
    193        {
    194          name: "aichat",
    195          elementId: "sidebar-switcher-genai-chat",
    196          url: "chrome://browser/content/genai/chat.html",
    197          keyId: "viewGenaiChatSidebarKb",
    198          menuId: "menu_genaiChatSidebar",
    199          menuL10nId: "menu-view-genai-chat",
    200          // Bug 1900915 to expose as conditional tool
    201          revampL10nId: "sidebar-menu-genai-chat-label",
    202          iconUrl: "chrome://global/skin/icons/highlights.svg",
    203          gleanClickEvent: Glean.sidebar.chatbotIconClick,
    204          toolContextMenuId: "aichat",
    205        }
    206      );
    207    }
    208 
    209    this.registerPrefSidebar(
    210      "browser.ml.pageAssist.enabled",
    211      "viewGenaiPageAssistSidebar",
    212      {
    213        name: "aipageassist",
    214        elementId: "sidebar-switcher-genai-page-assist",
    215        url: "chrome://browser/content/genai/pageAssist.html",
    216        menuId: "menu_genaiPageAssistSidebar",
    217        menuL10nId: "menu-view-genai-page-assist",
    218        revampL10nId: "sidebar-menu-genai-page-assist-label",
    219        iconUrl: "chrome://browser/skin/reader-mode.svg",
    220      }
    221    );
    222 
    223    this.registerPrefSidebar(
    224      "browser.ml.smartAssist.enabled",
    225      "viewGenaiSmartAssistSidebar",
    226      {
    227        name: "aismartassist",
    228        elementId: "sidebar-switcher-genai-smart-assist",
    229        url: "chrome://browser/content/genai/smartAssist.html",
    230        menuId: "menu_genaiSmartAssistSidebar",
    231        menuL10nId: "menu-view-genai-smart-assist",
    232        revampL10nId: "sidebar-menu-genai-smart-assist-label",
    233        iconUrl: "chrome://browser/skin/trending.svg",
    234      }
    235    );
    236 
    237    this.registerPrefSidebar(
    238      "browser.contextual-password-manager.enabled",
    239      "viewCPMSidebar",
    240      {
    241        name: "passwords",
    242        elementId: "sidebar-switcher-megalist",
    243        url: "chrome://global/content/megalist/megalist.html",
    244        menuId: "menu_megalistSidebar",
    245        menuL10nId: "menu-view-contextual-password-manager",
    246        revampL10nId: "sidebar-menu-contextual-password-manager-label",
    247        iconUrl: "chrome://browser/skin/login.svg",
    248        gleanEvent: Glean.contextualManager.sidebarToggle,
    249      }
    250    );
    251 
    252    if (this.sidebarRevampEnabled) {
    253      this._sidebars.set("viewCustomizeSidebar", {
    254        url: "chrome://browser/content/sidebar/sidebar-customize.html",
    255        revampL10nId: "sidebar-menu-customize-label",
    256        iconUrl: "chrome://global/skin/icons/settings.svg",
    257        gleanEvent: Glean.sidebarCustomize.panelToggle,
    258        visible: false,
    259      });
    260    }
    261 
    262    return this._sidebars;
    263  },
    264 
    265  /**
    266   * Returns a map of tools and extensions for use in the sidebar
    267   */
    268  get toolsAndExtensions() {
    269    if (this._toolsAndExtensions) {
    270      return this._toolsAndExtensions;
    271    }
    272 
    273    this._toolsAndExtensions = new Map();
    274    this.getTools().forEach(tool => {
    275      this._toolsAndExtensions.set(tool.commandID, tool);
    276    });
    277    this.getExtensions().forEach(extension => {
    278      this._toolsAndExtensions.set(extension.commandID, extension);
    279    });
    280    return this._toolsAndExtensions;
    281  },
    282 
    283  // Avoid getting the browser element from init() to avoid triggering the
    284  // <browser> constructor during startup if the sidebar is hidden.
    285  get browser() {
    286    if (this._browser) {
    287      return this._browser;
    288    }
    289    return (this._browser = document.getElementById("sidebar"));
    290  },
    291  POSITION_START_PREF: "sidebar.position_start",
    292  DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
    293  VISIBILITY_PREF: "sidebar.visibility",
    294  TOOLS_PREF: "sidebar.main.tools",
    295  INSTALLED_EXTENSIONS: "sidebar.installed.extensions",
    296 
    297  // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
    298  // and isn't persisted across windows
    299  lastOpenedId: null,
    300 
    301  _box: null,
    302  _pinnedTabsContainer: null,
    303  _pinnedTabsItemsWrapper: null,
    304  // The constructor of this label accesses the browser element due to the
    305  // control="sidebar" attribute, so avoid getting this label during startup.
    306  get _title() {
    307    if (this.__title) {
    308      return this.__title;
    309    }
    310    return (this.__title = document.getElementById("sidebar-title"));
    311  },
    312  _splitter: null,
    313  _reversePositionButton: null,
    314  _switcherPanel: null,
    315  _switcherTarget: null,
    316  _switcherArrow: null,
    317  _inited: false,
    318  _uninitializing: false,
    319  _switcherListenersAdded: false,
    320  _verticalNewTabListenerAdded: false,
    321  _localesObserverAdded: false,
    322  _mainResizeObserverAdded: false,
    323  _mainResizeObserver: null,
    324  _ongoingAnimations: [],
    325 
    326  /**
    327   * @type {MutationObserver | null}
    328   */
    329  _observer: null,
    330 
    331  _initDeferred: Promise.withResolvers(),
    332 
    333  get promiseInitialized() {
    334    return this._initDeferred.promise;
    335  },
    336 
    337  get initialized() {
    338    return this._inited;
    339  },
    340 
    341  get uninitializing() {
    342    return this._uninitializing;
    343  },
    344 
    345  get inSingleTabWindow() {
    346    return (
    347      !window.toolbar.visible ||
    348      window.document.documentElement.hasAttribute("taskbartab")
    349    );
    350  },
    351 
    352  get sidebarContainer() {
    353    if (!this._sidebarContainer) {
    354      // This is the *parent* of the `sidebar-main` component.
    355      // TODO: Rename this element in the markup in order to avoid confusion. (Bug 1904860)
    356      this._sidebarContainer = document.getElementById("sidebar-main");
    357    }
    358    return this._sidebarContainer;
    359  },
    360 
    361  get sidebarMain() {
    362    if (!this._sidebarMain) {
    363      this._sidebarMain = document.querySelector("sidebar-main");
    364    }
    365    return this._sidebarMain;
    366  },
    367 
    368  get contentArea() {
    369    if (!this._contentArea) {
    370      this._contentArea = document.getElementById("tabbrowser-tabbox");
    371    }
    372    return this._contentArea;
    373  },
    374 
    375  get toolbarButton() {
    376    if (!this._toolbarButton) {
    377      this._toolbarButton = document.getElementById("sidebar-button");
    378    }
    379    return this._toolbarButton;
    380  },
    381 
    382  get isLauncherDragging() {
    383    return this._launcherSplitter.getAttribute("state") === "dragging";
    384  },
    385 
    386  get isPinnedTabsDragging() {
    387    return this._pinnedTabsSplitter.getAttribute("state") === "dragging";
    388  },
    389 
    390  get sidebarTools() {
    391    return this.sidebarRevampTools ? this.sidebarRevampTools.split(",") : [];
    392  },
    393 
    394  get sidebarExtensions() {
    395    return this.installedExtensions ? this.installedExtensions.split(",") : [];
    396  },
    397 
    398  init() {
    399    // Initialize global state manager.
    400    this.SidebarManager;
    401 
    402    // Initialize per-window state manager.
    403    if (!this._state) {
    404      this._state = new this.SidebarState(this);
    405    }
    406 
    407    this._pinnedTabsContainer = document.getElementById(
    408      "pinned-tabs-container"
    409    );
    410    this._pinnedTabsItemsWrapper =
    411      this._pinnedTabsContainer.shadowRoot.querySelector(
    412        "[part=items-wrapper]"
    413      );
    414    this._box = document.getElementById("sidebar-box");
    415    this._splitter = document.getElementById("sidebar-splitter");
    416    this._launcherSplitter = document.getElementById(
    417      "sidebar-launcher-splitter"
    418    );
    419    this._pinnedTabsSplitter = document.getElementById(
    420      "vertical-pinned-tabs-splitter"
    421    );
    422    this._reversePositionButton = document.getElementById(
    423      "sidebar-reverse-position"
    424    );
    425    this._switcherPanel = document.getElementById("sidebarMenu-popup");
    426    this._switcherTarget = document.getElementById("sidebar-switcher-target");
    427    this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
    428    this._hoverBlockerCount = 0;
    429    if (
    430      Services.prefs.getBoolPref(
    431        "browser.tabs.allow_transparent_browser",
    432        false
    433      )
    434    ) {
    435      this.browser.setAttribute("transparent", "true");
    436    }
    437 
    438    const menubar = document.getElementById("viewSidebarMenu");
    439    const currentMenuItems = new Set(
    440      Array.from(menubar.childNodes, item => item.id)
    441    );
    442    for (const [commandID, sidebar] of this.sidebars.entries()) {
    443      if (
    444        !Object.hasOwn(sidebar, "extensionId") &&
    445        commandID !== "viewCustomizeSidebar" &&
    446        !currentMenuItems.has(sidebar.menuId)
    447      ) {
    448        // registerExtension() already creates menu items for extensions.
    449        const menuitem = this.createMenuItem(commandID, sidebar);
    450        menubar.appendChild(menuitem);
    451      }
    452    }
    453    if (this._mainResizeObserver) {
    454      this._mainResizeObserver.disconnect();
    455      this._mainResizeObserverAdded = false;
    456    }
    457    this._mainResizeObserver = new ResizeObserver(([entry]) =>
    458      this._handleLauncherResize(entry)
    459    );
    460 
    461    if (this.sidebarRevampEnabled && !BrowserHandler.kiosk) {
    462      if (!customElements.get("sidebar-main")) {
    463        ChromeUtils.importESModule(
    464          "chrome://browser/content/sidebar/sidebar-main.mjs",
    465          { global: "current" }
    466        );
    467      }
    468      this.revampComponentsLoaded = true;
    469      this._state.initializeState(this._showLauncherAfterInit);
    470      // clear the flag after we've used it
    471      delete this._showLauncherAfterInit;
    472 
    473      document.getElementById("sidebar-header").hidden = true;
    474      if (!this._mainResizeObserverAdded) {
    475        this._mainResizeObserver.observe(this.sidebarMain);
    476        this._mainResizeObserverAdded = true;
    477      }
    478      if (!this._browserResizeObserver) {
    479        this._browserResizeObserver = () => {
    480          // Report resize events to Glean.
    481          const current = this.browser.getBoundingClientRect().width;
    482          const previous = this._browserWidth;
    483          const percentage = (current / window.innerWidth) * 100;
    484          Glean.sidebar.resize.record({
    485            current: Math.round(current),
    486            previous: Math.round(previous),
    487            percentage: Math.round(percentage),
    488          });
    489          this._recordBrowserSize();
    490        };
    491        this._splitter.addEventListener("command", this._browserResizeObserver);
    492      }
    493      this._enableLauncherDragging();
    494      this._enablePinnedTabsSplitterDragging();
    495 
    496      // Record Glean metrics.
    497      this.recordVisibilitySetting();
    498      this.recordPositionSetting();
    499      this.recordTabsLayoutSetting();
    500    } else {
    501      this._switcherCloseButton = document.getElementById("sidebar-close");
    502      if (!this._switcherListenersAdded) {
    503        this._switcherCloseButton.addEventListener("command", () => {
    504          this.hide();
    505        });
    506        this._switcherTarget.addEventListener("command", () => {
    507          this.toggleSwitcherPanel();
    508        });
    509        this._switcherTarget.addEventListener("keydown", event => {
    510          this.handleKeydown(event);
    511        });
    512        this._switcherListenersAdded = true;
    513      }
    514      this._disableLauncherDragging();
    515      this._disablePinnedTabsDragging();
    516    }
    517    // We need to update the tab strip for vertical tabs during init
    518    // as there will be no tabstrip-orientation-change event
    519    if (CustomizableUI.verticalTabsEnabled) {
    520      this.toggleTabstrip();
    521    }
    522 
    523    // sets the sidebar to the left or right, based on a pref
    524    this.setPosition();
    525 
    526    this._inited = true;
    527 
    528    if (!this._localesObserverAdded) {
    529      Services.obs.addObserver(this, "intl:app-locales-changed");
    530      this._localesObserverAdded = true;
    531    }
    532    if (!this._tabstripOrientationObserverAdded) {
    533      Services.obs.addObserver(this, "tabstrip-orientation-change");
    534      this._tabstripOrientationObserverAdded = true;
    535    }
    536 
    537    requestIdleCallback(() => {
    538      const windowPrivacyMatches =
    539        !window.opener || this.windowPrivacyMatches(window.opener, window);
    540      // If other sources (like session store or source window) haven't set the
    541      // UI state at this point, load the backup state. (Do not load the backup
    542      // state if this is a popup, or we are coming from a window of a different
    543      // privacy level.)
    544      if (
    545        !this.uiStateInitialized &&
    546        !this.inSingleTabWindow &&
    547        (this.sidebarRevampEnabled || windowPrivacyMatches)
    548      ) {
    549        const backupState = this.SidebarManager.getBackupState();
    550        this.initializeUIState(backupState);
    551      }
    552    });
    553    this._initDeferred.resolve();
    554  },
    555 
    556  uninit() {
    557    // Set a flag to allow us to ignore pref changes while the host document is being unloaded.
    558    this._uninitializing = true;
    559 
    560    // If this is the last browser window, persist various values that should be
    561    // remembered for after a restart / reopening a browser window.
    562    let enumerator = Services.wm.getEnumerator("navigator:browser");
    563    if (!enumerator.hasMoreElements()) {
    564      let xulStore = Services.xulStore;
    565      xulStore.persist(this._title, "value");
    566 
    567      const currentState = this.getUIState();
    568      this.SidebarManager.setBackupState(currentState);
    569    }
    570 
    571    Services.obs.removeObserver(this, "intl:app-locales-changed");
    572    Services.obs.removeObserver(this, "tabstrip-orientation-change");
    573    delete this._tabstripOrientationObserverAdded;
    574 
    575    CustomizableUI.removeListener(this);
    576 
    577    if (this._observer) {
    578      this._observer.disconnect();
    579      this._observer = null;
    580    }
    581 
    582    if (this._mainResizeObserver) {
    583      this._mainResizeObserver.disconnect();
    584      this._mainResizeObserver = null;
    585    }
    586 
    587    if (this.revampComponentsLoaded) {
    588      // Explicitly disconnect the `sidebar-main` element so that listeners
    589      // setup by reactive controllers will also be removed.
    590      this.sidebarMain.remove();
    591    }
    592    this._splitter.removeEventListener("command", this._browserResizeObserver);
    593    this._disableLauncherDragging();
    594    this._disablePinnedTabsDragging();
    595  },
    596 
    597  /**
    598   * Keep track when sidebar.revamp is enabled by the user via about:preferences UI
    599   *
    600   * @param {boolean} isEnabled
    601   */
    602  enabledViaSettings(isEnabled = false) {
    603    this._showLauncherAfterInit = isEnabled;
    604  },
    605 
    606  /**
    607   * Handle the launcher being resized (either manually or programmatically).
    608   *
    609   * @param {ResizeObserverEntry} entry
    610   */
    611  _handleLauncherResize(entry) {
    612    this._state.launcherWidth = entry.contentBoxSize[0].inlineSize;
    613    if (this.isLauncherDragging) {
    614      this._state.launcherDragActive = true;
    615    }
    616    if (this._state.visibilitySetting === "expand-on-hover") {
    617      this.setLauncherCollapsedWidth();
    618    }
    619  },
    620 
    621  getUIState() {
    622    if (this.inSingleTabWindow) {
    623      return null;
    624    }
    625    return this._state.getProperties();
    626  },
    627 
    628  /**
    629   * Load the UI state information given by session store, backup state, or
    630   * adopted window.
    631   *
    632   * @param {SidebarStateProps} state
    633   */
    634  async initializeUIState(state) {
    635    if (!state) {
    636      return;
    637    }
    638    const isValidSidebar = !state.command || this.sidebars.has(state.command);
    639    if (!isValidSidebar) {
    640      state.command = "";
    641    }
    642 
    643    const hasOpenPanel =
    644      state.panelOpen &&
    645      state.command &&
    646      this.sidebars.has(state.command) &&
    647      this.currentID !== state.command;
    648    if (hasOpenPanel) {
    649      // There's a panel to show, so ignore the contradictory hidden property.
    650      delete state.hidden;
    651    }
    652    await this.promiseInitialized;
    653    await this.waitUntilStable(); // Finish currently scheduled tasks.
    654    await this._state.loadInitialState(state);
    655    await this.waitUntilStable(); // Finish newly scheduled tasks.
    656    this.updateToolbarButton();
    657    if (this.sidebarRevampVisibility === "expand-on-hover") {
    658      await this.toggleExpandOnHover(true);
    659    }
    660    this.uiStateInitialized = true;
    661  },
    662 
    663  /**
    664   * Toggle the vertical tabs preference.
    665   */
    666  toggleVerticalTabs() {
    667    Services.prefs.setBoolPref(
    668      "sidebar.verticalTabs",
    669      !this.sidebarVerticalTabsEnabled
    670    );
    671  },
    672 
    673  /**
    674   * The handler for Services.obs.addObserver.
    675   */
    676  observe(_subject, topic, _data) {
    677    switch (topic) {
    678      case "intl:app-locales-changed": {
    679        if (this.isOpen) {
    680          // The <tree> component used in history and bookmarks, but it does not
    681          // support live switching the app locale. Reload the entire sidebar to
    682          // invalidate any old text.
    683          this.hide({ dismissPanel: false });
    684          this.showInitially(this.lastOpenedId);
    685          break;
    686        }
    687        if (this.revampComponentsLoaded) {
    688          this.sidebarMain.requestUpdate();
    689        }
    690        break;
    691      }
    692      case "tabstrip-orientation-change": {
    693        this.promiseInitialized.then(() => this.toggleTabstrip());
    694        break;
    695      }
    696    }
    697  },
    698 
    699  /**
    700   * Ensure the title stays in sync with the source element, which updates for
    701   * l10n changes.
    702   *
    703   * @param {HTMLElement} [element]
    704   */
    705  observeTitleChanges(element) {
    706    if (!element) {
    707      return;
    708    }
    709    let observer = this._observer;
    710    if (!observer) {
    711      observer = new MutationObserver(() => {
    712        // it's possible for lastOpenedId to be null here
    713        this.title = this.sidebars.get(this.lastOpenedId)?.title;
    714      });
    715      // Re-use the observer.
    716      this._observer = observer;
    717    }
    718    observer.disconnect();
    719    observer.observe(element, {
    720      attributes: true,
    721      attributeFilter: ["label"],
    722    });
    723  },
    724 
    725  /**
    726   * Opens the switcher panel if it's closed, or closes it if it's open.
    727   */
    728  toggleSwitcherPanel() {
    729    if (
    730      this._switcherPanel.state == "open" ||
    731      this._switcherPanel.state == "showing"
    732    ) {
    733      this.hideSwitcherPanel();
    734    } else if (this._switcherPanel.state == "closed") {
    735      this.showSwitcherPanel();
    736    }
    737  },
    738 
    739  /**
    740   * Handles keydown on the the switcherTarget button
    741   *
    742   * @param  {Event} event
    743   */
    744  handleKeydown(event) {
    745    switch (event.key) {
    746      case "Enter":
    747      case " ": {
    748        this.toggleSwitcherPanel();
    749        event.stopPropagation();
    750        event.preventDefault();
    751        break;
    752      }
    753      case "Escape": {
    754        this.hideSwitcherPanel();
    755        event.stopPropagation();
    756        event.preventDefault();
    757        break;
    758      }
    759    }
    760  },
    761 
    762  hideSwitcherPanel() {
    763    this._switcherPanel.hidePopup();
    764  },
    765 
    766  showSwitcherPanel() {
    767    this._switcherPanel.addEventListener(
    768      "popuphiding",
    769      () => {
    770        this._switcherTarget.classList.remove("active");
    771        this._switcherTarget.setAttribute("aria-expanded", false);
    772      },
    773      { once: true }
    774    );
    775 
    776    // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
    777    let label =
    778      this._positionStart == RTL_UI
    779        ? gNavigatorBundle.getString("sidebar.moveToLeft")
    780        : gNavigatorBundle.getString("sidebar.moveToRight");
    781    this._reversePositionButton.setAttribute("label", label);
    782 
    783    // Open the sidebar switcher popup, anchored off the switcher toggle
    784    this._switcherPanel.hidden = false;
    785    this._switcherPanel.openPopup(this._switcherTarget);
    786 
    787    this._switcherTarget.classList.add("active");
    788    this._switcherTarget.setAttribute("aria-expanded", true);
    789  },
    790 
    791  updateShortcut({ keyId }) {
    792    let menuitem = this._switcherPanel?.querySelector(`[key="${keyId}"]`);
    793    if (!menuitem) {
    794      // If the menu item doesn't exist yet then the accel text will be set correctly
    795      // upon creation so there's nothing to do now.
    796      return;
    797    }
    798    menuitem.removeAttribute("acceltext");
    799  },
    800 
    801  /**
    802   * Change the pref that will trigger a call to setPosition
    803   */
    804  reversePosition() {
    805    Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
    806  },
    807 
    808  /**
    809   * Read the positioning pref and position the sidebar and the splitter
    810   * appropriately within the browser container.
    811   */
    812  setPosition() {
    813    // First reset all ordinals to match DOM ordering.
    814    let contentArea = document.getElementById("tabbrowser-tabbox");
    815    let browser = document.getElementById("browser");
    816    [...browser.children].forEach((node, i, children) => {
    817      node.style.order = this._positionStart ? i + 1 : children.length - i;
    818    });
    819    let sidebarContainer = document.getElementById("sidebar-main");
    820    let sidebarMain = document.querySelector("sidebar-main");
    821 
    822    // Indicate we've switched ordering to the box
    823    this._box.toggleAttribute("sidebar-positionend", !this._positionStart);
    824    sidebarMain.toggleAttribute("sidebar-positionend", !this._positionStart);
    825    contentArea.toggleAttribute("sidebar-positionend", !this._positionStart);
    826    sidebarContainer.toggleAttribute(
    827      "sidebar-positionend",
    828      !this._positionStart
    829    );
    830    this.toolbarButton &&
    831      this.toolbarButton.toggleAttribute(
    832        "sidebar-positionend",
    833        !this._positionStart
    834      );
    835 
    836    this.hideSwitcherPanel();
    837 
    838    let content = SidebarController.browser.contentWindow;
    839    if (content && content.updatePosition) {
    840      content.updatePosition();
    841    }
    842  },
    843 
    844  /**
    845   * Show/hide new sidebar based on sidebar.revamp pref
    846   */
    847  async toggleRevampSidebar() {
    848    await this.promiseInitialized;
    849    let wasOpen = this.isOpen;
    850    if (wasOpen) {
    851      this.hide({ dismissPanel: false });
    852    }
    853    // Reset sidebars map but preserve any existing extensions
    854    let extensionsArr = [];
    855    for (const [commandID, sidebar] of this.sidebars.entries()) {
    856      if (sidebar.hasOwnProperty("extensionId")) {
    857        extensionsArr.push({ commandID, sidebar });
    858      }
    859    }
    860    this.sidebars = this.generateSidebarsMap();
    861    for (const extension of extensionsArr) {
    862      this.sidebars.set(extension.commandID, extension.sidebar);
    863    }
    864    if (!this.sidebarRevampEnabled) {
    865      this._state.launcherVisible = false;
    866      document.getElementById("sidebar-header").hidden = false;
    867 
    868      // Ensure CPM isn't shown.
    869      const cpmMenuItem = document.querySelector("#sidebar-switcher-megalist");
    870      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
    871      cpmMenuItem.hidden = true;
    872    }
    873    if (!this._sidebars.get(this.lastOpenedId)) {
    874      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
    875      wasOpen = false;
    876    }
    877    this.updateToolbarButton();
    878    this._inited = false;
    879    this.init();
    880 
    881    // Reopen the panel in the new or old sidebar now that we've inited
    882    if (wasOpen) {
    883      this.toggle();
    884    }
    885  },
    886 
    887  /**
    888   * Try and adopt the status of the sidebar from another window.
    889   *
    890   * @param {Window} sourceWindow - Window to use as a source for sidebar status.
    891   * @returns {boolean} true if we adopted the state, or false if the caller should
    892   * initialize the state itself.
    893   */
    894  async adoptFromWindow(sourceWindow) {
    895    // If the opener had a sidebar, open the same sidebar in our window.
    896    // The opener can be the hidden window too, if we're coming from the state
    897    // where no windows are open, and the hidden window has no sidebar box.
    898    let sourceController = sourceWindow.SidebarController;
    899    if (!sourceController || !sourceController._box) {
    900      // no source UI or no _box means we also can't adopt the state.
    901      return false;
    902    }
    903 
    904    // If window is a popup, hide the sidebar
    905    if (this.inSingleTabWindow && this.sidebarRevampEnabled) {
    906      document.getElementById("sidebar-main").hidden = true;
    907      return false;
    908    }
    909    // Adopt the other window's UI state (it too could be a popup)
    910    // We get the properties directly forom the SidebarState instance as in this case
    911    // we need the command property even if no panel is currently open.
    912    const sourceState = sourceController.inPopup
    913      ? null
    914      : sourceController._state?.getProperties();
    915    await this.initializeUIState(sourceState);
    916 
    917    return true;
    918  },
    919 
    920  windowPrivacyMatches(w1, w2) {
    921    return (
    922      PrivateBrowsingUtils.isWindowPrivate(w1) ===
    923      PrivateBrowsingUtils.isWindowPrivate(w2)
    924    );
    925  },
    926 
    927  /**
    928   * If loading a sidebar was delayed on startup, start the load now.
    929   */
    930  async startDelayedLoad() {
    931    if (this.inSingleTabWindow) {
    932      this._state.launcherVisible = false;
    933      return;
    934    }
    935 
    936    let sourceWindow = window.opener;
    937    // No source window means this is the initial window.  If we're being
    938    // opened from another window, check that it is one we might open a sidebar
    939    // for.
    940    if (sourceWindow) {
    941      if (
    942        sourceWindow.closed ||
    943        sourceWindow.location.protocol != "chrome:" ||
    944        (!this.sidebarRevampEnabled &&
    945          !this.windowPrivacyMatches(sourceWindow, window))
    946      ) {
    947        return;
    948      }
    949      // Try to adopt the sidebar state from the source window
    950      if (await this.adoptFromWindow(sourceWindow)) {
    951        this.uiStateInitialized = true;
    952        return;
    953      }
    954    }
    955 
    956    // If we're not adopting settings from a parent window, set them now.
    957    let wasOpen = this._box.getAttribute("checked");
    958    if (!wasOpen) {
    959      return;
    960    }
    961 
    962    let commandID = this._state.command;
    963    if (commandID && this.sidebars.has(commandID)) {
    964      this.showInitially(commandID);
    965    } else {
    966      this._box.removeAttribute("checked");
    967      // Update the state, because the element it
    968      // refers to no longer exists, so we should assume this sidebar
    969      // panel has been uninstalled. (249883)
    970      this._state.command = "";
    971      // On a startup in which the startup cache was invalidated (e.g. app update)
    972      // extensions will not be started prior to delayedLoad, thus the
    973      // sidebarcommand element will not exist yet.  Store the commandID so
    974      // extensions may reopen if necessary.  A startup cache invalidation
    975      // can be forced (for testing) by deleting compatibility.ini from the
    976      // profile.
    977      this.lastOpenedId = commandID;
    978    }
    979    this.uiStateInitialized = true;
    980  },
    981 
    982  /**
    983   * Fire a "SidebarShown" event on the sidebar to give any interested parties
    984   * a chance to update the button or whatever.
    985   */
    986  _fireShowEvent() {
    987    let event = new CustomEvent("SidebarShown", { bubbles: true });
    988    this._switcherTarget.dispatchEvent(event);
    989  },
    990 
    991  /**
    992   * Report the current browser width to Glean, and store it internally.
    993   */
    994  _recordBrowserSize() {
    995    this._browserWidth = this.browser.getBoundingClientRect().width;
    996    Glean.sidebar.width.set(this._browserWidth);
    997  },
    998 
    999  /**
   1000   * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
   1001   * a chance to adjust focus as needed. An additional event is needed, because
   1002   * we don't want to focus the sidebar when it's opened on startup or in a new
   1003   * window, only when the user opens the sidebar.
   1004   */
   1005  _fireFocusedEvent() {
   1006    let event = new CustomEvent("SidebarFocused", { bubbles: true });
   1007    this.browser.contentWindow.dispatchEvent(event);
   1008  },
   1009 
   1010  /**
   1011   * True if the sidebar is currently open.
   1012   */
   1013  get isOpen() {
   1014    return this._box ? !this._box.hidden : false;
   1015  },
   1016 
   1017  /**
   1018   * The ID of the current sidebar.
   1019   */
   1020  get currentID() {
   1021    return this.isOpen ? this._state.command : "";
   1022  },
   1023 
   1024  /**
   1025   * The context menu of the current sidebar.
   1026   */
   1027  get currentContextMenu() {
   1028    const sidebar = this.sidebars.get(this.currentID);
   1029    if (!sidebar) {
   1030      return null;
   1031    }
   1032    return document.getElementById(sidebar.contextMenuId);
   1033  },
   1034 
   1035  get launcherVisible() {
   1036    return this._state?.launcherVisible;
   1037  },
   1038 
   1039  get launcherEverVisible() {
   1040    return this._state?.launcherEverVisible;
   1041  },
   1042 
   1043  get title() {
   1044    return this._title.value;
   1045  },
   1046 
   1047  set title(value) {
   1048    this._title.value = value;
   1049  },
   1050 
   1051  /**
   1052   * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
   1053   * with a different commandID, then the sidebar will be opened using the
   1054   * specified commandID. Otherwise the sidebar will be hidden.
   1055   *
   1056   * @param  {string}  commandID     ID of the sidebar.
   1057   * @param  {DOMNode} [triggerNode] Node, usually a button, that triggered the
   1058   *                                 visibility toggling of the sidebar.
   1059   * @returns {Promise}
   1060   */
   1061  toggle(commandID = this.lastOpenedId, triggerNode) {
   1062    if (
   1063      CustomizationHandler.isCustomizing() ||
   1064      CustomizationHandler.isExitingCustomizeMode
   1065    ) {
   1066      return Promise.resolve();
   1067    }
   1068    // First priority for a default value is this.lastOpenedId which is set during show()
   1069    // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
   1070    // have a persisted command either, or the command doesn't exist anymore, then
   1071    // fallback to a default sidebar.
   1072    if (!commandID) {
   1073      commandID = this._state.command;
   1074    }
   1075    if (!commandID || !this.sidebars.has(commandID)) {
   1076      if (this.sidebarRevampEnabled && this.sidebars.size) {
   1077        commandID = this.sidebars.keys().next().value;
   1078      } else {
   1079        commandID = this.DEFAULT_SIDEBAR_ID;
   1080      }
   1081    }
   1082 
   1083    if (this.isOpen && commandID == this.currentID) {
   1084      // Revamp sidebar: this case is a dismissal of the current sidebar panel. The launcher should stay open
   1085      // For legacy sidebar, this is a "sidebar" toggle and the current panel should be remembered
   1086      this.hide({ triggerNode, dismissPanel: this.sidebarRevampEnabled });
   1087      this.updateToolbarButton();
   1088      return Promise.resolve();
   1089    }
   1090 
   1091    if (!this.sidebarRevampEnabled) {
   1092      const cpmMenuItem = document.querySelector("#sidebar-switcher-megalist");
   1093      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
   1094      cpmMenuItem.hidden = true;
   1095    }
   1096 
   1097    return this.show(commandID, triggerNode);
   1098  },
   1099 
   1100  _getRects(animatingElements) {
   1101    return animatingElements.map(e => [
   1102      e.hidden,
   1103      e.getBoundingClientRect().toJSON(),
   1104    ]);
   1105  },
   1106 
   1107  /**
   1108   * Wait for Lit updates and ongoing animations to complete.
   1109   *
   1110   * @returns {Promise}
   1111   */
   1112  async waitUntilStable() {
   1113    if (!this.sidebarRevampEnabled) {
   1114      // Legacy sidebar doesn't have animations, nothing to await.
   1115      return null;
   1116    }
   1117    const tasks = [this.sidebarMain.updateComplete];
   1118    if (this._ongoingAnimations?.length) {
   1119      tasks.push(
   1120        ...this._ongoingAnimations.map(animation => animation.finished)
   1121      );
   1122    }
   1123    return Promise.allSettled(tasks);
   1124  },
   1125 
   1126  async _animateSidebarMain() {
   1127    let tabbox = document.getElementById("tabbrowser-tabbox");
   1128    let animatingElements;
   1129    let expandOnHoverEnabled = document.documentElement.hasAttribute(
   1130      "sidebar-expand-on-hover"
   1131    );
   1132    if (expandOnHoverEnabled) {
   1133      animatingElements = [this.sidebarContainer];
   1134 
   1135      this._addHoverStateBlocker();
   1136    } else {
   1137      animatingElements = [
   1138        this.sidebarContainer,
   1139        this._box,
   1140        this._splitter,
   1141        tabbox,
   1142      ];
   1143    }
   1144    let resetElements = () => {
   1145      for (let el of animatingElements) {
   1146        el.style.minWidth =
   1147          el.style.maxWidth =
   1148          el.style.marginLeft =
   1149          el.style.marginRight =
   1150          el.style.display =
   1151            "";
   1152      }
   1153      this.sidebarContainer.toggleAttribute(
   1154        "sidebar-ongoing-animations",
   1155        false
   1156      );
   1157      this._box.toggleAttribute("sidebar-ongoing-animations", false);
   1158      tabbox.toggleAttribute("sidebar-ongoing-animations", false);
   1159    };
   1160    if (this._ongoingAnimations.length) {
   1161      this._ongoingAnimations.forEach(a => a.cancel());
   1162      this._ongoingAnimations = [];
   1163      resetElements();
   1164    }
   1165 
   1166    let fromRects = this._getRects(animatingElements);
   1167 
   1168    // We need to wait for lit to re-render, and us to get the final width.
   1169    // This is a bit unfortunate but alas...
   1170    await new Promise(resolve => {
   1171      queueMicrotask(() => resolve(this.sidebarMain.updateComplete));
   1172    });
   1173    let toRects = this._getRects(animatingElements);
   1174 
   1175    const options = {
   1176      duration: document.documentElement.hasAttribute("sidebar-expand-on-hover")
   1177        ? this._animationExpandOnHoverDurationMs
   1178        : this._animationDurationMs,
   1179      easing: "ease-in-out",
   1180    };
   1181    let animations = [];
   1182    let sidebarOnLeft = this._positionStart != RTL_UI;
   1183    let sidebarShift = 0;
   1184    for (let i = 0; i < animatingElements.length; ++i) {
   1185      const el = animatingElements[i];
   1186      const [wasHidden, from] = fromRects[i];
   1187      const [isHidden, to] = toRects[i];
   1188 
   1189      // For the sidebar, we need some special cases to make the animation
   1190      // nicer (keeping the icon positions).
   1191      const isSidebar = el === this.sidebarContainer;
   1192 
   1193      if (wasHidden != isHidden) {
   1194        if (wasHidden) {
   1195          from.left = from.right = sidebarOnLeft ? to.left : to.right;
   1196        } else {
   1197          to.left = to.right = sidebarOnLeft ? from.left : from.right;
   1198        }
   1199      }
   1200      const widthGrowth = to.width - from.width;
   1201      if (isSidebar) {
   1202        sidebarShift = widthGrowth;
   1203      }
   1204 
   1205      let fromTranslate = sidebarOnLeft
   1206        ? from.left - to.left
   1207        : from.right - to.right;
   1208      let toTranslate = 0;
   1209 
   1210      // We fix the element to the larger width during the animation if needed,
   1211      // but keeping the right flex width, and thus our original position, with
   1212      // a negative margin.
   1213      el.style.minWidth =
   1214        el.style.maxWidth =
   1215        el.style.marginLeft =
   1216        el.style.marginRight =
   1217        el.style.display =
   1218          "";
   1219      if (isHidden && !wasHidden) {
   1220        el.style.display = "flex";
   1221      }
   1222 
   1223      if (widthGrowth < 0) {
   1224        el.style.minWidth = el.style.maxWidth = from.width + "px";
   1225        el.style["margin-" + (sidebarOnLeft ? "right" : "left")] =
   1226          widthGrowth + "px";
   1227        if (isSidebar) {
   1228          toTranslate = sidebarOnLeft ? widthGrowth : -widthGrowth;
   1229        } else if (el === this._box) {
   1230          // This is very hacky, but this code doesn't deal well with
   1231          // more than two elements moving, and this is the less invasive change.
   1232          // It would be better to treat "sidebar + sidebar-box" as a unit.
   1233          // We only hit this when completely hiding the box.
   1234          fromTranslate = sidebarOnLeft ? -sidebarShift : sidebarShift;
   1235          toTranslate = sidebarOnLeft
   1236            ? fromTranslate + widthGrowth
   1237            : fromTranslate - widthGrowth;
   1238        }
   1239      } else if (isSidebar) {
   1240        fromTranslate += sidebarOnLeft ? -widthGrowth : widthGrowth;
   1241      }
   1242 
   1243      animations.push(
   1244        el.animate(
   1245          [
   1246            { translate: `${fromTranslate}px 0 0` },
   1247            { translate: `${toTranslate}px 0 0` },
   1248          ],
   1249          options
   1250        )
   1251      );
   1252      if (!isSidebar || !this._positionStart) {
   1253        continue;
   1254      }
   1255      // We want to keep the buttons in place during the animation, for which
   1256      // we might need to compensate.
   1257      if (!this._state.launcherExpanded) {
   1258        animations.push(
   1259          this.sidebarMain.animate(
   1260            [{ translate: "0" }, { translate: `${-toTranslate}px 0 0` }],
   1261            options
   1262          )
   1263        );
   1264      } else {
   1265        animations.push(
   1266          this.sidebarMain.animate(
   1267            [{ translate: `${-fromTranslate}px 0 0` }, { translate: "0" }],
   1268            options
   1269          )
   1270        );
   1271      }
   1272    }
   1273    this._ongoingAnimations = animations;
   1274    this.sidebarContainer.toggleAttribute("sidebar-ongoing-animations", true);
   1275    this.sidebarMain.toggleAttribute("sidebar-ongoing-animations", true);
   1276    this._box.toggleAttribute("sidebar-ongoing-animations", true);
   1277    tabbox.toggleAttribute("sidebar-ongoing-animations", true);
   1278    await Promise.allSettled(animations.map(a => a.finished));
   1279    if (this._ongoingAnimations === animations) {
   1280      this._ongoingAnimations = [];
   1281      resetElements();
   1282    }
   1283 
   1284    if (expandOnHoverEnabled) {
   1285      await this._removeHoverStateBlocker();
   1286    }
   1287  },
   1288 
   1289  /**
   1290   * For sidebar.revamp=true only, handle the keyboard or sidebar-button command to toggle the sidebar state
   1291   */
   1292  async handleToolbarButtonClick() {
   1293    if (this.inSingleTabWindow || this.uninitializing) {
   1294      return;
   1295    }
   1296 
   1297    const initialExpandedValue = this._state.launcherExpanded;
   1298 
   1299    // What toggle means depends on the sidebar.visibility pref.
   1300    const expandOnToggle = ["always-show", "expand-on-hover"].includes(
   1301      this.sidebarRevampVisibility
   1302    );
   1303 
   1304    // when the launcher is toggled open by the user, we disable expand-on-hover interactions.
   1305    if (this.sidebarRevampVisibility === "expand-on-hover") {
   1306      await this.toggleExpandOnHover(initialExpandedValue);
   1307    }
   1308 
   1309    if (this._animationEnabled && !window.gReduceMotion) {
   1310      this._animateSidebarMain();
   1311    }
   1312 
   1313    if (expandOnToggle) {
   1314      // just expand/collapse the launcher
   1315      this._state.updateVisibility(true, !initialExpandedValue);
   1316      this.updateToolbarButton();
   1317      return;
   1318    }
   1319 
   1320    const shouldShowLauncher = !this._state.launcherVisible;
   1321    // show/hide the launcher
   1322    this._state.updateVisibility(shouldShowLauncher);
   1323    // if we're showing and there was panel open, open it again
   1324    if (shouldShowLauncher && this._state.command) {
   1325      await this.show(this._state.command);
   1326    } else if (!shouldShowLauncher) {
   1327      // hide the open panel. It will re-open next time as we don't change the command value
   1328      this.hide({ dismissPanel: false });
   1329    }
   1330    this.updateToolbarButton();
   1331  },
   1332 
   1333  /**
   1334   * Update `checked` state and tooltip text of the toolbar button.
   1335   */
   1336  updateToolbarButton(toolbarButton = this.toolbarButton) {
   1337    if (!toolbarButton || this.inSingleTabWindow) {
   1338      return;
   1339    }
   1340    if (!this.sidebarRevampEnabled) {
   1341      toolbarButton.dataset.l10nId = "show-sidebars";
   1342      toolbarButton.checked = this.isOpen;
   1343    } else {
   1344      let sidebarToggleKey = document.getElementById("toggleSidebarKb");
   1345      const shortcut = ShortcutUtils.prettifyShortcut(sidebarToggleKey);
   1346      toolbarButton.dataset.l10nArgs = JSON.stringify({ shortcut });
   1347      // we need to use the pref rather than SidebarController's getter here
   1348      // as the getter might not have the new value yet
   1349      const isVerticalTabs = Services.prefs.getBoolPref("sidebar.verticalTabs");
   1350      if (isVerticalTabs) {
   1351        toolbarButton.toggleAttribute("expanded", this.sidebarMain.expanded);
   1352      } else {
   1353        toolbarButton.toggleAttribute("expanded", false);
   1354      }
   1355      this.handleToolBadges();
   1356      switch (this.sidebarRevampVisibility) {
   1357        case "always-show":
   1358        case "expand-on-hover":
   1359          // Toolbar button controls expanded state.
   1360          toolbarButton.checked = this.sidebarMain.expanded;
   1361          toolbarButton.dataset.l10nId = toolbarButton.checked
   1362            ? "sidebar-widget-collapse-sidebar2"
   1363            : "sidebar-widget-expand-sidebar2";
   1364          break;
   1365        case "hide-sidebar":
   1366          // Toolbar button controls hidden state.
   1367          toolbarButton.checked = !this.sidebarContainer.hidden;
   1368          toolbarButton.dataset.l10nId = toolbarButton.checked
   1369            ? "sidebar-widget-hide-sidebar2"
   1370            : "sidebar-widget-show-sidebar2";
   1371          break;
   1372      }
   1373    }
   1374  },
   1375 
   1376  /**
   1377   * Handles badges display for the toolbar and sidebar.
   1378   * Check if a tool(toolID) has requested a badge from pref (i.e) sidebar.notification.badge.{toolID})
   1379   * Ensure that badges are shown or cleared based on the sidebar visibility and user interaction.
   1380   *
   1381   * @param {string|null} toolID
   1382   */
   1383  handleToolBadges(toolID = null) {
   1384    const toolPrefList = this.SidebarManager.getBadgeTools();
   1385 
   1386    for (const pref of toolPrefList) {
   1387      if (toolID && toolID !== pref) {
   1388        continue;
   1389      }
   1390 
   1391      const badgePref = Services.prefs.getBoolPref(
   1392        `sidebar.notification.badge.${pref}`,
   1393        false
   1394      );
   1395      const commandID = [...this.toolsAndExtensions.keys()].find(
   1396        id => toolsNameMap[id] === pref
   1397      );
   1398 
   1399      if (!commandID) {
   1400        continue;
   1401      }
   1402 
   1403      const isSidebarClosed = !this._state?.launcherVisible;
   1404      const isCurrentView = this._state?.command === commandID;
   1405 
   1406      // Don't show sidebar badge if sidebar is open and user is already viewing the tool panel
   1407      if (badgePref && isCurrentView && this.isOpen) {
   1408        this.dismissSidebarBadge(commandID);
   1409      }
   1410 
   1411      // Show badge on toolbar if we would have shown it on a visible tool but sidebar is closed
   1412      const tool = this.toolsAndExtensions.get(commandID);
   1413      if (
   1414        this.sidebarRevampEnabled &&
   1415        badgePref &&
   1416        !tool.disabled &&
   1417        !tool.hidden &&
   1418        isSidebarClosed
   1419      ) {
   1420        this._showToolbarButtonBadge();
   1421      } else {
   1422        this._clearToolbarButtonBadge();
   1423      }
   1424 
   1425      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
   1426    }
   1427  },
   1428 
   1429  _addHoverStateBlocker() {
   1430    this._hoverBlockerCount++;
   1431    MousePosTracker.removeListener(this);
   1432  },
   1433 
   1434  async _removeHoverStateBlocker() {
   1435    if (this._hoverBlockerCount == 1) {
   1436      let isHovered = this._checkIsHoveredOverLauncher();
   1437 
   1438      // Collapse sidebar if needed
   1439      if (this._state.launcherExpanded && !isHovered) {
   1440        if (this._animationEnabled && !window.gReduceMotion) {
   1441          this._animateSidebarMain();
   1442        }
   1443        this._state.launcherExpanded = false;
   1444        await this.waitUntilStable();
   1445      }
   1446 
   1447      // Re-add MousePosTracker listener
   1448      MousePosTracker.addListener(this);
   1449    }
   1450    if (this._hoverBlockerCount > 0) {
   1451      this._hoverBlockerCount--;
   1452    }
   1453  },
   1454 
   1455  _showToolbarButtonBadge() {
   1456    const badgeEl = this.toolbarButton?.querySelector(".toolbarbutton-badge");
   1457    return badgeEl?.classList.add("feature-callout");
   1458  },
   1459 
   1460  _clearToolbarButtonBadge() {
   1461    const badgeEl = this.toolbarButton?.querySelector(".toolbarbutton-badge");
   1462    return badgeEl?.classList.remove("feature-callout");
   1463  },
   1464 
   1465  /**
   1466   * Set badge toolID pref false on clicking the tool icon
   1467   *
   1468   * @param {string} view
   1469   */
   1470  dismissSidebarBadge(view) {
   1471    const prefName = `sidebar.notification.badge.${toolsNameMap[view]}`;
   1472    if (Services.prefs.getBoolPref(prefName, false)) {
   1473      Services.prefs.setBoolPref(prefName, false);
   1474    }
   1475  },
   1476 
   1477  /**
   1478   * Enable the splitter which can be used to resize the launcher.
   1479   */
   1480  _enableLauncherDragging() {
   1481    if (!this._launcherSplitter.hidden) {
   1482      // Already showing the launcher splitter with observers connected.
   1483      // Nothing to do.
   1484      return;
   1485    }
   1486    this._panelResizeObserver = new ResizeObserver(
   1487      ([entry]) => (this._state.panelWidth = entry.contentBoxSize[0].inlineSize)
   1488    );
   1489    this._panelResizeObserver.observe(this._box);
   1490 
   1491    this._launcherDropHandler = () => (this._state.launcherDragActive = false);
   1492    this._launcherSplitter.addEventListener(
   1493      "command",
   1494      this._launcherDropHandler
   1495    );
   1496 
   1497    this._launcherSplitter.hidden = false;
   1498  },
   1499 
   1500  /**
   1501   * Enable the splitter which can be used to resize the pinned tabs container.
   1502   */
   1503  _enablePinnedTabsSplitterDragging() {
   1504    if (!this._pinnedTabsSplitter.hidden) {
   1505      // Already showing the launcher splitter with observers connected.
   1506      // Nothing to do.
   1507      return;
   1508    }
   1509    this._pinnedTabsResizeObserver = new ResizeObserver(() => {
   1510      if (this.isPinnedTabsDragging) {
   1511        this._state.pinnedTabsDragActive = true;
   1512      }
   1513    });
   1514 
   1515    this._itemsWrapperResizeObserver = new ResizeObserver(async () => {
   1516      await window.promiseDocumentFlushed(() => {
   1517        // Adjust pinned tabs container height if needed
   1518        requestAnimationFrame(() => {
   1519          // If we are currently moving tabs, don't resize
   1520          if (this._pinnedTabsContainer.hasAttribute("dragActive")) {
   1521            return;
   1522          }
   1523 
   1524          this.updatePinnedTabsHeightOnResize();
   1525        });
   1526      });
   1527    });
   1528    this._pinnedTabsResizeObserver.observe(this._pinnedTabsContainer);
   1529    this._itemsWrapperResizeObserver.observe(this._pinnedTabsItemsWrapper);
   1530 
   1531    this._pinnedTabsDropHandler = () =>
   1532      (this._state.pinnedTabsDragActive = false);
   1533    this._pinnedTabsSplitter.addEventListener(
   1534      "command",
   1535      this._pinnedTabsDropHandler
   1536    );
   1537 
   1538    this._pinnedTabsSplitter.hidden = false;
   1539  },
   1540 
   1541  /**
   1542   * Disable the launcher splitter and remove any active observers.
   1543   */
   1544  _disableLauncherDragging() {
   1545    if (this._panelResizeObserver) {
   1546      this._panelResizeObserver.disconnect();
   1547    }
   1548    this._launcherSplitter.removeEventListener(
   1549      "command",
   1550      this._launcherDropHandler
   1551    );
   1552 
   1553    this._launcherSplitter.hidden = true;
   1554  },
   1555 
   1556  /**
   1557   * Disable the pinned tabs splitter and remove any active observers.
   1558   */
   1559  _disablePinnedTabsDragging() {
   1560    if (this._pinnedTabsResizeObserver) {
   1561      this._pinnedTabsResizeObserver.disconnect();
   1562    }
   1563    if (this._itemsWrapperResizeObserver) {
   1564      this._itemsWrapperResizeObserver.disconnect();
   1565    }
   1566 
   1567    this._pinnedTabsSplitter.hidden = true;
   1568  },
   1569 
   1570  _loadSidebarExtension(commandID) {
   1571    let sidebar = this.sidebars.get(commandID);
   1572    if (typeof sidebar?.onload === "function") {
   1573      sidebar.onload();
   1574    }
   1575  },
   1576 
   1577  updatePinnedTabsHeightOnResize() {
   1578    let itemsWrapperHeight = window.windowUtils.getBoundsWithoutFlushing(
   1579      this._pinnedTabsItemsWrapper
   1580    ).height;
   1581    if (this._state.pinnedTabsHeight > itemsWrapperHeight) {
   1582      this._state.pinnedTabsHeight = itemsWrapperHeight;
   1583      if (this._state.launcherExpanded) {
   1584        this._state.expandedPinnedTabsHeight = this._state.pinnedTabsHeight;
   1585      } else {
   1586        this._state.collapsedPinnedTabsHeight = this._state.pinnedTabsHeight;
   1587      }
   1588    }
   1589  },
   1590 
   1591  /**
   1592   * Ensure tools reflect the current pref state
   1593   */
   1594  refreshTools() {
   1595    let changed = false;
   1596    const tools = new Set(this.sidebarRevampTools.split(","));
   1597    this.toolsAndExtensions.forEach(tool => {
   1598      const expected = !tools.has(tool.name);
   1599      if (tool.disabled != expected) {
   1600        tool.disabled = expected;
   1601        changed = true;
   1602      }
   1603    });
   1604    if (changed) {
   1605      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
   1606    }
   1607  },
   1608 
   1609  /**
   1610   * Sets the disabled property for a tool when customizing sidebar options
   1611   *
   1612   * @param {string} commandID
   1613   */
   1614  toggleTool(commandID) {
   1615    const toggledTool = this.toolsAndExtensions.get(commandID);
   1616    const toolName = toggledTool.name;
   1617    toggledTool.disabled = !toggledTool.disabled;
   1618 
   1619    if (!toggledTool.disabled) {
   1620      // If re-enabling tool, remove from the map and add it to the end
   1621      this.toolsAndExtensions.delete(commandID);
   1622      this.toolsAndExtensions.set(commandID, toggledTool);
   1623    }
   1624 
   1625    this.SidebarManager.updateToolsPref(toolName, toggledTool.disabled);
   1626 
   1627    if (toggledTool.disabled) {
   1628      this.dismissSidebarBadge(commandID);
   1629    }
   1630    window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
   1631  },
   1632 
   1633  addOrUpdateExtension(commandID, extension) {
   1634    if (this.inSingleTabWindow) {
   1635      return;
   1636    }
   1637    if (this.toolsAndExtensions.has(commandID)) {
   1638      // Update existing extension
   1639      let extensionToUpdate = this.toolsAndExtensions.get(commandID);
   1640      extensionToUpdate.icon = extension.icon;
   1641      extensionToUpdate.iconUrl = extension.iconUrl;
   1642      extensionToUpdate.tooltiptext = extension.label;
   1643      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
   1644    } else {
   1645      // Add new extension
   1646      const name = extension.extensionId;
   1647      this.toolsAndExtensions.set(commandID, {
   1648        view: commandID,
   1649        extensionId: extension.extensionId,
   1650        icon: extension.icon,
   1651        iconUrl: extension.iconUrl,
   1652        tooltiptext: extension.label,
   1653        disabled: !this.sidebarTools.includes(name), // name is the extensionID
   1654        name,
   1655      });
   1656      window.dispatchEvent(new CustomEvent("SidebarItemAdded"));
   1657    }
   1658  },
   1659 
   1660  /**
   1661   * Add menu items for a browser extension. Add the extension to the
   1662   * `sidebars` map.
   1663   *
   1664   * @param {string} commandID
   1665   * @param {object} props
   1666   */
   1667  registerExtension(commandID, props) {
   1668    const sidebarTools = this.sidebarTools;
   1669    const installedExtensions = this.sidebarExtensions;
   1670    const name = props.extensionId;
   1671 
   1672    // An extension that is newly installed will be added to the sidebar.main.tools
   1673    // pref by default until a user deselects it; separately we update our list of
   1674    // sidebar extensions to ensure it keeps track of what's been installed.
   1675    if (!installedExtensions.includes(name) && !sidebarTools.includes(name)) {
   1676      sidebarTools.push(name);
   1677      installedExtensions.push(name);
   1678      Services.prefs.setStringPref(this.TOOLS_PREF, sidebarTools.join());
   1679      Services.prefs.setStringPref(
   1680        this.INSTALLED_EXTENSIONS,
   1681        installedExtensions.join()
   1682      );
   1683    }
   1684 
   1685    const sidebar = {
   1686      title: props.title,
   1687      url: "chrome://browser/content/webext-panels.xhtml",
   1688      menuId: props.menuId,
   1689      switcherMenuId: `sidebarswitcher_menu_${commandID}`,
   1690      keyId: `ext-key-id-${commandID}`,
   1691      label: props.title,
   1692      icon: props.icon,
   1693      iconUrl: props.iconUrl,
   1694      classAttribute: "menuitem-iconic webextension-menuitem",
   1695      // The following properties are specific to extensions
   1696      extensionId: props.extensionId,
   1697      onload: props.onload,
   1698      name,
   1699    };
   1700    this.sidebars.set(commandID, sidebar);
   1701 
   1702    // Insert a menuitem for View->Show Sidebars.
   1703    const menuitem = this.createMenuItem(commandID, sidebar);
   1704    document.getElementById("viewSidebarMenu").appendChild(menuitem);
   1705    this.addOrUpdateExtension(commandID, sidebar);
   1706 
   1707    if (!this.sidebarRevampEnabled) {
   1708      // Insert a toolbarbutton for the sidebar dropdown selector.
   1709      let switcherMenuitem = this.createMenuItem(commandID, sidebar);
   1710      switcherMenuitem.setAttribute("id", sidebar.switcherMenuId);
   1711      switcherMenuitem.removeAttribute("type");
   1712 
   1713      let separator = document.getElementById("sidebar-extensions-separator");
   1714      separator.parentNode.insertBefore(switcherMenuitem, separator);
   1715    }
   1716    this._setExtensionAttributes(
   1717      commandID,
   1718      { icon: props.icon, iconUrl: props.iconUrl, label: props.title },
   1719      sidebar
   1720    );
   1721  },
   1722 
   1723  /**
   1724   * Create a menu item for the View>Sidebars submenu in the menubar.
   1725   *
   1726   * @param {string} commandID
   1727   * @param {object} sidebar
   1728   * @returns {Element}
   1729   */
   1730  createMenuItem(commandID, sidebar) {
   1731    const menuitem = document.createXULElement("menuitem");
   1732    menuitem.setAttribute("id", sidebar.menuId);
   1733    menuitem.setAttribute("type", "checkbox");
   1734    // Some menu items get checkbox type removed, so should show the sidebar
   1735    menuitem.addEventListener("command", () =>
   1736      this[menuitem.hasAttribute("type") ? "toggle" : "show"](commandID)
   1737    );
   1738    if (sidebar.classAttribute) {
   1739      menuitem.setAttribute("class", sidebar.classAttribute);
   1740    }
   1741    if (sidebar.keyId) {
   1742      menuitem.setAttribute("key", sidebar.keyId);
   1743    }
   1744    if (sidebar.menuL10nId) {
   1745      menuitem.dataset.l10nId = sidebar.menuL10nId;
   1746    }
   1747    if (this.inSingleTabWindow) {
   1748      menuitem.setAttribute("disabled", "true");
   1749    }
   1750    return menuitem;
   1751  },
   1752 
   1753  /**
   1754   * Update attributes on all existing menu items for a browser extension.
   1755   *
   1756   * @param {string} commandID
   1757   * @param {object} attributes
   1758   * @param {string} attributes.icon
   1759   * @param {string} attributes.iconUrl
   1760   * @param {string} attributes.label
   1761   * @param {boolean} needsRefresh
   1762   */
   1763  setExtensionAttributes(commandID, attributes, needsRefresh) {
   1764    const sidebar = this.sidebars.get(commandID);
   1765    this._setExtensionAttributes(commandID, attributes, sidebar, needsRefresh);
   1766    this.addOrUpdateExtension(commandID, sidebar);
   1767  },
   1768 
   1769  _setExtensionAttributes(
   1770    commandID,
   1771    { icon, iconUrl, label },
   1772    sidebar,
   1773    needsRefresh = false
   1774  ) {
   1775    sidebar.icon = icon;
   1776    sidebar.iconUrl = iconUrl;
   1777    sidebar.label = label;
   1778 
   1779    const updateAttributes = el => {
   1780      // TODO Bug 1996762 - Add support for dark-theme sidebar icons
   1781      // --webextension-menuitem-image-dark is used in dark themes
   1782      el.style.setProperty("--webextension-menuitem-image", sidebar.icon);
   1783      el.setAttribute("label", sidebar.label);
   1784    };
   1785 
   1786    updateAttributes(document.getElementById(sidebar.menuId), sidebar);
   1787    const switcherMenu = document.getElementById(sidebar.switcherMenuId);
   1788    if (switcherMenu) {
   1789      updateAttributes(switcherMenu, sidebar);
   1790    }
   1791    if (this.initialized && this.currentID === commandID) {
   1792      // Update the sidebar title if this extension is the current sidebar.
   1793      this.title = label;
   1794      if (this.isOpen && needsRefresh) {
   1795        this.show(commandID);
   1796      }
   1797    }
   1798  },
   1799 
   1800  /**
   1801   * Retrieve the list of registered browser extensions.
   1802   *
   1803   * @returns {Array}
   1804   */
   1805  getExtensions() {
   1806    const extensions = [];
   1807    for (const [commandID, sidebar] of this.sidebars.entries()) {
   1808      if (Object.hasOwn(sidebar, "extensionId")) {
   1809        const disabled = !this.sidebarTools.includes(sidebar.name);
   1810 
   1811        extensions.push({
   1812          commandID,
   1813          view: commandID,
   1814          extensionId: sidebar.extensionId,
   1815          iconUrl: sidebar.iconUrl,
   1816          tooltiptext: sidebar.label,
   1817          disabled,
   1818          name: sidebar.name,
   1819        });
   1820      }
   1821    }
   1822 
   1823    return extensions;
   1824  },
   1825 
   1826  /**
   1827   * Retrieve the list of tools in the sidebar
   1828   *
   1829   * @returns {Array}
   1830   */
   1831  getTools() {
   1832    return Object.keys(toolsNameMap)
   1833      .filter(commandID => this.sidebars.get(commandID))
   1834      .map(commandID => {
   1835        const sidebar = this.sidebars.get(commandID);
   1836        const disabled = !this.sidebarTools.includes(toolsNameMap[commandID]);
   1837        return {
   1838          commandID,
   1839          view: commandID,
   1840          name: sidebar.name,
   1841          iconUrl: sidebar.iconUrl,
   1842          l10nId: sidebar.revampL10nId,
   1843          disabled,
   1844          // Reflect the current tool state defaulting to visible
   1845          get hidden() {
   1846            return !(sidebar.visible ?? true);
   1847          },
   1848          get attention() {
   1849            return sidebar.attention ?? false;
   1850          },
   1851          contextMenu: sidebar.toolContextMenuId,
   1852        };
   1853      });
   1854  },
   1855 
   1856  /**
   1857   * Remove a browser extension.
   1858   *
   1859   * @param {string} commandID
   1860   */
   1861  removeExtension(commandID) {
   1862    if (this.inSingleTabWindow) {
   1863      return;
   1864    }
   1865    const sidebar = this.sidebars.get(commandID);
   1866    if (!sidebar) {
   1867      return;
   1868    }
   1869    if (this.currentID === commandID) {
   1870      // If the extension removal is a update, we don't want to forget this panel.
   1871      // So, let the sidebarAction extension API code remove the lastOpenedId as needed
   1872      this.hide({ dismissPanel: false });
   1873    }
   1874    document.getElementById(sidebar.menuId)?.remove();
   1875    document.getElementById(sidebar.switcherMenuId)?.remove();
   1876 
   1877    this.sidebars.delete(commandID);
   1878    this.toolsAndExtensions.delete(commandID);
   1879    window.dispatchEvent(new CustomEvent("SidebarItemRemoved"));
   1880  },
   1881 
   1882  /**
   1883   * Show the sidebar.
   1884   *
   1885   * This wraps the internal method, including a ping to telemetry.
   1886   *
   1887   * @param {string}  commandID     ID of the sidebar to use.
   1888   * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
   1889   *                                showing of the sidebar.
   1890   * @returns {Promise<boolean>}
   1891   */
   1892  async show(commandID, triggerNode) {
   1893    if (this.inSingleTabWindow) {
   1894      return false;
   1895    }
   1896    if (this.currentID && commandID !== this.currentID) {
   1897      // If there is currently a panel open, we are about to hide it in order
   1898      // to show another one, so record a "hide" event on the current panel.
   1899      this._recordPanelToggle(this.currentID, false);
   1900    }
   1901    this._recordPanelToggle(commandID, true);
   1902 
   1903    // Extensions without private window access wont be in the
   1904    // sidebars map.
   1905    if (!this.sidebars.has(commandID)) {
   1906      return false;
   1907    }
   1908    return this._show(commandID).then(() => {
   1909      this._loadSidebarExtension(commandID);
   1910 
   1911      if (triggerNode) {
   1912        updateToggleControlLabel(triggerNode);
   1913      }
   1914      this.updateToolbarButton();
   1915      this.dismissSidebarBadge(commandID);
   1916 
   1917      this._fireFocusedEvent();
   1918      return true;
   1919    });
   1920  },
   1921 
   1922  /**
   1923   * Show the sidebar, without firing the focused event or logging telemetry.
   1924   * This is intended to be used when the sidebar is opened automatically
   1925   * when a window opens (not triggered by user interaction).
   1926   *
   1927   * @param {string} commandID ID of the sidebar.
   1928   * @returns {Promise<boolean>}
   1929   */
   1930  async showInitially(commandID) {
   1931    if (this.inSingleTabWindow) {
   1932      return false;
   1933    }
   1934    this._recordPanelToggle(commandID, true);
   1935 
   1936    // Extensions without private window access wont be in the
   1937    // sidebars map.
   1938    if (!this.sidebars.has(commandID)) {
   1939      return false;
   1940    }
   1941    return this._show(commandID).then(() => {
   1942      this._loadSidebarExtension(commandID);
   1943      return true;
   1944    });
   1945  },
   1946 
   1947  /**
   1948   * Implementation for show. Also used internally for sidebars that are shown
   1949   * when a window is opened and we don't want to ping telemetry.
   1950   *
   1951   * @param {string} commandID ID of the sidebar.
   1952   * @returns {Promise<void>}
   1953   */
   1954  _show(commandID) {
   1955    return new Promise(resolve => {
   1956      const willShowEvent = new CustomEvent("SidebarWillShow");
   1957      this.browser.contentWindow?.dispatchEvent(willShowEvent);
   1958 
   1959      this._state.panelOpen = true;
   1960      if (this.sidebarRevampEnabled) {
   1961        this._box.dispatchEvent(
   1962          new CustomEvent("sidebar-show", { detail: { viewId: commandID } })
   1963        );
   1964      } else {
   1965        this.hideSwitcherPanel();
   1966      }
   1967 
   1968      this.selectMenuItem(commandID);
   1969      this._box.hidden = this._splitter.hidden = false;
   1970 
   1971      this._box.setAttribute("checked", "true");
   1972      this._state.command = commandID;
   1973 
   1974      let { icon, url, title, sourceL10nEl, contextMenuId } =
   1975        this.sidebars.get(commandID);
   1976      if (icon) {
   1977        this._switcherTarget.style.setProperty(
   1978          "--webextension-menuitem-image",
   1979          icon
   1980        );
   1981      } else {
   1982        this._switcherTarget.style.removeProperty(
   1983          "--webextension-menuitem-image"
   1984        );
   1985      }
   1986 
   1987      if (contextMenuId) {
   1988        this._box.setAttribute("context", contextMenuId);
   1989      } else {
   1990        this._box.removeAttribute("context");
   1991      }
   1992 
   1993      // use to live update <tree> elements if the locale changes
   1994      this.lastOpenedId = commandID;
   1995      // These title changes only apply to the old sidebar menu
   1996      if (!this.sidebarRevampEnabled) {
   1997        this.title = title;
   1998        // Keep the title element in the switcher in sync with any l10n changes.
   1999        this.observeTitleChanges(sourceL10nEl);
   2000      }
   2001 
   2002      this.browser.setAttribute("src", url); // kick off async load
   2003 
   2004      if (this.browser.contentDocument.location.href != url) {
   2005        // make sure to clear the timeout if the load is aborted
   2006        this.browser.addEventListener("unload", () => {
   2007          if (this.browser.loadingTimerID) {
   2008            clearTimeout(this.browser.loadingTimerID);
   2009            delete this.browser.loadingTimerID;
   2010            resolve();
   2011          }
   2012        });
   2013        this.browser.addEventListener(
   2014          "load",
   2015          () => {
   2016            // We're handling the 'load' event before it bubbles up to the usual
   2017            // (non-capturing) event handlers. Let it bubble up before resolving.
   2018            this.browser.loadingTimerID = setTimeout(() => {
   2019              delete this.browser.loadingTimerID;
   2020              resolve();
   2021 
   2022              // Now that the currentId is updated, fire a show event.
   2023              this._fireShowEvent();
   2024              this._recordBrowserSize();
   2025            }, 0);
   2026          },
   2027          { capture: true, once: true }
   2028        );
   2029      } else {
   2030        resolve();
   2031 
   2032        // Now that the currentId is updated, fire a show event.
   2033        this._fireShowEvent();
   2034        this._recordBrowserSize();
   2035      }
   2036    });
   2037  },
   2038 
   2039  /**
   2040   * Hide the sidebar.
   2041   *
   2042   * @param {object} options - Parameter object.
   2043   * @param {DOMNode} options.triggerNode - Node, usually a button, that triggered the
   2044   *                                        hiding of the sidebar.
   2045   * @param {boolean} options.dismissPanel -Only close the panel or close the whole sidebar (the default.)
   2046   */
   2047  hide({ triggerNode, dismissPanel = this.sidebarRevampEnabled } = {}) {
   2048    if (!this.isOpen) {
   2049      return;
   2050    }
   2051 
   2052    const willHideEvent = new CustomEvent("SidebarWillHide", {
   2053      cancelable: true,
   2054    });
   2055    this.browser.contentWindow?.dispatchEvent(willHideEvent);
   2056    if (willHideEvent.defaultPrevented) {
   2057      return;
   2058    }
   2059 
   2060    this.hideSwitcherPanel();
   2061    this._recordPanelToggle(this.currentID, false);
   2062    this._state.panelOpen = false;
   2063    if (dismissPanel) {
   2064      // The user is explicitly closing this panel so we don't want it to
   2065      // automatically re-open next time the sidebar is shown
   2066      this._state.command = "";
   2067      this.lastOpenedId = null;
   2068    }
   2069 
   2070    if (this.sidebarRevampEnabled) {
   2071      this._box.dispatchEvent(new CustomEvent("sidebar-hide"));
   2072    }
   2073    this.selectMenuItem("");
   2074 
   2075    // Replace the document currently displayed in the sidebar with about:blank
   2076    // so that we can free memory by unloading the page. We need to explicitly
   2077    // create a new content viewer because the old one doesn't get destroyed
   2078    // until about:blank has loaded (which does not happen as long as the
   2079    // element is hidden).
   2080    this.browser.setAttribute("src", "about:blank");
   2081    this.browser.docShell?.createAboutBlankDocumentViewer(null, null);
   2082 
   2083    this._box.removeAttribute("checked");
   2084    this._box.removeAttribute("context");
   2085    this._box.hidden = this._splitter.hidden = true;
   2086 
   2087    let selBrowser = gBrowser.selectedBrowser;
   2088    selBrowser.focus();
   2089    if (triggerNode) {
   2090      updateToggleControlLabel(triggerNode);
   2091    }
   2092    this.updateToolbarButton();
   2093  },
   2094 
   2095  /**
   2096   * Record to Glean when any of the sidebar panels is loaded or unloaded.
   2097   *
   2098   * @param {string} commandID
   2099   * @param {boolean} opened
   2100   */
   2101  _recordPanelToggle(commandID, opened) {
   2102    const sidebar = this.sidebars.get(commandID);
   2103    if (!sidebar) {
   2104      return;
   2105    }
   2106    const isExtension = sidebar && Object.hasOwn(sidebar, "extensionId");
   2107    const version = this.sidebarRevampEnabled ? "new" : "old";
   2108    if (isExtension) {
   2109      const addonId = sidebar.extensionId;
   2110      const addonName = WebExtensionPolicy.getByID(addonId)?.name;
   2111      Glean.extension.sidebarToggle.record({
   2112        opened,
   2113        version,
   2114        addon_id: AMTelemetry.getTrimmedString(addonId),
   2115        addon_name: addonName && AMTelemetry.getTrimmedString(addonName),
   2116      });
   2117    } else if (sidebar.gleanEvent && sidebar.recordSidebarVersion) {
   2118      sidebar.gleanEvent.record({ opened, version });
   2119    } else if (sidebar.gleanEvent) {
   2120      sidebar.gleanEvent.record({ opened });
   2121    }
   2122  },
   2123 
   2124  /**
   2125   * Use MousePosTracker to manually check for hover state over launcher
   2126   */
   2127  _checkIsHoveredOverLauncher() {
   2128    // Manually check mouse position
   2129    let isHovered;
   2130    MousePosTracker._callListener({
   2131      onMouseEnter: () => (isHovered = true),
   2132      onMouseLeave: () => (isHovered = false),
   2133      getMouseTargetRect: () => this.getMouseTargetRect(),
   2134    });
   2135    return isHovered;
   2136  },
   2137 
   2138  /**
   2139   * Record to Glean when any of the sidebar icons are clicked.
   2140   *
   2141   * @param {string} commandID - Command ID of the icon.
   2142   * @param {boolean} expanded - Whether the sidebar was expanded when clicked.
   2143   */
   2144  recordIconClick(commandID, expanded) {
   2145    const sidebar = this.sidebars.get(commandID);
   2146    const isExtension = sidebar && Object.hasOwn(sidebar, "extensionId");
   2147    if (isExtension) {
   2148      const addonId = sidebar.extensionId;
   2149      Glean.sidebar.addonIconClick.record({
   2150        sidebar_open: expanded,
   2151        addon_id: AMTelemetry.getTrimmedString(addonId),
   2152      });
   2153    } else if (sidebar.gleanClickEvent) {
   2154      sidebar.gleanClickEvent.record({
   2155        sidebar_open: expanded,
   2156      });
   2157    }
   2158  },
   2159 
   2160  /**
   2161   * Sets the checked state only on the menu items of the specified sidebar, or
   2162   * none if the argument is an empty string.
   2163   */
   2164  selectMenuItem(commandID) {
   2165    for (let [id, { menuId, triggerButtonId }] of this.sidebars) {
   2166      let menu = document.getElementById(menuId);
   2167      if (!menu) {
   2168        continue;
   2169      }
   2170      let triggerbutton =
   2171        triggerButtonId && document.getElementById(triggerButtonId);
   2172      if (id == commandID) {
   2173        menu.setAttribute("checked", "true");
   2174        if (triggerbutton) {
   2175          triggerbutton.setAttribute("checked", "true");
   2176          updateToggleControlLabel(triggerbutton);
   2177        }
   2178      } else {
   2179        menu.removeAttribute("checked");
   2180        if (triggerbutton) {
   2181          triggerbutton.removeAttribute("checked");
   2182          updateToggleControlLabel(triggerbutton);
   2183        }
   2184      }
   2185    }
   2186  },
   2187 
   2188  toggleTabstrip() {
   2189    let toVerticalTabs = CustomizableUI.verticalTabsEnabled;
   2190    let tabStrip = gBrowser.tabContainer;
   2191    let arrowScrollbox = tabStrip.arrowScrollbox;
   2192    let currentScrollOrientation = arrowScrollbox.getAttribute("orient");
   2193 
   2194    if (
   2195      (!toVerticalTabs && currentScrollOrientation !== "vertical") ||
   2196      (toVerticalTabs && currentScrollOrientation === "vertical")
   2197    ) {
   2198      // Nothing to update
   2199      return;
   2200    }
   2201 
   2202    if (toVerticalTabs) {
   2203      arrowScrollbox.setAttribute("orient", "vertical");
   2204      tabStrip.setAttribute("orient", "vertical");
   2205      this._clearToolbarButtonBadge();
   2206    } else {
   2207      arrowScrollbox.setAttribute("orient", "horizontal");
   2208      tabStrip.removeAttribute("expanded");
   2209      tabStrip.setAttribute("orient", "horizontal");
   2210    }
   2211 
   2212    let verticalToolbar = document.getElementById(
   2213      CustomizableUI.AREA_VERTICAL_TABSTRIP
   2214    );
   2215    verticalToolbar.toggleAttribute("visible", toVerticalTabs);
   2216    // Re-render sidebar-main so that templating is updated
   2217    // for proper keyboard navigation for Tools
   2218    this.sidebarMain.requestUpdate();
   2219    if (
   2220      !this.verticalTabsEnabled &&
   2221      this.sidebarRevampVisibility == "hide-sidebar"
   2222    ) {
   2223      // the sidebar.visibility pref didn't change so launcherExpanded hasn't
   2224      // been updated; we need to set it here to un-expand the launcher
   2225      this._state.launcherExpanded = false;
   2226    }
   2227  },
   2228 
   2229  debouncedMouseEnter() {
   2230    const contentArea = document.getElementById("tabbrowser-tabbox");
   2231    this._box.toggleAttribute("sidebar-launcher-hovered", true);
   2232    contentArea.toggleAttribute("sidebar-launcher-hovered", true);
   2233    this._state.launcherHoverActive = true;
   2234    if (this._animationEnabled && !window.gReduceMotion) {
   2235      this._animateSidebarMain();
   2236    }
   2237    this._state.launcherExpanded = true;
   2238    this._mouseEnterDeferred.resolve();
   2239  },
   2240 
   2241  onMouseLeave() {
   2242    if (!this._state.launcherExpanded) {
   2243      return;
   2244    }
   2245    this.mouseEnterTask.disarm();
   2246    this._mouseEnterDeferred.resolve();
   2247    const contentArea = document.getElementById("tabbrowser-tabbox");
   2248    this._box.toggleAttribute("sidebar-launcher-hovered", false);
   2249    contentArea.toggleAttribute("sidebar-launcher-hovered", false);
   2250    this._state.launcherHoverActive = false;
   2251    if (this._animationEnabled && !window.gReduceMotion) {
   2252      this._animateSidebarMain();
   2253    }
   2254    this._state.launcherExpanded = false;
   2255  },
   2256 
   2257  onMouseEnter() {
   2258    if (this._state.launcherExpanded) {
   2259      return;
   2260    }
   2261    this._mouseEnterDeferred = Promise.withResolvers();
   2262    this.mouseEnterTask = new DeferredTask(
   2263      () => {
   2264        let isHovered = this._checkIsHoveredOverLauncher();
   2265 
   2266        // Only expand sidebar if mouse is still hovering over sidebar launcher
   2267        if (isHovered) {
   2268          this.debouncedMouseEnter();
   2269        }
   2270      },
   2271      this._animationExpandOnHoverDelayDurationMs,
   2272      EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS
   2273    );
   2274    this.mouseEnterTask?.arm();
   2275  },
   2276 
   2277  get expandOnHoverComplete() {
   2278    return this._mouseEnterDeferred?.promise || Promise.resolve();
   2279  },
   2280 
   2281  async setLauncherCollapsedWidth() {
   2282    let browserEl = document.getElementById("browser");
   2283    if (this.getUIState().launcherExpanded) {
   2284      this._state.launcherExpanded = false;
   2285    }
   2286    await this.waitUntilStable();
   2287    let collapsedWidth = await new Promise(resolve => {
   2288      requestAnimationFrame(() => {
   2289        resolve(this._getRects([this.sidebarMain])[0][1].width);
   2290      });
   2291    });
   2292 
   2293    browserEl.style.setProperty(
   2294      "--sidebar-launcher-collapsed-width",
   2295      `${collapsedWidth}px`
   2296    );
   2297  },
   2298 
   2299  getMouseTargetRect() {
   2300    let launcherRect = window.windowUtils.getBoundsWithoutFlushing(
   2301      SidebarController.sidebarMain
   2302    );
   2303    return {
   2304      top: launcherRect.top,
   2305      bottom: launcherRect.bottom,
   2306      left: this._positionStart
   2307        ? launcherRect.left
   2308        : launcherRect.left + LAUNCHER_SPLITTER_WIDTH,
   2309      right: this._positionStart
   2310        ? launcherRect.right - LAUNCHER_SPLITTER_WIDTH
   2311        : launcherRect.right,
   2312    };
   2313  },
   2314 
   2315  async handleEvent(e) {
   2316    switch (e.type) {
   2317      case "popupshown":
   2318        /* Temporarily remove MousePosTracker listener when a context menu is open */
   2319        if (e.composedTarget.tagName !== "tooltip") {
   2320          this._addHoverStateBlocker();
   2321        }
   2322        break;
   2323      case "popuphidden":
   2324        if (e.composedTarget.tagName !== "tooltip") {
   2325          await this._removeHoverStateBlocker();
   2326        }
   2327        break;
   2328      default:
   2329        break;
   2330    }
   2331  },
   2332 
   2333  async toggleExpandOnHover(isEnabled, isDragEnded) {
   2334    document.documentElement.toggleAttribute(
   2335      "sidebar-expand-on-hover",
   2336      isEnabled
   2337    );
   2338    if (isEnabled) {
   2339      if (!this._state) {
   2340        this._state = new this.SidebarState(this);
   2341      }
   2342      await this.waitUntilStable();
   2343      MousePosTracker.addListener(this);
   2344      if (!isDragEnded) {
   2345        await this.setLauncherCollapsedWidth();
   2346      }
   2347      document.addEventListener("popupshown", this);
   2348      document.addEventListener("popuphidden", this);
   2349      // Reset user-preferred height
   2350      this.sidebarMain.buttonsWrapper.style.height = this._state
   2351        .launcherExpanded
   2352        ? ""
   2353        : "0";
   2354    } else {
   2355      this._removeHoverStateBlocker();
   2356      MousePosTracker.removeListener(this);
   2357      if (!this.mouseOverTask?.isFinalized) {
   2358        this.mouseOverTask?.finalize();
   2359      }
   2360      document.removeEventListener("popupshown", this);
   2361      document.removeEventListener("popuphidden", this);
   2362      // Add back user-preferred height if defined
   2363      if (
   2364        this._state.launcherExpanded &&
   2365        this._state.expandedToolsHeight !== undefined &&
   2366        this.sidebarMain.buttonGroup
   2367      ) {
   2368        this.sidebarMain.buttonGroup.style.height =
   2369          this._state.expandedToolsHeight;
   2370      } else if (
   2371        !this._state.launcherExpanded &&
   2372        this._state.collapsedToolsHeight !== undefined &&
   2373        this.sidebarMain.buttonGroup
   2374      ) {
   2375        this.sidebarMain.buttonGroup.style.height =
   2376          this._state.collapsedToolsHeight;
   2377      }
   2378    }
   2379 
   2380    document.documentElement.toggleAttribute(
   2381      "sidebar-expand-on-hover",
   2382      isEnabled
   2383    );
   2384  },
   2385 
   2386  /**
   2387   * Report visibility preference to Glean.
   2388   *
   2389   * @param {string} [value] - The preference value.
   2390   */
   2391  recordVisibilitySetting(value = this.sidebarRevampVisibility) {
   2392    let visibilitySetting = "hide";
   2393    if (value === "always-show") {
   2394      visibilitySetting = "always";
   2395    } else if (value === "expand-on-hover") {
   2396      visibilitySetting = "expand-on-hover";
   2397    }
   2398    Glean.sidebar.displaySettings.set(visibilitySetting);
   2399  },
   2400 
   2401  /**
   2402   * Report position preference to Glean.
   2403   *
   2404   * @param {boolean} [value] - The preference value.
   2405   */
   2406  recordPositionSetting(value = this._positionStart) {
   2407    Glean.sidebar.positionSettings.set(value !== RTL_UI ? "left" : "right");
   2408  },
   2409 
   2410  /**
   2411   * Report tabs layout preference to Glean.
   2412   *
   2413   * @param {boolean} [value] - The preference value.
   2414   */
   2415  recordTabsLayoutSetting(value = this.sidebarVerticalTabsEnabled) {
   2416    Glean.sidebar.tabsLayout.set(value ? "vertical" : "horizontal");
   2417  },
   2418 };
   2419 
   2420 ChromeUtils.defineESModuleGetters(SidebarController, {
   2421  AIWindow:
   2422    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
   2423  SidebarManager:
   2424    "moz-src:///browser/components/sidebar/SidebarManager.sys.mjs",
   2425  SidebarState: "moz-src:///browser/components/sidebar/SidebarState.sys.mjs",
   2426 });
   2427 
   2428 // Add getters related to the position here, since we will want them
   2429 // available for both startDelayedLoad and init.
   2430 XPCOMUtils.defineLazyPreferenceGetter(
   2431  SidebarController,
   2432  "_positionStart",
   2433  SidebarController.POSITION_START_PREF,
   2434  true,
   2435  (_aPreference, _previousValue, newValue) => {
   2436    if (
   2437      !SidebarController.uninitializing &&
   2438      !SidebarController.inSingleTabWindow
   2439    ) {
   2440      SidebarController.setPosition();
   2441      SidebarController.recordPositionSetting(newValue);
   2442    }
   2443  }
   2444 );
   2445 XPCOMUtils.defineLazyPreferenceGetter(
   2446  SidebarController,
   2447  "_animationEnabled",
   2448  "sidebar.animation.enabled",
   2449  true
   2450 );
   2451 XPCOMUtils.defineLazyPreferenceGetter(
   2452  SidebarController,
   2453  "_animationDurationMs",
   2454  "sidebar.animation.duration-ms",
   2455  200
   2456 );
   2457 XPCOMUtils.defineLazyPreferenceGetter(
   2458  SidebarController,
   2459  "_animationExpandOnHoverDurationMs",
   2460  "sidebar.animation.expand-on-hover.duration-ms",
   2461  400
   2462 );
   2463 XPCOMUtils.defineLazyPreferenceGetter(
   2464  SidebarController,
   2465  "_animationExpandOnHoverDelayDurationMs",
   2466  "sidebar.animation.expand-on-hover.delay-duration-ms",
   2467  200
   2468 );
   2469 XPCOMUtils.defineLazyPreferenceGetter(
   2470  SidebarController,
   2471  "sidebarRevampEnabled",
   2472  "sidebar.revamp",
   2473  false,
   2474  (_aPreference, _previousValue, newValue) => {
   2475    if (!SidebarController.uninitializing) {
   2476      SidebarController.toggleRevampSidebar();
   2477      SidebarController._state.revampEnabled = newValue;
   2478    }
   2479  }
   2480 );
   2481 XPCOMUtils.defineLazyPreferenceGetter(
   2482  SidebarController,
   2483  "sidebarRevampTools",
   2484  "sidebar.main.tools",
   2485  "",
   2486  () => {
   2487    if (
   2488      !SidebarController.inSingleTabWindow &&
   2489      !SidebarController.uninitializing
   2490    ) {
   2491      SidebarController.refreshTools();
   2492    }
   2493  }
   2494 );
   2495 XPCOMUtils.defineLazyPreferenceGetter(
   2496  SidebarController,
   2497  "installedExtensions",
   2498  "sidebar.installed.extensions",
   2499  ""
   2500 );
   2501 
   2502 XPCOMUtils.defineLazyPreferenceGetter(
   2503  SidebarController,
   2504  "sidebarRevampVisibility",
   2505  "sidebar.visibility",
   2506  "always-show",
   2507  (_aPreference, _previousValue, newValue) => {
   2508    if (
   2509      !SidebarController.inSingleTabWindow &&
   2510      !SidebarController.uninitializing
   2511    ) {
   2512      SidebarController.toggleExpandOnHover(newValue === "expand-on-hover");
   2513      SidebarController.recordVisibilitySetting(newValue);
   2514      if (SidebarController._state) {
   2515        // we need to use the pref rather than SidebarController's getter here
   2516        // as the getter might not have the new value yet
   2517        const isVerticalTabs = Services.prefs.getBoolPref(
   2518          "sidebar.verticalTabs"
   2519        );
   2520        SidebarController._state.revampVisibility = newValue;
   2521        if (
   2522          SidebarController._animationEnabled &&
   2523          !window.gReduceMotion &&
   2524          newValue !== "expand-on-hover"
   2525        ) {
   2526          SidebarController._animateSidebarMain();
   2527        }
   2528 
   2529        // launcher is always initially expanded with vertical tabs unless we're doing expand-on-hover
   2530        let forceExpand = false;
   2531        if (
   2532          isVerticalTabs &&
   2533          ["always-show", "hide-sidebar"].includes(newValue)
   2534        ) {
   2535          forceExpand = true;
   2536        }
   2537 
   2538        // horizontal tabs and hide-sidebar = visible initially.
   2539        // vertical tab and hide-sidebar = not visible initially
   2540        let showLauncher = true;
   2541        if (newValue == "hide-sidebar" && isVerticalTabs) {
   2542          showLauncher = false;
   2543        }
   2544        SidebarController._state.updateVisibility(showLauncher, forceExpand);
   2545      }
   2546      SidebarController.updateToolbarButton();
   2547    }
   2548  }
   2549 );
   2550 
   2551 XPCOMUtils.defineLazyPreferenceGetter(
   2552  SidebarController,
   2553  "sidebarVerticalTabsEnabled",
   2554  "sidebar.verticalTabs",
   2555  false,
   2556  (_aPreference, _previousValue, newValue) => {
   2557    if (
   2558      !SidebarController.uninitializing &&
   2559      !SidebarController.inSingleTabWindow
   2560    ) {
   2561      SidebarController.recordTabsLayoutSetting(newValue);
   2562      if (newValue) {
   2563        SidebarController._enablePinnedTabsSplitterDragging();
   2564      } else {
   2565        SidebarController._disablePinnedTabsDragging();
   2566      }
   2567      SidebarController._state.updatePinnedTabsHeight();
   2568      SidebarController._state.updateToolsHeight();
   2569    }
   2570  }
   2571 );