tor-browser

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

CustomizableUI.sys.mjs (295143B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      7 import { SearchWidgetTracker } from "moz-src:///browser/components/customizableui/SearchWidgetTracker.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     13  AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
     14  BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
     15  CustomizableWidgets:
     16    "moz-src:///browser/components/customizableui/CustomizableWidgets.sys.mjs",
     17  HomePage: "resource:///modules/HomePage.sys.mjs",
     18  PanelMultiView:
     19    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     20  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     21  ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
     22 });
     23 
     24 ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
     25  const kUrl =
     26    "chrome://browser/locale/customizableui/customizableWidgets.properties";
     27  return Services.strings.createBundle(kUrl);
     28 });
     29 
     30 const kDefaultThemeID = "default-theme@mozilla.org";
     31 
     32 const kSpecialWidgetPfx = "customizableui-special-";
     33 
     34 const kPrefCustomizationState = "browser.uiCustomization.state";
     35 const kPrefCustomizationHorizontalTabstrip =
     36  "browser.uiCustomization.horizontalTabstrip";
     37 const kPrefCustomizationHorizontalTabsBackup =
     38  "browser.uiCustomization.horizontalTabsBackup";
     39 const kPrefCustomizationNavBarWhenVerticalTabs =
     40  "browser.uiCustomization.navBarWhenVerticalTabs";
     41 const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
     42 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
     43 const kPrefDrawInTitlebar = "browser.tabs.inTitlebar";
     44 const kPrefUIDensity = "browser.uidensity";
     45 const kPrefAutoTouchMode = "browser.touchmode.auto";
     46 const kPrefAutoHideDownloadsButton = "browser.download.autohideButton";
     47 const kPrefProtonToolbarVersion = "browser.proton.toolbar.version";
     48 const kPrefHomeButtonUsed = "browser.engagement.home-button.has-used";
     49 const kPrefLibraryButtonUsed = "browser.engagement.library-button.has-used";
     50 const kPrefSidebarButtonUsed = "browser.engagement.sidebar-button.has-used";
     51 const kPrefSidebarRevampEnabled = "sidebar.revamp";
     52 const kPrefSidebarVerticalTabsEnabled = "sidebar.verticalTabs";
     53 const kPrefSidebarPositionStartEnabled = "sidebar.position_start";
     54 
     55 const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL;
     56 
     57 var gDefaultTheme;
     58 var gSelectedTheme;
     59 
     60 /**
     61 * The keys are the handlers that are fired when the event type (the value)
     62 * is fired on the subview. A widget that provides a subview has the option
     63 * of providing onViewShowing and onViewHiding event handlers.
     64 */
     65 const kSubviewEvents = ["ViewShowing", "ViewHiding"];
     66 
     67 /**
     68 * The current version. We can use this to auto-add new default widgets as necessary.
     69 * (would be const but isn't because of testing purposes)
     70 */
     71 var kVersion = 23;
     72 
     73 /**
     74 * The current version for base browser.
     75 */
     76 var kVersionBaseBrowser = 2;
     77 const NoScriptId = "_73a6fe31-595d-460b-a920-fcc0f8843232_-browser-action";
     78 
     79 /**
     80 * The current version for tor browser.
     81 */
     82 var kVersionTorBrowser = 1;
     83 
     84 /**
     85 * Buttons removed from built-ins by version they were removed. kVersion must be
     86 * bumped any time a new id is added to this. Use the button id as key, and
     87 * version the button is removed in as the value.  e.g. "pocket-button": 5
     88 */
     89 var ObsoleteBuiltinButtons = {
     90  "feed-button": 15,
     91 };
     92 
     93 /**
     94 * gPalette is a map of every widget that CustomizableUI.sys.mjs knows about, keyed
     95 * on their IDs.
     96 */
     97 var gPalette = new Map();
     98 
     99 /**
    100 * gAreas maps area IDs to Sets of properties about those areas. An area is a
    101 * place where a widget can be put.
    102 */
    103 var gAreas = new Map();
    104 
    105 /**
    106 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
    107 * are placed within that area (either directly in the area node, or in the
    108 * customizationTarget of the node).
    109 */
    110 var gPlacements = new Map();
    111 
    112 /**
    113 * gFuturePlacements represent placements that will happen for areas that have
    114 * not yet loaded (due to lazy-loading). This can occur when add-ons register
    115 * widgets.
    116 */
    117 var gFuturePlacements = new Map();
    118 
    119 var gSupportedWidgetTypes = new Set([
    120  // A button that does a command.
    121  "button",
    122 
    123  // A button that opens a view in a panel (or in a subview of the panel).
    124  "view",
    125 
    126  // A combination of the above, which looks different depending on whether it's
    127  // located in the toolbar or in the panel: When located in the toolbar, shown
    128  // as a combined item of a button and a dropmarker button. The button triggers
    129  // the command and the dropmarker button opens the view. When located in the
    130  // panel, shown as one item which opens the view, and the button command
    131  // cannot be triggered separately.
    132  "button-and-view",
    133 
    134  // A custom widget that defines its own markup.
    135  "custom",
    136 ]);
    137 
    138 /**
    139 * gPanelsForWindow is a list of known panels in a window which we may need to close
    140 * should command events fire which target them.
    141 *
    142 * @type {WeakMap<Element, Set<Element>>}
    143 */
    144 var gPanelsForWindow = new WeakMap();
    145 
    146 /**
    147 * gSeenWidgets remembers which widgets the user has seen for the first time
    148 * before. This way, if a new widget is created, and the user has not seen it
    149 * before, it can be put in its default location. Otherwise, it remains in the
    150 * palette.
    151 */
    152 var gSeenWidgets = new Set();
    153 
    154 /**
    155 * gDirtyAreaCache is a set of area IDs for areas where items have been added,
    156 * moved or removed at least once. This set is persisted, and is used to
    157 * optimize building of toolbars in the default case where no toolbars should
    158 * be "dirty".
    159 */
    160 var gDirtyAreaCache = new Set();
    161 
    162 /**
    163 * gPendingBuildAreas is a map from area IDs to map from build nodes to their
    164 * existing children at the time of node registration, that are waiting
    165 * for the area to be registered
    166 */
    167 var gPendingBuildAreas = new Map();
    168 
    169 var gSavedState = null;
    170 var gRestoring = false;
    171 var gDirty = false;
    172 var gInBatchStack = 0;
    173 var gResetting = false;
    174 var gUndoResetting = false;
    175 
    176 /**
    177 * gBuildAreas maps area IDs to actual area nodes within browser windows.
    178 */
    179 var gBuildAreas = new Map();
    180 
    181 /**
    182 * gBuildWindows is a map of windows that have registered build areas, mapped
    183 * to a Set of known toolboxes in that window.
    184 */
    185 var gBuildWindows = new Map();
    186 
    187 var gNewElementCount = 0;
    188 var gGroupWrapperCache = new Map();
    189 var gSingleWrapperCache = new WeakMap();
    190 var gListeners = new Set();
    191 
    192 var gUIStateBeforeReset = {
    193  uiCustomizationState: null,
    194  drawInTitlebar: null,
    195  currentTheme: null,
    196  uiDensity: null,
    197  autoTouchMode: null,
    198  sidebarPositionStart: null,
    199 };
    200 
    201 /*
    202 * The current tab orientation: initially null until initialization,
    203 * true for vertical, false for horizontal
    204 */
    205 var gCurrentVerticalTabs = null;
    206 
    207 XPCOMUtils.defineLazyPreferenceGetter(
    208  lazy,
    209  "gDebuggingEnabled",
    210  kPrefCustomizationDebug,
    211  false,
    212  (pref, oldVal, newVal) => {
    213    if (typeof lazy.log != "undefined") {
    214      lazy.log.maxLogLevel = newVal ? "all" : "log";
    215    }
    216  }
    217 );
    218 
    219 XPCOMUtils.defineLazyPreferenceGetter(
    220  lazy,
    221  "resetPBMToolbarButtonEnabled",
    222  "browser.privatebrowsing.resetPBM.enabled",
    223  false
    224 );
    225 
    226 XPCOMUtils.defineLazyPreferenceGetter(
    227  lazy,
    228  "sidebarRevampEnabled",
    229  "sidebar.revamp",
    230  false,
    231  (pref, oldVal, newVal) => {
    232    if (!newVal) {
    233      return;
    234    }
    235    let navbarPlacements = CustomizableUI.getWidgetIdsInArea(
    236      CustomizableUI.AREA_NAVBAR
    237    );
    238    if (!navbarPlacements.includes("sidebar-button")) {
    239      CustomizableUI.addWidgetToArea(
    240        "sidebar-button",
    241        CustomizableUI.AREA_NAVBAR,
    242        Services.prefs.getBoolPref(kPrefSidebarPositionStartEnabled, true)
    243          ? 0
    244          : undefined // Adds to the end of navbar if position_start is false.
    245      );
    246    }
    247    // Ensure CUI knows to not restore this button if the user later removes it
    248    let prefId = "browser.toolbarbuttons.introduced.sidebar-button";
    249    Services.prefs.setBoolPref(prefId, true);
    250  }
    251 );
    252 
    253 XPCOMUtils.defineLazyPreferenceGetter(
    254  lazy,
    255  "verticalTabsPref",
    256  "sidebar.verticalTabs",
    257  false,
    258  (pref, oldVal, newVal) => {
    259    lazy.log.debug(
    260      `sidebar.verticalTabs change handler, calling updateTabStripOrientation with value: ${newVal}, gCurrentVerticalTabs: ${gCurrentVerticalTabs}`
    261    );
    262    CustomizableUIInternal.updateTabStripOrientation();
    263  }
    264 );
    265 
    266 XPCOMUtils.defineLazyPreferenceGetter(
    267  lazy,
    268  "horizontalPlacementsPref",
    269  kPrefCustomizationHorizontalTabstrip,
    270  ""
    271 );
    272 
    273 XPCOMUtils.defineLazyPreferenceGetter(
    274  lazy,
    275  "verticalPlacementsPref",
    276  kPrefCustomizationNavBarWhenVerticalTabs,
    277  ""
    278 );
    279 
    280 ChromeUtils.defineLazyGetter(lazy, "log", () => {
    281  let { ConsoleAPI } = ChromeUtils.importESModule(
    282    "resource://gre/modules/Console.sys.mjs"
    283  );
    284  let consoleOptions = {
    285    maxLogLevel: lazy.gDebuggingEnabled ? "all" : "log",
    286    prefix: "CustomizableUI",
    287  };
    288  return new ConsoleAPI(consoleOptions);
    289 });
    290 
    291 /**
    292 * This is the internal, private implementation of most of the CustomizableUI
    293 * API. This is intentionally not exported, but is instead called directly
    294 * from within this module via the exported CustomizableUI object, which
    295 * allows us to get a type of encapsulation.
    296 */
    297 var CustomizableUIInternal = {
    298  /**
    299   * Main entrypoint to initializing the CustomizableUI singleton. This is
    300   * called once the very first time this module is evaluated anywhere, via a
    301   * a call at the very end of this module file.
    302   *
    303   * This sets up observers, registers built-in widgets, and loads the saved
    304   * customization state from preferences, and performs any migration on that
    305   * loaded state.
    306   */
    307  initialize() {
    308    lazy.log.debug("Initializing");
    309 
    310    lazy.AddonManagerPrivate.databaseReady.then(async () => {
    311      lazy.AddonManager.addAddonListener(this);
    312 
    313      let addons = await lazy.AddonManager.getAddonsByTypes(["theme"]);
    314      gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID);
    315      gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme;
    316    });
    317 
    318    this.addListener(this);
    319    this.defineBuiltInWidgets();
    320    this.loadSavedState();
    321    this.updateForNewVersion();
    322    this.updateForNewProtonVersion();
    323    this.markObsoleteBuiltinButtonsSeen();
    324    this.updateForBaseBrowser();
    325    this.updateForTorBrowser();
    326 
    327    this.registerArea(
    328      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
    329      {
    330        type: CustomizableUI.TYPE_PANEL,
    331        defaultPlacements: [],
    332        anchor: "nav-bar-overflow-button",
    333      },
    334      true
    335    );
    336 
    337    this.registerArea(
    338      CustomizableUI.AREA_ADDONS,
    339      {
    340        type: CustomizableUI.TYPE_PANEL,
    341        defaultPlacements: [],
    342        anchor: "unified-extensions-button",
    343      },
    344      false
    345    );
    346 
    347    let navbarPlacements = [
    348      lazy.sidebarRevampEnabled ? "sidebar-button" : null,
    349      "back-button",
    350      "forward-button",
    351      "stop-reload-button",
    352      Services.policies.isAllowed("removeHomeButtonByDefault")
    353        ? null
    354        : "home-button",
    355      "vertical-spacer",
    356      "urlbar-container",
    357      // Don't want springs either side of the urlbar. tor-browser#41736
    358      // Base-browser additions tor-browser#41736. If you want to add to, remove
    359      // from, or rearrange this list, then bump the kVersionBaseBrowser and
    360      // update existing saved states in _updateForBaseBrowser.
    361      // Or if the change is only meant for tor-browser, bump kVersionTorBrowser
    362      // instead and update the existing saved states in _updateForTorBrowser.
    363      "security-level-button",
    364      "new-identity-button",
    365      "downloads-button",
    366      AppConstants.MOZ_DEV_EDITION ? "developer-button" : null,
    367      "fxa-toolbar-menu-button",
    368      lazy.resetPBMToolbarButtonEnabled ? "reset-pbm-toolbar-button" : null,
    369    ].filter(name => name);
    370 
    371    this.registerArea(
    372      CustomizableUI.AREA_NAVBAR,
    373      {
    374        type: CustomizableUI.TYPE_TOOLBAR,
    375        overflowable: true,
    376        defaultPlacements: navbarPlacements,
    377        verticalTabsDefaultPlacements: [
    378          "firefox-view-button",
    379          "alltabs-button",
    380        ],
    381        defaultCollapsed: false,
    382      },
    383      true
    384    );
    385    // navbarPlacements does not match the initial default XHTML layout.
    386    // Therefore we always need to rebuild the navbar area when
    387    // registerToolbarNode is called. tor-browser#41736
    388    gDirtyAreaCache.add(CustomizableUI.AREA_NAVBAR);
    389 
    390    if (!Services.appinfo.nativeMenubar) {
    391      this.registerArea(
    392        CustomizableUI.AREA_MENUBAR,
    393        {
    394          type: CustomizableUI.TYPE_TOOLBAR,
    395          defaultPlacements: ["menubar-items"],
    396          defaultCollapsed: true,
    397        },
    398        true
    399      );
    400    }
    401 
    402    this.registerArea(
    403      CustomizableUI.AREA_TABSTRIP,
    404      {
    405        type: CustomizableUI.TYPE_TOOLBAR,
    406        defaultPlacements: [
    407          "firefox-view-button",
    408          "tabbrowser-tabs",
    409          "new-tab-button",
    410          "alltabs-button",
    411        ],
    412        verticalTabsDefaultPlacements: [],
    413        defaultCollapsed: null,
    414      },
    415      true
    416    );
    417 
    418    this.registerArea(
    419      CustomizableUI.AREA_VERTICAL_TABSTRIP,
    420      {
    421        type: "toolbar",
    422        defaultPlacements: [],
    423        verticalTabsDefaultPlacements: ["tabbrowser-tabs"],
    424        defaultCollapsed: null,
    425      },
    426      true
    427    );
    428 
    429    this.registerArea(
    430      CustomizableUI.AREA_BOOKMARKS,
    431      {
    432        type: CustomizableUI.TYPE_TOOLBAR,
    433        defaultPlacements: ["personal-bookmarks"],
    434        defaultCollapsed: "newtab",
    435      },
    436      true
    437    );
    438    lazy.log.debug(`All the areas registered: ${[...gAreas.keys()]}`);
    439 
    440    // At initialization, if we find vertical tabs enabled but not sidebar.revamp
    441    // we'll enable revamp rather than disable vertical tabs.
    442    this.reconcileSidebarPrefs(kPrefSidebarVerticalTabsEnabled);
    443 
    444    this.initializeForTabsOrientation(CustomizableUI.verticalTabsEnabled);
    445 
    446    SearchWidgetTracker.init();
    447 
    448    Services.obs.addObserver(this, "browser-set-toolbar-visibility");
    449 
    450    Services.prefs.addObserver(kPrefSidebarVerticalTabsEnabled, this);
    451    Services.prefs.addObserver(kPrefSidebarRevampEnabled, this);
    452    Services.prefs.addObserver(kPrefSidebarPositionStartEnabled, this);
    453  },
    454 
    455  /**
    456   * Implements the onEnabled method for the AddonListener interface. Called
    457   * when an add-on is marked as enabled.
    458   *
    459   * @param {AddonInternal} addon
    460   *   The add-on that was enabled.
    461   */
    462  onEnabled(addon) {
    463    if (addon.type == "theme") {
    464      gSelectedTheme = addon;
    465    }
    466  },
    467 
    468  /**
    469   * Returns a new Set that contains the IDs of all built-in customizable areas.
    470   *
    471   * @type {Set<string>}
    472   */
    473  get builtinAreas() {
    474    return new Set([
    475      ...this.builtinToolbars,
    476      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
    477      CustomizableUI.AREA_ADDONS,
    478    ]);
    479  },
    480 
    481  /**
    482   * Returns a new Set that contains the IDs of all built-in customizable
    483   * toolbar areas.
    484   *
    485   * @type {Set<string>}
    486   */
    487  get builtinToolbars() {
    488    let toolbars = new Set([
    489      CustomizableUI.AREA_NAVBAR,
    490      CustomizableUI.AREA_BOOKMARKS,
    491      CustomizableUI.AREA_TABSTRIP,
    492    ]);
    493    if (AppConstants.platform != "macosx") {
    494      toolbars.add(CustomizableUI.AREA_MENUBAR);
    495    }
    496    return toolbars;
    497  },
    498 
    499  /**
    500   * Goes through the list of widgets defined in CustomizableWidgets and
    501   * registers them with CustomizableUI.
    502   */
    503  defineBuiltInWidgets() {
    504    for (let widgetDefinition of lazy.CustomizableWidgets) {
    505      this.createBuiltinWidget(widgetDefinition);
    506    }
    507  },
    508 
    509  /**
    510   * Runs any necessary migrations on the current saved customization state
    511   * to get us to a kVersion compatible state.
    512   */
    513  // eslint-disable-next-line complexity
    514  updateForNewVersion() {
    515    // We should still enter even if gSavedState.currentVersion >= kVersion
    516    // because the per-widget pref facility is independent of versioning.
    517    if (!gSavedState) {
    518      // Flip all the prefs so we don't try to re-introduce later:
    519      for (let [, widget] of gPalette) {
    520        if (widget.defaultArea && widget._introducedInVersion === "pref") {
    521          let prefId = "browser.toolbarbuttons.introduced." + widget.id;
    522          Services.prefs.setBoolPref(prefId, true);
    523        }
    524      }
    525      return;
    526    }
    527 
    528    let currentVersion = gSavedState.currentVersion;
    529    for (let [id, widget] of gPalette) {
    530      if (widget.defaultArea) {
    531        let shouldAdd = false;
    532        let shouldSetPref = false;
    533        let prefId = "browser.toolbarbuttons.introduced." + widget.id;
    534        if (widget._introducedInVersion === "pref") {
    535          try {
    536            shouldAdd = !Services.prefs.getBoolPref(prefId);
    537          } catch (ex) {
    538            // Pref doesn't exist:
    539            shouldAdd = true;
    540          }
    541          shouldSetPref = shouldAdd;
    542        } else if (widget._introducedInVersion > currentVersion) {
    543          shouldAdd = true;
    544        } else if (
    545          widget._introducedByPref &&
    546          Services.prefs.getBoolPref(widget._introducedByPref)
    547        ) {
    548          shouldSetPref = shouldAdd = !Services.prefs.getBoolPref(
    549            prefId,
    550            false
    551          );
    552        }
    553 
    554        if (shouldAdd) {
    555          let futurePlacements = gFuturePlacements.get(widget.defaultArea);
    556          if (futurePlacements) {
    557            futurePlacements.add(id);
    558          } else {
    559            gFuturePlacements.set(widget.defaultArea, new Set([id]));
    560          }
    561          if (shouldSetPref) {
    562            Services.prefs.setBoolPref(prefId, true);
    563          }
    564        }
    565      }
    566    }
    567 
    568    // Nothing to migrate now if we don't have placements.
    569    if (!gSavedState.placements) {
    570      return;
    571    }
    572 
    573    if (
    574      currentVersion < 7 &&
    575      gSavedState.placements[CustomizableUI.AREA_NAVBAR]
    576    ) {
    577      let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    578      let newPlacements = [
    579        "back-button",
    580        "forward-button",
    581        "stop-reload-button",
    582        "home-button",
    583      ];
    584      for (let button of placements) {
    585        if (!newPlacements.includes(button)) {
    586          newPlacements.push(button);
    587        }
    588      }
    589 
    590      if (!newPlacements.includes("sidebar-button")) {
    591        newPlacements.unshift("sidebar-button");
    592      }
    593 
    594      gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements;
    595    }
    596 
    597    if (currentVersion < 8 && gSavedState.placements["PanelUI-contents"]) {
    598      let savedPanelPlacements = gSavedState.placements["PanelUI-contents"];
    599      delete gSavedState.placements["PanelUI-contents"];
    600      let defaultPlacements = [
    601        "edit-controls",
    602        "zoom-controls",
    603        "new-window-button",
    604        "privatebrowsing-button",
    605        "save-page-button",
    606        "print-button",
    607        "history-panelmenu",
    608        "fullscreen-button",
    609        "find-button",
    610        "preferences-button",
    611        // This widget no longer exists as of 2023, see Bug 1799009.
    612        "add-ons-button",
    613        "sync-button",
    614      ];
    615 
    616      if (!AppConstants.MOZ_DEV_EDITION) {
    617        defaultPlacements.splice(-1, 0, "developer-button");
    618      }
    619 
    620      savedPanelPlacements = savedPanelPlacements.filter(
    621        id => !defaultPlacements.includes(id)
    622      );
    623 
    624      if (savedPanelPlacements.length) {
    625        gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] =
    626          savedPanelPlacements;
    627      }
    628    }
    629 
    630    if (currentVersion < 9 && gSavedState.placements["nav-bar"]) {
    631      let placements = gSavedState.placements["nav-bar"];
    632      if (placements.includes("urlbar-container")) {
    633        let urlbarIndex = placements.indexOf("urlbar-container");
    634        let secondSpringIndex = urlbarIndex + 1;
    635        // Insert if there isn't already a spring before the urlbar
    636        if (
    637          urlbarIndex == 0 ||
    638          !placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring")
    639        ) {
    640          placements.splice(urlbarIndex, 0, "spring");
    641          // The url bar is now 1 index later, so increment the insertion point for
    642          // the second spring.
    643          secondSpringIndex++;
    644        }
    645        // If the search container is present, insert after the search container
    646        // instead of after the url bar
    647        let searchContainerIndex = placements.indexOf("search-container");
    648        if (searchContainerIndex != -1) {
    649          secondSpringIndex = searchContainerIndex + 1;
    650        }
    651        if (
    652          secondSpringIndex == placements.length ||
    653          !placements[secondSpringIndex].startsWith(
    654            kSpecialWidgetPfx + "spring"
    655          )
    656        ) {
    657          placements.splice(secondSpringIndex, 0, "spring");
    658        }
    659      }
    660 
    661      // Finally, replace the bookmarks menu button with the library one if present
    662      if (placements.includes("bookmarks-menu-button")) {
    663        let bmbIndex = placements.indexOf("bookmarks-menu-button");
    664        placements.splice(bmbIndex, 1);
    665        let downloadButtonIndex = placements.indexOf("downloads-button");
    666        let libraryIndex =
    667          downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1;
    668        placements.splice(libraryIndex, 0, "library-button");
    669      }
    670    }
    671 
    672    if (currentVersion < 10) {
    673      for (let placements of Object.values(gSavedState.placements)) {
    674        if (placements.includes("webcompat-reporter-button")) {
    675          placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
    676          break;
    677        }
    678      }
    679    }
    680 
    681    // Move the downloads button to the default position in the navbar if it's
    682    // not there already.
    683    if (currentVersion < 11) {
    684      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    685      // First remove from wherever it currently lives, if anywhere:
    686      for (let placements of Object.values(gSavedState.placements)) {
    687        let existingIndex = placements.indexOf("downloads-button");
    688        if (existingIndex != -1) {
    689          placements.splice(existingIndex, 1);
    690          break; // It can only be in 1 place, so no point looking elsewhere.
    691        }
    692      }
    693 
    694      // Now put the button in the navbar in the correct spot:
    695      if (navbarPlacements) {
    696        let insertionPoint = navbarPlacements.indexOf("urlbar-container");
    697        // Deliberately iterate to 1 past the end of the array to insert at the
    698        // end if need be.
    699        while (++insertionPoint < navbarPlacements.length) {
    700          let widget = navbarPlacements[insertionPoint];
    701          // If we find a non-searchbar, non-spacer node, break out of the loop:
    702          if (
    703            widget != "search-container" &&
    704            !this.matchingSpecials(widget, "spring")
    705          ) {
    706            break;
    707          }
    708        }
    709        // We either found the right spot, or reached the end of the
    710        // placements, so insert here:
    711        navbarPlacements.splice(insertionPoint, 0, "downloads-button");
    712      }
    713    }
    714 
    715    if (currentVersion < 12) {
    716      const removedButtons = [
    717        "loop-call-button",
    718        "loop-button-throttled",
    719        "pocket-button",
    720      ];
    721      for (let placements of Object.values(gSavedState.placements)) {
    722        for (let button of removedButtons) {
    723          let buttonIndex = placements.indexOf(button);
    724          if (buttonIndex != -1) {
    725            placements.splice(buttonIndex, 1);
    726          }
    727        }
    728      }
    729    }
    730 
    731    // Remove the old placements from the now-gone Nightly-only
    732    // "New non-e10s window" button.
    733    if (currentVersion < 13) {
    734      for (let placements of Object.values(gSavedState.placements)) {
    735        let buttonIndex = placements.indexOf("e10s-button");
    736        if (buttonIndex != -1) {
    737          placements.splice(buttonIndex, 1);
    738        }
    739      }
    740    }
    741 
    742    // Remove unsupported custom toolbar saved placements
    743    if (currentVersion < 14) {
    744      for (let area in gSavedState.placements) {
    745        if (!this.builtinAreas.has(area)) {
    746          delete gSavedState.placements[area];
    747        }
    748      }
    749    }
    750 
    751    // Add the FxA toolbar menu as the right most button item
    752    if (currentVersion < 16) {
    753      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    754      // Place the menu item as the first item to the left of the hamburger menu
    755      if (navbarPlacements) {
    756        navbarPlacements.push("fxa-toolbar-menu-button");
    757      }
    758    }
    759 
    760    // Add firefox-view if not present
    761    if (currentVersion < 18) {
    762      let tabstripPlacements =
    763        gSavedState.placements[CustomizableUI.AREA_TABSTRIP];
    764      if (
    765        tabstripPlacements &&
    766        !tabstripPlacements.includes("firefox-view-button")
    767      ) {
    768        tabstripPlacements.unshift("firefox-view-button");
    769      }
    770    }
    771 
    772    // Unified Extensions addon button migration, which puts any browser action
    773    // buttons in the overflow menu into the addons panel instead.
    774    if (currentVersion < 19) {
    775      let overflowPlacements =
    776        gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] || [];
    777      // The most likely case is that there are no AREA_ADDONS placements, in which case the
    778      // array won't exist.
    779      let addonsPlacements =
    780        gSavedState.placements[CustomizableUI.AREA_ADDONS] || [];
    781 
    782      // Migration algorithm for transitioning to Unified Extensions:
    783      //
    784      // 1. Create two arrays, one for extension widgets, one for built-in widgets.
    785      // 2. Iterate all items in the overflow panel, and push them into the
    786      //    appropriate array based on whether or not its an extension widget.
    787      // 3. Overwrite the overflow panel placements with the built-in widgets array.
    788      // 4. Prepend the extension widgets to the addonsPlacements array. Note that this
    789      //    does not overwrite this array as a precaution because it's possible
    790      //    (though pretty unlikely) that some widgets are already there.
    791      //
    792      // For extension widgets that were in the palette, they will be appended to the
    793      // addons area when they're created within createWidget.
    794      let extWidgets = [];
    795      let builtInWidgets = [];
    796      for (let widgetId of overflowPlacements) {
    797        if (CustomizableUI.isWebExtensionWidget(widgetId)) {
    798          extWidgets.push(widgetId);
    799        } else {
    800          builtInWidgets.push(widgetId);
    801        }
    802      }
    803      gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] =
    804        builtInWidgets;
    805      gSavedState.placements[CustomizableUI.AREA_ADDONS] = [
    806        ...extWidgets,
    807        ...addonsPlacements,
    808      ];
    809    }
    810 
    811    // Add the PBM reset button as the right most button item
    812    if (currentVersion < 20) {
    813      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    814      // Place the button as the first item to the left of the hamburger menu
    815      if (
    816        navbarPlacements &&
    817        !navbarPlacements.includes("reset-pbm-toolbar-button")
    818      ) {
    819        navbarPlacements.push("reset-pbm-toolbar-button");
    820      }
    821    }
    822 
    823    if (currentVersion < 21) {
    824      // If the vertical-spacer has not yet been added, ensure its to the left of the urlbar initially
    825      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    826      if (!navbarPlacements.includes("vertical-spacer")) {
    827        let urlbarContainerPosition =
    828          navbarPlacements.indexOf("urlbar-container");
    829        gSavedState.placements[CustomizableUI.AREA_NAVBAR].splice(
    830          urlbarContainerPosition - 1,
    831          0,
    832          "vertical-spacer"
    833        );
    834      }
    835    }
    836 
    837    if (currentVersion < 22) {
    838      if (!Services.prefs.getBoolPref(kPrefSidebarPositionStartEnabled, true)) {
    839        // If the sidebar is on the right, the toolbar button is also on the right.
    840        const navbarPlacements =
    841          gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    842        if (navbarPlacements[0] === "sidebar-button") {
    843          navbarPlacements.shift();
    844          navbarPlacements.push("sidebar-button");
    845        }
    846      }
    847    }
    848 
    849    if (currentVersion < 23) {
    850      const navbarPlacements =
    851        gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    852 
    853      let buttonIndex = navbarPlacements.indexOf("save-to-pocket-button");
    854      if (buttonIndex != -1) {
    855        navbarPlacements.splice(buttonIndex, 1);
    856      }
    857    }
    858  },
    859 
    860  /**
    861   * A separate state migration method for when we introduced the Proton
    862   * retheme in 2021. Because the Proton retheme was toggle-able via a pref,
    863   * it was important to have the migration separated out. Like most old
    864   * migrations, this is probably a historical artifact at this point.
    865   */
    866  updateForNewProtonVersion() {
    867    const VERSION = 3;
    868    let currentVersion = Services.prefs.getIntPref(
    869      kPrefProtonToolbarVersion,
    870      0
    871    );
    872    if (currentVersion >= VERSION) {
    873      return;
    874    }
    875 
    876    let placements = gSavedState?.placements?.[CustomizableUI.AREA_NAVBAR];
    877 
    878    if (!placements) {
    879      // The profile was created with this version, so no need to migrate.
    880      Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION);
    881      return;
    882    }
    883 
    884    // Remove the home button if it hasn't been used and is set to about:home
    885    if (currentVersion < 1) {
    886      let homePage = lazy.HomePage.get();
    887      if (
    888        placements.includes("home-button") &&
    889        !Services.prefs.getBoolPref(kPrefHomeButtonUsed) &&
    890        (homePage == "about:home" || homePage == "about:blank") &&
    891        Services.policies.isAllowed("removeHomeButtonByDefault")
    892      ) {
    893        placements.splice(placements.indexOf("home-button"), 1);
    894      }
    895    }
    896 
    897    // Remove the library button if it hasn't been used
    898    if (currentVersion < 2) {
    899      if (
    900        placements.includes("library-button") &&
    901        !Services.prefs.getBoolPref(kPrefLibraryButtonUsed)
    902      ) {
    903        placements.splice(placements.indexOf("library-button"), 1);
    904      }
    905    }
    906 
    907    // Remove the library button if it hasn't been used
    908    if (currentVersion < 3) {
    909      if (
    910        placements.includes("sidebar-button") &&
    911        !Services.prefs.getBoolPref(kPrefSidebarButtonUsed)
    912      ) {
    913        placements.splice(placements.indexOf("sidebar-button"), 1);
    914      }
    915    }
    916 
    917    Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION);
    918  },
    919 
    920  /**
    921   * When upgrading, checks to see if any built-in button definitions were
    922   * just removed / marked obsolete (see ObsoleteBuiltinButtons) for the
    923   * current kVersion. If so, marks those buttons as seen.
    924   */
    925  markObsoleteBuiltinButtonsSeen() {
    926    if (!gSavedState) {
    927      return;
    928    }
    929    let currentVersion = gSavedState.currentVersion;
    930    if (currentVersion >= kVersion) {
    931      return;
    932    }
    933    // we're upgrading, update state if necessary
    934    for (let id in ObsoleteBuiltinButtons) {
    935      let version = ObsoleteBuiltinButtons[id];
    936      if (version == kVersion) {
    937        gSeenWidgets.add(id);
    938        gDirty = true;
    939      }
    940    }
    941  },
    942 
    943  updateForBaseBrowser() {
    944    if (!gSavedState) {
    945      // Use the defaults.
    946      return;
    947    }
    948 
    949    const currentVersion = gSavedState.currentVersionBaseBrowser;
    950 
    951    if (currentVersion < 1) {
    952      // NOTE: In base-browser/tor-browser version 12.5a5, and earlier, the
    953      // toolbar was configured by setting the full JSON string for the default
    954      // "browser.uiCustomization.state" preference value. The disadvantage is
    955      // that we could not update this value in a way that existing users (who
    956      // would have non-default preference values) would also get the desired
    957      // change (e.g. for adding or removing a button).
    958      //
    959      // With tor-browser#41736 we want to switch to changing the toolbar
    960      // dynamically like firefox. Therefore, this first version transfer simply
    961      // gets the toolbar into the same state we wanted before, away from the
    962      // default firefox state.
    963      //
    964      // If an existing user state aligned with the previous default
    965      // "browser.uiCustomization.state" then this shouldn't visibly change
    966      // anything.
    967      // If a user explicitly customized the toolbar to go back to the firefox
    968      // default, then this may undo those changes.
    969      const navbarPlacements =
    970        gSavedState.placements[CustomizableUI.AREA_NAVBAR];
    971      if (navbarPlacements) {
    972        const getBeforeAfterUrlbar = () => {
    973          // NOTE: The urlbar is non-removable from the navbar, so should have
    974          // an index.
    975          const index = navbarPlacements.indexOf("urlbar-container");
    976          let after = index + 1;
    977          if (
    978            after < navbarPlacements.length &&
    979            navbarPlacements[after] === "search-container"
    980          ) {
    981            // Skip past the search-container.
    982            after++;
    983          }
    984          return { before: index - 1, after };
    985        };
    986 
    987        // Remove the urlbar springs either side of the urlbar.
    988        const { before, after } = getBeforeAfterUrlbar();
    989        if (
    990          after < navbarPlacements.length &&
    991          this.matchingSpecials(navbarPlacements[after], "spring")
    992        ) {
    993          // Remove the spring after.
    994          navbarPlacements.splice(after, 1);
    995          // NOTE: The `before` index does not change.
    996        }
    997        if (
    998          before >= 0 &&
    999          this.matchingSpecials(navbarPlacements[before], "spring")
   1000        ) {
   1001          // Remove the spring before.
   1002          navbarPlacements.splice(before, 1);
   1003        }
   1004 
   1005        // Make sure the security-level-button and new-identity-button appears
   1006        // in the toolbar.
   1007        for (const id of ["new-identity-button", "security-level-button"]) {
   1008          let alreadyAdded = false;
   1009          for (const placements of Object.values(gSavedState.placements)) {
   1010            if (placements.includes(id)) {
   1011              alreadyAdded = true;
   1012              break;
   1013            }
   1014          }
   1015          if (alreadyAdded) {
   1016            continue;
   1017          }
   1018 
   1019          // Add to the nav-bar, after the urlbar-container.
   1020          // NOTE: We have already removed the spring after the urlbar.
   1021          navbarPlacements.splice(getBeforeAfterUrlbar().after, 0, id);
   1022        }
   1023      }
   1024 
   1025      // Remove save-to-pocket-button. See tor-browser#18886 and
   1026      // tor-browser#31602.
   1027      for (const placements of Object.values(gSavedState.placements)) {
   1028        let buttonIndex = placements.indexOf("save-to-pocket-button");
   1029        if (buttonIndex != -1) {
   1030          placements.splice(buttonIndex, 1);
   1031        }
   1032      }
   1033 
   1034      // Remove unused fields that used to be part of
   1035      // "browser.uiCustomization.state".
   1036      delete gSavedState.placements["PanelUI-contents"];
   1037      delete gSavedState.placements["addon-bar"];
   1038    }
   1039 
   1040    if (currentVersion < 2) {
   1041      // Matches against kVersion 19, i.e. when the unified-extensions-button
   1042      // was introduced and extensions were moved from the palette to
   1043      // AREA_ADDONS.
   1044      // For base browser, we want the NoScript addon to be moved from the
   1045      // default palette to AREA_NAVBAR, so that if it becomes shown through the
   1046      // preference extensions.hideNoScript it will appear in the toolbar.
   1047      // If the NoScript addon is already in AREA_NAVBAR, we instead flip the
   1048      // extensions.hideNoScript preference so that it remains visible.
   1049      // See tor-browser#41581.
   1050      const navbarPlacements =
   1051        gSavedState.placements[CustomizableUI.AREA_NAVBAR];
   1052      if (navbarPlacements) {
   1053        let noScriptVisible = false;
   1054        for (const [area, placements] of Object.entries(
   1055          gSavedState.placements
   1056        )) {
   1057          const index = placements.indexOf(NoScriptId);
   1058          if (index === -1) {
   1059            continue;
   1060          }
   1061          if (area === CustomizableUI.AREA_ADDONS) {
   1062            // Has been placed in the ADDONS area.
   1063            // Most likely, this is an alpha or nightly user who received the
   1064            // firefox update in a run before this one. In this case, we want to
   1065            // match the same behaviour as a stable user: hide the button and
   1066            // move it to the NAVBAR instead.
   1067            placements.splice(index, 1);
   1068          } else {
   1069            // It is in an area other than the ADDON (and not in the palette).
   1070            noScriptVisible = true;
   1071          }
   1072        }
   1073        if (noScriptVisible) {
   1074          // Keep the button where it is and make sure it is visible.
   1075          Services.prefs.setBoolPref("extensions.hideNoScript", false);
   1076        } else {
   1077          // Should appear just before unified-extensions-button, which is
   1078          // currently not part of the default placements.
   1079          const placeIndex = navbarPlacements.indexOf(
   1080            "unified-extensions-button"
   1081          );
   1082          if (placeIndex === -1) {
   1083            navbarPlacements.push(NoScriptId);
   1084          } else {
   1085            navbarPlacements.splice(placeIndex, 0, NoScriptId);
   1086          }
   1087        }
   1088      }
   1089    }
   1090  },
   1091 
   1092  updateForTorBrowser() {
   1093    if (!gSavedState) {
   1094      // Use the defaults.
   1095      return;
   1096    }
   1097 
   1098    const currentVersion = gSavedState.currentVersionTorBrowser;
   1099 
   1100    if (currentVersion < 1) {
   1101      // Remove torbutton-button, which no longer exists.
   1102      for (const placements of Object.values(gSavedState.placements)) {
   1103        let buttonIndex = placements.indexOf("torbutton-button");
   1104        if (buttonIndex != -1) {
   1105          placements.splice(buttonIndex, 1);
   1106        }
   1107      }
   1108    }
   1109  },
   1110 
   1111  /**
   1112   * If a new area was defined, or new default widgets for an area are defined,
   1113   * this reconciles the placements of those new default widgets with the
   1114   * existing customization and toolbar state of the browser.
   1115   *
   1116   * @param {string} aArea
   1117   *   The ID of the area to reconcile the default widget state for.
   1118   */
   1119  placeNewDefaultWidgetsInArea(aArea) {
   1120    let futurePlacedWidgets = gFuturePlacements.get(aArea);
   1121    let savedPlacements =
   1122      gSavedState && gSavedState.placements && gSavedState.placements[aArea];
   1123    let defaultPlacements;
   1124    if (
   1125      CustomizableUI.verticalTabsEnabled &&
   1126      gAreas.get(aArea).has("verticalTabsDefaultPlacements")
   1127    ) {
   1128      defaultPlacements = gAreas
   1129        .get(aArea)
   1130        .get("verticalTabsDefaultPlacements");
   1131    } else {
   1132      defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
   1133    }
   1134    if (
   1135      !savedPlacements ||
   1136      !savedPlacements.length ||
   1137      !futurePlacedWidgets ||
   1138      !defaultPlacements ||
   1139      !defaultPlacements.length
   1140    ) {
   1141      return;
   1142    }
   1143    let defaultWidgetIndex = -1;
   1144 
   1145    for (let widgetId of futurePlacedWidgets) {
   1146      let widget = gPalette.get(widgetId);
   1147      if (
   1148        !widget ||
   1149        widget.source !== CustomizableUI.SOURCE_BUILTIN ||
   1150        !widget.defaultArea ||
   1151        !(widget._introducedInVersion || widget._introducedByPref) ||
   1152        savedPlacements.includes(widget.id)
   1153      ) {
   1154        continue;
   1155      }
   1156      defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
   1157      if (defaultWidgetIndex === -1) {
   1158        continue;
   1159      }
   1160      // Now we know that this widget should be here by default, was newly introduced,
   1161      // and we have a saved state to insert into, and a default state to work off of.
   1162      // Try introducing after widgets that come before it in the default placements:
   1163      for (let i = defaultWidgetIndex; i >= 0; i--) {
   1164        // Special case: if the defaults list this widget as coming first, insert at the beginning:
   1165        if (i === 0 && i === defaultWidgetIndex) {
   1166          savedPlacements.splice(0, 0, widget.id);
   1167          // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
   1168          // safe, and we won't skip any items.
   1169          futurePlacedWidgets.delete(widget.id);
   1170          gDirty = true;
   1171          break;
   1172        }
   1173        // Otherwise, if we're somewhere other than the beginning, check if the previous
   1174        // widget is in the saved placements.
   1175        if (i) {
   1176          let previousWidget = defaultPlacements[i - 1];
   1177          let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
   1178          if (previousWidgetIndex != -1) {
   1179            savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
   1180            futurePlacedWidgets.delete(widget.id);
   1181            gDirty = true;
   1182            break;
   1183          }
   1184        }
   1185      }
   1186      // The loop above either inserts the item or doesn't - either way, we can get away
   1187      // with doing nothing else now; if the item remains in gFuturePlacements, we'll
   1188      // add it at the end in restoreStateForArea.
   1189    }
   1190    this.saveState();
   1191  },
   1192 
   1193  /**
   1194   * @see CustomizableUI.getCustomizationTarget
   1195   * @param {Element|null} aElement
   1196   * @returns {Element|null}
   1197   */
   1198  getCustomizationTarget(aElement) {
   1199    if (!aElement) {
   1200      return null;
   1201    }
   1202 
   1203    if (
   1204      !aElement._customizationTarget &&
   1205      aElement.hasAttribute("customizable")
   1206    ) {
   1207      let id = aElement.getAttribute("customizationtarget");
   1208      if (id) {
   1209        aElement._customizationTarget =
   1210          aElement.ownerDocument.getElementById(id);
   1211      }
   1212 
   1213      if (!aElement._customizationTarget) {
   1214        aElement._customizationTarget = aElement;
   1215      }
   1216    }
   1217 
   1218    return aElement._customizationTarget;
   1219  },
   1220 
   1221  /**
   1222   * Given a customizable widget ID, creates and returns a WidgetGroupWrapper
   1223   * or a XULGroupWrapper for that widget (depending on how the widget is
   1224   * provided). This wrapper is then cached and returned for future calls
   1225   * for that same widget ID.
   1226   *
   1227   * If the customizable widget ID cannot be resolved to a particular provider,
   1228   * null is returned.
   1229   *
   1230   * @param {string} aWidgetId
   1231   *   The ID of the customizable widget to get the WidgetGroupWrapper /
   1232   *   XULGroupWrapper for.
   1233   * @returns {WidgetGroupWrapper|XULGroupWrapper|null}
   1234   *   The appropriate GroupWrapper for the widget, or null if no such wrapper
   1235   *   can be found.
   1236   */
   1237  wrapWidget(aWidgetId) {
   1238    if (gGroupWrapperCache.has(aWidgetId)) {
   1239      return gGroupWrapperCache.get(aWidgetId);
   1240    }
   1241 
   1242    let provider = this.getWidgetProvider(aWidgetId);
   1243    if (!provider) {
   1244      return null;
   1245    }
   1246 
   1247    if (provider == CustomizableUI.PROVIDER_API) {
   1248      let widget = gPalette.get(aWidgetId);
   1249      if (!widget.wrapper) {
   1250        widget.wrapper = new WidgetGroupWrapper(widget);
   1251        gGroupWrapperCache.set(aWidgetId, widget.wrapper);
   1252      }
   1253      return widget.wrapper;
   1254    }
   1255 
   1256    // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
   1257    // XXXgijs: this causes bugs in code that depends on widgetWrapper.provider
   1258    // giving an accurate answer... filed as bug 1379821
   1259    let wrapper = new XULWidgetGroupWrapper(aWidgetId);
   1260    gGroupWrapperCache.set(aWidgetId, wrapper);
   1261    return wrapper;
   1262  },
   1263 
   1264  /**
   1265   * @see CustomizableUI.registerArea
   1266   * @param {string} aName
   1267   * @param {object} aProperties
   1268   * @param {boolean} aInternalCaller
   1269   */
   1270  registerArea(aName, aProperties, aInternalCaller) {
   1271    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
   1272      throw new Error("Invalid area name");
   1273    }
   1274 
   1275    let areaIsKnown = gAreas.has(aName);
   1276    let props = areaIsKnown ? gAreas.get(aName) : new Map();
   1277    const kImmutableProperties = new Set(["type", "overflowable"]);
   1278    for (let key in aProperties) {
   1279      if (
   1280        areaIsKnown &&
   1281        kImmutableProperties.has(key) &&
   1282        props.get(key) != aProperties[key]
   1283      ) {
   1284        throw new Error("An area cannot change the property for '" + key + "'");
   1285      }
   1286      props.set(key, aProperties[key]);
   1287    }
   1288    // Default to a toolbar:
   1289    if (!props.has("type")) {
   1290      props.set("type", CustomizableUI.TYPE_TOOLBAR);
   1291    }
   1292    if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
   1293      // Check aProperties instead of props because this check is only interested
   1294      // in the passed arguments, not the state of a potentially pre-existing area.
   1295      if (!aInternalCaller && aProperties.defaultCollapsed) {
   1296        throw new Error(
   1297          "defaultCollapsed is only allowed for default toolbars."
   1298        );
   1299      }
   1300      if (!props.has("defaultCollapsed")) {
   1301        props.set("defaultCollapsed", true);
   1302      }
   1303    } else if (props.has("defaultCollapsed")) {
   1304      throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
   1305    }
   1306    // Sanity check type:
   1307    let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_PANEL];
   1308    if (!allTypes.includes(props.get("type"))) {
   1309      throw new Error("Invalid area type " + props.get("type"));
   1310    }
   1311 
   1312    // And to no placements:
   1313    if (!props.has("defaultPlacements")) {
   1314      props.set("defaultPlacements", []);
   1315    }
   1316    // Sanity check default placements array:
   1317    if (!Array.isArray(props.get("defaultPlacements"))) {
   1318      throw new Error("Should provide an array of default placements");
   1319    }
   1320 
   1321    if (!areaIsKnown) {
   1322      gAreas.set(aName, props);
   1323 
   1324      // Reconcile new default widgets. Have to do this before we start restoring things.
   1325      this.placeNewDefaultWidgetsInArea(aName);
   1326 
   1327      if (
   1328        props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
   1329        !gPlacements.has(aName)
   1330      ) {
   1331        lazy.log.debug(
   1332          `registerArea ${aName}, no gPlacements yet, nothing to restore`
   1333        );
   1334        // Guarantee this area exists in gFuturePlacements, to avoid checking it in
   1335        // various places elsewhere.
   1336        if (!gFuturePlacements.has(aName)) {
   1337          gFuturePlacements.set(aName, new Set());
   1338        }
   1339      } else {
   1340        this.restoreStateForArea(aName);
   1341      }
   1342 
   1343      // If we have pending build area nodes, register all of them
   1344      if (gPendingBuildAreas.has(aName)) {
   1345        let pendingNodes = gPendingBuildAreas.get(aName);
   1346        for (let pendingNode of pendingNodes) {
   1347          this.registerToolbarNode(pendingNode);
   1348        }
   1349        gPendingBuildAreas.delete(aName);
   1350      }
   1351    }
   1352  },
   1353 
   1354  /**
   1355   * @see CustomizableUI.unregisterArea
   1356   * @param {string} aName
   1357   * @param {boolean} [aDestroyPlacements]
   1358   */
   1359  unregisterArea(aName, aDestroyPlacements) {
   1360    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
   1361      throw new Error("Invalid area name");
   1362    }
   1363    if (!gAreas.has(aName) && !gPlacements.has(aName)) {
   1364      throw new Error("Area not registered");
   1365    }
   1366 
   1367    // Move all the widgets out
   1368    this.beginBatchUpdate();
   1369    try {
   1370      let placements = gPlacements.get(aName);
   1371      if (placements) {
   1372        // Need to clone this array so removeWidgetFromArea doesn't modify it
   1373        placements = [...placements];
   1374        placements.forEach(this.removeWidgetFromArea, this);
   1375      }
   1376 
   1377      // Delete all remaining traces.
   1378      gAreas.delete(aName);
   1379      // Only destroy placements when necessary:
   1380      if (aDestroyPlacements) {
   1381        gPlacements.delete(aName);
   1382      } else {
   1383        // Otherwise we need to re-set them, as removeFromArea will have emptied
   1384        // them out:
   1385        gPlacements.set(aName, placements);
   1386      }
   1387      gFuturePlacements.delete(aName);
   1388      let existingAreaNodes = gBuildAreas.get(aName);
   1389      if (existingAreaNodes) {
   1390        for (let areaNode of existingAreaNodes) {
   1391          this.notifyListeners(
   1392            "onAreaNodeUnregistered",
   1393            aName,
   1394            this.getCustomizationTarget(areaNode),
   1395            CustomizableUI.REASON_AREA_UNREGISTERED
   1396          );
   1397        }
   1398      }
   1399      gBuildAreas.delete(aName);
   1400    } finally {
   1401      this.endBatchUpdate(true);
   1402    }
   1403  },
   1404 
   1405  /**
   1406   * @see CustomizableUI.registerToolbarNode
   1407   * @param {Element} aToolbar
   1408   */
   1409  registerToolbarNode(aToolbar) {
   1410    let area = aToolbar.id;
   1411    if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
   1412      return;
   1413    }
   1414    let areaProperties = gAreas.get(area);
   1415 
   1416    // If this area is not registered, try to do it automatically:
   1417    if (!areaProperties) {
   1418      if (!gPendingBuildAreas.has(area)) {
   1419        gPendingBuildAreas.set(area, []);
   1420      }
   1421      gPendingBuildAreas.get(area).push(aToolbar);
   1422      return;
   1423    }
   1424 
   1425    this.beginBatchUpdate();
   1426    try {
   1427      let placements = gPlacements.get(area);
   1428      if (
   1429        !placements &&
   1430        areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR
   1431      ) {
   1432        this.restoreStateForArea(area);
   1433        placements = gPlacements.get(area);
   1434      }
   1435 
   1436      // For toolbars that need it, mark as dirty.
   1437      let defaultPlacements = areaProperties.get("defaultPlacements");
   1438      if (
   1439        !this.builtinToolbars.has(area) ||
   1440        placements.length != defaultPlacements.length ||
   1441        !placements.every((id, i) => id == defaultPlacements[i])
   1442      ) {
   1443        gDirtyAreaCache.add(area);
   1444      }
   1445 
   1446      if (areaProperties.get("overflowable")) {
   1447        aToolbar.overflowable = new OverflowableToolbar(aToolbar);
   1448      }
   1449 
   1450      this.registerBuildArea(area, aToolbar);
   1451 
   1452      // We only build the toolbar if it's been marked as "dirty". Dirty means
   1453      // one of the following things:
   1454      // 1) Items have been added, moved or removed from this toolbar before.
   1455      // 2) The number of children of the toolbar does not match the length of
   1456      //    the placements array for that area.
   1457      //
   1458      // This notion of being "dirty" is stored in a cache which is persisted
   1459      // in the saved state.
   1460      //
   1461      // Secondly, if the list of placements contains an API-provided widget,
   1462      // we need to call `buildArea` or it won't be built and put in the toolbar.
   1463      if (
   1464        gDirtyAreaCache.has(area) ||
   1465        placements.some(id => gPalette.has(id))
   1466      ) {
   1467        this.buildArea(area, placements, aToolbar);
   1468      } else {
   1469        // We must have a builtin toolbar that's in the default state. We need
   1470        // to only make sure that all the special nodes are correct.
   1471        let specials = placements.filter(p => this.isSpecialWidget(p));
   1472        if (specials.length) {
   1473          this.updateSpecialsForBuiltinToolbar(aToolbar, specials);
   1474        }
   1475      }
   1476      this.notifyListeners(
   1477        "onAreaNodeRegistered",
   1478        area,
   1479        this.getCustomizationTarget(aToolbar)
   1480      );
   1481    } finally {
   1482      lazy.log.debug(
   1483        `registerToolbarNode for ${area}, tabstripAreasReady? ${this.tabstripAreasReady}`
   1484      );
   1485      this.endBatchUpdate();
   1486    }
   1487  },
   1488 
   1489  /**
   1490   * For each "special" node in a toolbar (any spring, spacer, separator, or
   1491   * element with an ID prefixed with kSpecialWidgetPfx), assigns the unique
   1492   * ID from the saved state to the DOM nodes. These unique IDs are things like
   1493   * "customizableui-special-spring30".
   1494   *
   1495   * @param {Element} aToolbar
   1496   *   The <xul:toolbar> node to update the special widget children IDs for.
   1497   * @param {string[]} aSpecialIDs
   1498   *   An array of special node IDs from the saved placements state.
   1499   */
   1500  updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) {
   1501    // Nodes are going to be in the correct order, so we can do this straightforwardly:
   1502    let { children } = this.getCustomizationTarget(aToolbar);
   1503    for (let kid of children) {
   1504      if (
   1505        this.matchingSpecials(aSpecialIDs[0], kid) &&
   1506        kid.getAttribute("skipintoolbarset") != "true"
   1507      ) {
   1508        kid.id = aSpecialIDs.shift();
   1509      }
   1510      if (!aSpecialIDs.length) {
   1511        return;
   1512      }
   1513    }
   1514  },
   1515 
   1516  /**
   1517   * This does the work of causing a customizable area to reflect the placements
   1518   * that have been computed for that area. This means taking the initial
   1519   * default DOM state of the area, and then modifying it to match the computed
   1520   * state (either the state that had been saved in preferences, or the state
   1521   * computed after doing runtime checks during initialization).
   1522   *
   1523   * @param {string} aAreaId
   1524   *   The ID of the customizable area to "build".
   1525   * @param {string[]} aPlacements
   1526   *   The IDs of the customizable widgets that are expected to be in the
   1527   *   customizable area.
   1528   * @param {Element} aAreaNode
   1529   *   The node associated with the area (as opposed to the customization
   1530   *   target within that area).
   1531   */
   1532  buildArea(aAreaId, aPlacements, aAreaNode) {
   1533    let document = aAreaNode.ownerDocument;
   1534    let window = document.defaultView;
   1535    let inPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
   1536    let container = this.getCustomizationTarget(aAreaNode);
   1537    let areaIsPanel =
   1538      gAreas.get(aAreaId).get("type") == CustomizableUI.TYPE_PANEL;
   1539 
   1540    if (!container) {
   1541      throw new Error(
   1542        "Expected area " + aAreaId + " to have a customizationTarget attribute."
   1543      );
   1544    }
   1545 
   1546    // Restore nav-bar visibility since it may have been hidden
   1547    // through a migration path (bug 938980) or an add-on.
   1548    if (aAreaId == CustomizableUI.AREA_NAVBAR) {
   1549      aAreaNode.collapsed = false;
   1550    }
   1551 
   1552    this.beginBatchUpdate();
   1553 
   1554    try {
   1555      let currentNode = container.firstElementChild;
   1556      let placementsToRemove = new Set();
   1557      for (let id of aPlacements) {
   1558        while (
   1559          currentNode &&
   1560          currentNode.getAttribute("skipintoolbarset") == "true"
   1561        ) {
   1562          currentNode = currentNode.nextElementSibling;
   1563        }
   1564 
   1565        // Fix ids for specials and continue, for correctly placed specials.
   1566        if (
   1567          currentNode &&
   1568          (!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) &&
   1569          this.matchingSpecials(id, currentNode)
   1570        ) {
   1571          currentNode.id = id;
   1572        }
   1573        if (currentNode && currentNode.id == id) {
   1574          currentNode = currentNode.nextElementSibling;
   1575          continue;
   1576        }
   1577 
   1578        if (this.isSpecialWidget(id) && areaIsPanel) {
   1579          placementsToRemove.add(id);
   1580          continue;
   1581        }
   1582 
   1583        let [provider, node] = this.getWidgetNode(id, window);
   1584        if (!node) {
   1585          lazy.log.debug("Unknown widget: " + id);
   1586          continue;
   1587        }
   1588 
   1589        let widget = null;
   1590        // If the placements have items in them which are (now) no longer removable,
   1591        // we shouldn't be moving them:
   1592        if (provider == CustomizableUI.PROVIDER_API) {
   1593          widget = gPalette.get(id);
   1594          if (!widget.removable && aAreaId != widget.defaultArea) {
   1595            placementsToRemove.add(id);
   1596            continue;
   1597          }
   1598        } else if (
   1599          provider == CustomizableUI.PROVIDER_XUL &&
   1600          node.parentNode != container &&
   1601          !this.isWidgetRemovable(node)
   1602        ) {
   1603          placementsToRemove.add(id);
   1604          continue;
   1605        } // Special widgets are always removable, so no need to check them
   1606 
   1607        if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
   1608          continue;
   1609        }
   1610 
   1611        if (!inPrivateWindow && widget?.hideInNonPrivateBrowsing) {
   1612          continue;
   1613        }
   1614 
   1615        this.ensureButtonContextMenu(node, aAreaNode);
   1616 
   1617        // This needs updating in case we're resetting / undoing a reset.
   1618        if (widget) {
   1619          widget.currentArea = aAreaId;
   1620        }
   1621        this.insertWidgetBefore(node, currentNode, container, aAreaId);
   1622        if (gResetting) {
   1623          this.notifyListeners("onWidgetReset", node, container);
   1624        } else if (gUndoResetting) {
   1625          this.notifyListeners("onWidgetUndoMove", node, container);
   1626        }
   1627      }
   1628 
   1629      if (currentNode) {
   1630        let palette = window.gNavToolbox ? window.gNavToolbox.palette : null;
   1631        let limit = currentNode.previousElementSibling;
   1632        let node = container.lastElementChild;
   1633        while (node && node != limit) {
   1634          let previousSibling = node.previousElementSibling;
   1635          // Nodes opt-in to removability. If they're removable, and we haven't
   1636          // seen them in the placements array, then we toss them into the palette
   1637          // if one exists. If no palette exists, we just remove the node. If the
   1638          // node is not removable, we leave it where it is. However, we can only
   1639          // safely touch elements that have an ID - both because we depend on
   1640          // IDs (or are specials), and because such elements are not intended to
   1641          // be widgets (eg, titlebar-spacer elements).
   1642          if (
   1643            (node.id || this.isSpecialWidget(node)) &&
   1644            node.getAttribute("skipintoolbarset") != "true"
   1645          ) {
   1646            if (this.isWidgetRemovable(node)) {
   1647              if (node.id && (gResetting || gUndoResetting)) {
   1648                let widget = gPalette.get(node.id);
   1649                if (widget) {
   1650                  widget.currentArea = null;
   1651                }
   1652              }
   1653              this.notifyDOMChange(node, null, container, true, () => {
   1654                if (palette && !this.isSpecialWidget(node.id)) {
   1655                  palette.appendChild(node);
   1656                  this.removeLocationAttributes(node);
   1657                } else {
   1658                  container.removeChild(node);
   1659                }
   1660              });
   1661            } else {
   1662              node.setAttribute("removable", false);
   1663              lazy.log.debug(
   1664                "Adding non-removable widget to placements of " +
   1665                  aAreaId +
   1666                  ": " +
   1667                  node.id
   1668              );
   1669              gPlacements.get(aAreaId).push(node.id);
   1670              gDirty = true;
   1671            }
   1672          }
   1673          node = previousSibling;
   1674        }
   1675      }
   1676 
   1677      // If there are placements in here which aren't removable from their original area,
   1678      // we remove them from this area's placement array. They will (have) be(en) added
   1679      // to their original area's placements array in the block above this one.
   1680      if (placementsToRemove.size) {
   1681        let placementAry = gPlacements.get(aAreaId);
   1682        for (let id of placementsToRemove) {
   1683          let index = placementAry.indexOf(id);
   1684          placementAry.splice(index, 1);
   1685        }
   1686      }
   1687 
   1688      if (gResetting) {
   1689        this.notifyListeners("onAreaReset", aAreaId, container);
   1690      }
   1691    } finally {
   1692      this.endBatchUpdate();
   1693    }
   1694  },
   1695 
   1696  /**
   1697   * @see CustomizableUI.addPanelCloseListeners
   1698   * @param {Element} aPanel
   1699   */
   1700  addPanelCloseListeners(aPanel) {
   1701    aPanel.addEventListener("click", this, { mozSystemGroup: true });
   1702    aPanel.addEventListener("keypress", this, { mozSystemGroup: true });
   1703    let win = aPanel.ownerGlobal;
   1704    if (!gPanelsForWindow.has(win)) {
   1705      gPanelsForWindow.set(win, new Set());
   1706    }
   1707    gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
   1708  },
   1709 
   1710  /**
   1711   * @see CustomizableUI.removePanelCloseListeners
   1712   * @param {Element} aPanel
   1713   */
   1714  removePanelCloseListeners(aPanel) {
   1715    aPanel.removeEventListener("click", this, { mozSystemGroup: true });
   1716    aPanel.removeEventListener("keypress", this, { mozSystemGroup: true });
   1717    let win = aPanel.ownerGlobal;
   1718    let panels = gPanelsForWindow.get(win);
   1719    if (panels) {
   1720      panels.delete(this._getPanelForNode(aPanel));
   1721    }
   1722  },
   1723 
   1724  /**
   1725   * Given a customizable widget node, attempts to assign it the correct
   1726   * context / contextmenu attributes for the current placement of that
   1727   * widget.
   1728   *
   1729   * @param {Element} aNode
   1730   *   The node to set the context / contextmenu attributes for.
   1731   * @param {Element} aAreaNode
   1732   *   The node representing the area that aNode is currently placed within.
   1733   * @param {boolean} forcePanel
   1734   *   True if we should force the panel context menu, regardless of the
   1735   *   current placement. This is mainly useful for the overflow panel, where
   1736   *   the placement may be the overflowable toolbar, but visually the widget
   1737   *   is within the overflowable toolbar panel.
   1738   */
   1739  ensureButtonContextMenu(aNode, aAreaNode, forcePanel) {
   1740    const kPanelItemContextMenu = "customizationPanelItemContextMenu";
   1741 
   1742    let currentContextMenu =
   1743      aNode.getAttribute("context") || aNode.getAttribute("contextmenu");
   1744    let contextMenuForPlace;
   1745 
   1746    if (
   1747      CustomizableUI.isWebExtensionWidget(aNode.id) &&
   1748      (aAreaNode?.id == CustomizableUI.AREA_ADDONS ||
   1749        aNode.getAttribute("overflowedItem") == "true")
   1750    ) {
   1751      contextMenuForPlace = null;
   1752    } else {
   1753      contextMenuForPlace =
   1754        forcePanel || "panel" == CustomizableUI.getPlaceForItem(aAreaNode)
   1755          ? kPanelItemContextMenu
   1756          : null;
   1757    }
   1758    if (contextMenuForPlace && !currentContextMenu) {
   1759      aNode.setAttribute("context", contextMenuForPlace);
   1760    } else if (
   1761      currentContextMenu == kPanelItemContextMenu &&
   1762      contextMenuForPlace != kPanelItemContextMenu
   1763    ) {
   1764      aNode.removeAttribute("context");
   1765      aNode.removeAttribute("contextmenu");
   1766    }
   1767  },
   1768 
   1769  /**
   1770   * Returns the ID of the provider for a given widget. This is one of the
   1771   * CustomizableUI.PROVIDER_* constants.
   1772   *
   1773   * @param {string} aWidgetId
   1774   *   The ID of the widget to get the provider for.
   1775   * @returns {string}
   1776   *   One of the CustomizableUI.PROVIDER_* constants.
   1777   */
   1778  getWidgetProvider(aWidgetId) {
   1779    if (this.isSpecialWidget(aWidgetId)) {
   1780      return CustomizableUI.PROVIDER_SPECIAL;
   1781    }
   1782    if (gPalette.has(aWidgetId)) {
   1783      return CustomizableUI.PROVIDER_API;
   1784    }
   1785    // If this was an API widget that was destroyed, return null:
   1786    if (gSeenWidgets.has(aWidgetId)) {
   1787      return null;
   1788    }
   1789 
   1790    // We fall back to the XUL provider, but we don't know for sure (at this
   1791    // point) whether it exists there either. So the API is technically lying.
   1792    // Ideally, it would be able to return an error value (or throw an
   1793    // exception) if it really didn't exist. Our code calling this function
   1794    // handles that fine, but this is a public API.
   1795    return CustomizableUI.PROVIDER_XUL;
   1796  },
   1797 
   1798  /**
   1799   * @typedef {string|null} GetWidgetNodeIndex0
   1800   *   The ID of the provider for the widget node. This is one of the constants
   1801   *   in CustomizableUI.PROVIDER_*. This is null if no node is found for the
   1802   *   widget ID.
   1803   * @typedef {Element|null} GetWidgetNodeIndex1
   1804   *   The found node associated with a widget ID, or null if no such node can
   1805   *   be found.
   1806   * @typedef {[GetWidgetNodeIndex0, GetWidgetNodeIndex1]} GetWidgetNodeResult
   1807   */
   1808 
   1809  /**
   1810   * For a given window, returns the node associated with a widget ID.
   1811   *
   1812   * @param {string} aWidgetId
   1813   *   The ID of the widget to get the associated node for in the window.
   1814   * @param {DOMWindow} aWindow
   1815   *   The window to find the node for, associated with aWidgetId.
   1816   * @returns {GetWidgetNodeResult}
   1817   *   The found node information.
   1818   */
   1819  getWidgetNode(aWidgetId, aWindow) {
   1820    let document = aWindow.document;
   1821 
   1822    if (this.isSpecialWidget(aWidgetId)) {
   1823      let widgetNode =
   1824        document.getElementById(aWidgetId) ||
   1825        this.createSpecialWidget(aWidgetId, document);
   1826      return [CustomizableUI.PROVIDER_SPECIAL, widgetNode];
   1827    }
   1828 
   1829    let widget = gPalette.get(aWidgetId);
   1830    if (widget) {
   1831      // If we have an instance of this widget already, just use that.
   1832      if (widget.instances.has(document)) {
   1833        lazy.log.debug(
   1834          "An instance of widget " +
   1835            aWidgetId +
   1836            " already exists in this " +
   1837            "document. Reusing."
   1838        );
   1839        return [CustomizableUI.PROVIDER_API, widget.instances.get(document)];
   1840      }
   1841 
   1842      return [
   1843        CustomizableUI.PROVIDER_API,
   1844        this.buildWidgetNode(document, widget),
   1845      ];
   1846    }
   1847 
   1848    lazy.log.debug("Searching for " + aWidgetId + " in toolbox.");
   1849    let node = this.findXULWidgetInWindow(aWidgetId, aWindow);
   1850    if (node) {
   1851      return [CustomizableUI.PROVIDER_XUL, node];
   1852    }
   1853 
   1854    lazy.log.debug("No node for " + aWidgetId + " found.");
   1855    return [null, null];
   1856  },
   1857 
   1858  /**
   1859   * @see CustomizableUI.registerPanelNode
   1860   * @param {Element} aNode
   1861   * @param {Element} aAreaId
   1862   */
   1863  registerPanelNode(aNode, aAreaId) {
   1864    if (gBuildAreas.has(aAreaId) && gBuildAreas.get(aAreaId).has(aNode)) {
   1865      return;
   1866    }
   1867 
   1868    aNode._customizationTarget = aNode;
   1869    this.addPanelCloseListeners(this._getPanelForNode(aNode));
   1870 
   1871    let placements = gPlacements.get(aAreaId);
   1872    this.buildArea(aAreaId, placements, aNode);
   1873    this.notifyListeners("onAreaNodeRegistered", aAreaId, aNode);
   1874 
   1875    for (let child of aNode.children) {
   1876      if (child.localName != "toolbarbutton") {
   1877        if (child.localName == "toolbaritem") {
   1878          this.ensureButtonContextMenu(child, aNode, true);
   1879        }
   1880        continue;
   1881      }
   1882      this.ensureButtonContextMenu(child, aNode, true);
   1883    }
   1884 
   1885    this.registerBuildArea(aAreaId, aNode);
   1886  },
   1887 
   1888  /**
   1889   * @type {CustomizableUIOnWidgetAddedCallback}
   1890   */
   1891  onWidgetAdded(aWidgetId, aArea, _aPosition) {
   1892    this.insertNode(aWidgetId, aArea, true);
   1893 
   1894    if (!gResetting) {
   1895      this._clearPreviousUIState();
   1896    }
   1897  },
   1898 
   1899  /**
   1900   * @type {CustomizableUIOnWidgetRemovedCallback}
   1901   */
   1902  onWidgetRemoved(aWidgetId, aArea) {
   1903    let areaNodes = gBuildAreas.get(aArea);
   1904    if (!areaNodes) {
   1905      return;
   1906    }
   1907 
   1908    let area = gAreas.get(aArea);
   1909    let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
   1910    let isOverflowable = isToolbar && area.get("overflowable");
   1911    let showInPrivateBrowsing = gPalette.has(aWidgetId)
   1912      ? gPalette.get(aWidgetId).showInPrivateBrowsing
   1913      : true;
   1914    let hideInNonPrivateBrowsing =
   1915      gPalette.get(aWidgetId)?.hideInNonPrivateBrowsing ?? false;
   1916 
   1917    for (let areaNode of areaNodes) {
   1918      let window = areaNode.ownerGlobal;
   1919      if (
   1920        !showInPrivateBrowsing &&
   1921        lazy.PrivateBrowsingUtils.isWindowPrivate(window)
   1922      ) {
   1923        continue;
   1924      }
   1925 
   1926      if (
   1927        hideInNonPrivateBrowsing &&
   1928        !lazy.PrivateBrowsingUtils.isWindowPrivate(window)
   1929      ) {
   1930        continue;
   1931      }
   1932 
   1933      let container = this.getCustomizationTarget(areaNode);
   1934      let widgetNode = window.document.getElementById(aWidgetId);
   1935      if (widgetNode && isOverflowable) {
   1936        container = areaNode.overflowable.getContainerFor(widgetNode);
   1937      }
   1938 
   1939      if (!widgetNode || !container.contains(widgetNode)) {
   1940        lazy.log.info(
   1941          "Widget " + aWidgetId + " not found, unable to remove from " + aArea
   1942        );
   1943        continue;
   1944      }
   1945 
   1946      this.notifyDOMChange(widgetNode, null, container, true, () => {
   1947        // We remove location attributes here to make sure they're gone too when a
   1948        // widget is removed from a toolbar to the palette. See bug 930950.
   1949        this.removeLocationAttributes(widgetNode);
   1950        // We also need to remove the panel context menu if it's there:
   1951        this.ensureButtonContextMenu(widgetNode);
   1952        if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
   1953          container.removeChild(widgetNode);
   1954        } else {
   1955          window.gNavToolbox.palette.appendChild(widgetNode);
   1956        }
   1957      });
   1958 
   1959      let windowCache = gSingleWrapperCache.get(window);
   1960      if (windowCache) {
   1961        windowCache.delete(aWidgetId);
   1962      }
   1963    }
   1964    if (!gResetting) {
   1965      this._clearPreviousUIState();
   1966    }
   1967  },
   1968 
   1969  /**
   1970   * @type {CustomizableUIOnWidgetMovedCallback}
   1971   */
   1972  onWidgetMoved(aWidgetId, aArea, _aOldPosition, _aNewPosition) {
   1973    this.insertNode(aWidgetId, aArea);
   1974    if (!gResetting) {
   1975      this._clearPreviousUIState();
   1976    }
   1977  },
   1978 
   1979  /**
   1980   * @type {CustomizableUIOnCustomizeEnd}
   1981   */
   1982  onCustomizeEnd() {
   1983    this._clearPreviousUIState();
   1984  },
   1985 
   1986  /**
   1987   * Registers a customizable area of a window with CustomizableUI such that it
   1988   * can then be "built" to reflect the current stored state.
   1989   *
   1990   * @param {string} aAreaId
   1991   *   The ID of the area to be registered.
   1992   * @param {Element} aAreaNode
   1993   *   The element for the area in the window being registered.
   1994   */
   1995  registerBuildArea(aAreaId, aAreaNode) {
   1996    // We ensure that the window is registered to have its customization data
   1997    // cleaned up when unloading.
   1998    let window = aAreaNode.ownerGlobal;
   1999    if (window.closed) {
   2000      return;
   2001    }
   2002    this.registerBuildWindow(window);
   2003 
   2004    // Also register this build area's toolbox.
   2005    if (window.gNavToolbox) {
   2006      gBuildWindows.get(window).add(window.gNavToolbox);
   2007    }
   2008 
   2009    if (!gBuildAreas.has(aAreaId)) {
   2010      gBuildAreas.set(aAreaId, new Set());
   2011    }
   2012 
   2013    gBuildAreas.get(aAreaId).add(aAreaNode);
   2014 
   2015    // Give a class to all customize targets to be used for styling in Customize Mode
   2016    let customizableNode = this.getCustomizeTargetForArea(aAreaId, window);
   2017    customizableNode.classList.add("customization-target");
   2018  },
   2019 
   2020  /**
   2021   * Registers a browser window with customizable elements with CustomizableUI.
   2022   * This is mainly used to set up event handlers to perform cleanups if and
   2023   * when the window closes. If the window is already registered, this is a
   2024   * no-op.
   2025   *
   2026   * @param {DOMWindow} aWindow
   2027   *   The window to register.
   2028   */
   2029  registerBuildWindow(aWindow) {
   2030    if (!gBuildWindows.has(aWindow)) {
   2031      gBuildWindows.set(aWindow, new Set());
   2032 
   2033      aWindow.addEventListener("unload", this);
   2034      aWindow.addEventListener("command", this, true);
   2035 
   2036      this.notifyListeners("onWindowOpened", aWindow);
   2037    }
   2038  },
   2039 
   2040  /**
   2041   * Unregisters a browser window that was registered with registerBuildWindow.
   2042   * The onAreaNodeUnregistered callback will be called for each customizable
   2043   * area within the window being unregistered. The onWidgetInstanceRemoved
   2044   * will be called for each widget in the window being unregistered. Finally,
   2045   * the onWindowClosed listener will be called with the window.
   2046   *
   2047   * @param {DOMWindow} aWindow
   2048   *   The window to unregister.
   2049   */
   2050  unregisterBuildWindow(aWindow) {
   2051    aWindow.removeEventListener("unload", this);
   2052    aWindow.removeEventListener("command", this, true);
   2053    gPanelsForWindow.delete(aWindow);
   2054    gBuildWindows.delete(aWindow);
   2055    gSingleWrapperCache.delete(aWindow);
   2056    let document = aWindow.document;
   2057 
   2058    for (let [areaId, areaNodes] of gBuildAreas) {
   2059      let areaProperties = gAreas.get(areaId);
   2060      for (let node of areaNodes) {
   2061        if (node.ownerDocument == document) {
   2062          this.notifyListeners(
   2063            "onAreaNodeUnregistered",
   2064            areaId,
   2065            this.getCustomizationTarget(node),
   2066            CustomizableUI.REASON_WINDOW_CLOSED
   2067          );
   2068          if (areaProperties.get("overflowable")) {
   2069            node.overflowable.uninit();
   2070            node.overflowable = null;
   2071          }
   2072          areaNodes.delete(node);
   2073        }
   2074      }
   2075    }
   2076 
   2077    for (let [, widget] of gPalette) {
   2078      widget.instances.delete(document);
   2079      this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
   2080    }
   2081 
   2082    for (let [, pendingNodes] of gPendingBuildAreas) {
   2083      for (let i = pendingNodes.length - 1; i >= 0; i--) {
   2084        if (pendingNodes[i].ownerDocument == document) {
   2085          pendingNodes.splice(i, 1);
   2086        }
   2087      }
   2088    }
   2089 
   2090    this.notifyListeners("onWindowClosed", aWindow);
   2091  },
   2092 
   2093  /**
   2094   * @see CustomizableUI.handleNewBrowserWindow
   2095   * @param {DOMWindow} aWindow
   2096   */
   2097  handleNewBrowserWindow(aWindow) {
   2098    let { gNavToolbox, document, gBrowser } = aWindow;
   2099    gNavToolbox.palette = document.getElementById(
   2100      "BrowserToolbarPalette"
   2101    ).content;
   2102 
   2103    let isVerticalTabs = Services.prefs.getBoolPref(
   2104      kPrefSidebarVerticalTabsEnabled,
   2105      false
   2106    );
   2107    let nonRemovables;
   2108 
   2109    // We don't want these normally non-removable elements to get put back into the
   2110    // tabstrip if we're initializing with vertical tabs.
   2111    // We do this for all windows, including popups, as otherwise it's possible for
   2112    // us to get confused about window state and save a blank state to prefs.
   2113    if (isVerticalTabs) {
   2114      nonRemovables = [gBrowser.tabContainer];
   2115      for (let elem of nonRemovables) {
   2116        elem.setAttribute("removable", "true");
   2117        // tell CUI to ignore this element when it builds the toolbar areas
   2118        elem.setAttribute("skipintoolbarset", "true");
   2119      }
   2120    }
   2121 
   2122    // Now register all the toolbars
   2123    for (let area of CustomizableUI.areas) {
   2124      let type = CustomizableUI.getAreaType(area);
   2125      if (type == CustomizableUI.TYPE_TOOLBAR) {
   2126        let node = document.getElementById(area);
   2127        this.registerToolbarNode(node);
   2128      }
   2129    }
   2130 
   2131    // Handle initial state of vertical tabs.
   2132    if (isVerticalTabs) {
   2133      // Show the vertical tabs toolbar
   2134      aWindow.setToolbarVisibility(
   2135        document.getElementById(CustomizableUI.AREA_VERTICAL_TABSTRIP),
   2136        true,
   2137        false,
   2138        false
   2139      );
   2140      let tabstripToolbar = document.getElementById(
   2141        CustomizableUI.AREA_TABSTRIP
   2142      );
   2143      let wasCollapsed = tabstripToolbar.collapsed;
   2144      aWindow.TabBarVisibility.update();
   2145      if (tabstripToolbar.collapsed !== wasCollapsed) {
   2146        let eventParams = {
   2147          detail: {
   2148            visible: !tabstripToolbar.collapsed,
   2149          },
   2150          bubbles: true,
   2151        };
   2152        let event = new CustomEvent("toolbarvisibilitychange", eventParams);
   2153        tabstripToolbar.dispatchEvent(event);
   2154      }
   2155 
   2156      for (let elem of nonRemovables) {
   2157        elem.setAttribute("removable", "false");
   2158        elem.removeAttribute("skipintoolbarset");
   2159      }
   2160    }
   2161  },
   2162 
   2163  /**
   2164   * Sets some attributes on a customizable widget when it is introduced into
   2165   * the DOM or moved around within it. Those attributes are "cui-anchorid"
   2166   * and "cui-areatype".
   2167   *
   2168   * @param {Element} aNode
   2169   *   The customizable widget node being inserted or moved within the DOM.
   2170   * @param {string} aAreaId
   2171   *   The area that the customizable widget is being moved into or within.
   2172   */
   2173  setLocationAttributes(aNode, aAreaId) {
   2174    let props = gAreas.get(aAreaId);
   2175    if (!props) {
   2176      throw new Error(
   2177        "Expected area " +
   2178          aAreaId +
   2179          " to have a properties Map " +
   2180          "associated with it."
   2181      );
   2182    }
   2183 
   2184    aNode.setAttribute("cui-areatype", props.get("type") || "");
   2185    let anchor = props.get("anchor");
   2186    if (anchor) {
   2187      aNode.setAttribute("cui-anchorid", anchor);
   2188    } else {
   2189      aNode.removeAttribute("cui-anchorid");
   2190    }
   2191  },
   2192 
   2193  /**
   2194   * Removes any location attributes from a customizable widget node when the
   2195   * node is removed from any of the registered customizable areas.
   2196   *
   2197   * @param {Element} aNode
   2198   *   The node being removed from the customizable area.
   2199   */
   2200  removeLocationAttributes(aNode) {
   2201    aNode.removeAttribute("cui-areatype");
   2202    aNode.removeAttribute("cui-anchorid");
   2203  },
   2204 
   2205  /**
   2206   * Inserts a node associated with the customizable widget with ID aWidgetId
   2207   * into all the areas with aAreaId across all windows.
   2208   *
   2209   * @param {string} aWidgetId
   2210   *   The ID of the customizable widget to insert into aAreaId across all
   2211   *   windows.
   2212   * @param {string} aAreaId
   2213   *   The ID of the area to insert the widget into across all windows.
   2214   *   This method is a no-op if aAreaId is not associated with a registered
   2215   *   area.
   2216   * @param {boolean} isNew
   2217   *   True if the widget is being newly inserted as opposed to moved.
   2218   */
   2219  insertNode(aWidgetId, aAreaId, isNew) {
   2220    let areaNodes = gBuildAreas.get(aAreaId);
   2221    if (!areaNodes) {
   2222      return;
   2223    }
   2224 
   2225    let placements = gPlacements.get(aAreaId);
   2226    if (!placements) {
   2227      lazy.log.error(
   2228        "Could not find any placements for " +
   2229          aAreaId +
   2230          " when moving a widget."
   2231      );
   2232      return;
   2233    }
   2234 
   2235    // Go through each of the nodes associated with this area and move the
   2236    // widget to the requested location.
   2237    for (let areaNode of areaNodes) {
   2238      this.insertNodeInWindow(aWidgetId, areaNode, isNew);
   2239    }
   2240  },
   2241 
   2242  /**
   2243   * Inserts a widget with ID aWidgetId into the passed area node in the
   2244   * position dictated by CustomizableUI's internal positioning state for
   2245   * widgets.
   2246   *
   2247   * @param {string} aWidgetId
   2248   *   The ID of the widget to insert into aAreaNode.
   2249   * @param {Element} aAreaNode
   2250   *   The customizable area node to insert aWidgetId into.
   2251   * @param {boolean} isNew
   2252   *   True if the widget is being inserted for the first time, instead of
   2253   *   moved.
   2254   */
   2255  insertNodeInWindow(aWidgetId, aAreaNode, isNew) {
   2256    let window = aAreaNode.ownerGlobal;
   2257    let showInPrivateBrowsing = gPalette.has(aWidgetId)
   2258      ? gPalette.get(aWidgetId).showInPrivateBrowsing
   2259      : true;
   2260    let hideInNonPrivateBrowsing =
   2261      gPalette.get(aWidgetId)?.hideInNonPrivateBrowsing ?? false;
   2262 
   2263    if (
   2264      !showInPrivateBrowsing &&
   2265      lazy.PrivateBrowsingUtils.isWindowPrivate(window)
   2266    ) {
   2267      return;
   2268    }
   2269 
   2270    if (
   2271      hideInNonPrivateBrowsing &&
   2272      !lazy.PrivateBrowsingUtils.isWindowPrivate(window)
   2273    ) {
   2274      return;
   2275    }
   2276 
   2277    let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
   2278    if (!widgetNode) {
   2279      lazy.log.error("Widget '" + aWidgetId + "' not found, unable to move");
   2280      return;
   2281    }
   2282 
   2283    let areaId = aAreaNode.id;
   2284    if (isNew) {
   2285      this.ensureButtonContextMenu(widgetNode, aAreaNode);
   2286    }
   2287 
   2288    let [insertionContainer, nextNode] = this.findInsertionPoints(
   2289      widgetNode,
   2290      aAreaNode
   2291    );
   2292    this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
   2293  },
   2294 
   2295  /**
   2296   * @typedef {Element|null} FindInsertionPointsIndex0
   2297   *   The container that the node should be inserted into, or null if no such
   2298   *   container can be found.
   2299   * @typedef {Element|null} FindInsertionPointsIndex1
   2300   *   The node that the insertion should occur before, or null if no such
   2301   *   sibling can be found.
   2302   * @typedef {[FindInsertionPointsIndex0, FindInsertionPointsIndex1]} FindInsertionPointsResult
   2303   */
   2304 
   2305  /**
   2306   * Given a node for a customizable widget that may have a placement within an
   2307   * area, find the location in the DOM where it makes the most sense to insert
   2308   * that node. In the event of there being a placement for aNode in aAreaNode,
   2309   * this insertion point will reflect the index of the node in that area's
   2310   * placements array. In the event of there not being a pre-existing placement
   2311   * for aNode in aAreaNode, the node will be prepended to the area.
   2312   *
   2313   * @param {Element} aNode
   2314   * @param {Element} aAreaNode
   2315   * @returns {FindInsertionPointsResult}
   2316   */
   2317  findInsertionPoints(aNode, aAreaNode) {
   2318    let areaId = aAreaNode.id;
   2319    let props = gAreas.get(areaId);
   2320 
   2321    // For overflowable toolbars, rely on them (because the work is more complicated):
   2322    if (
   2323      props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
   2324      props.get("overflowable")
   2325    ) {
   2326      return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
   2327    }
   2328 
   2329    let container = this.getCustomizationTarget(aAreaNode);
   2330    let placements = gPlacements.get(areaId);
   2331    let nodeIndex = placements.indexOf(aNode.id);
   2332 
   2333    while (++nodeIndex < placements.length) {
   2334      let nextNodeId = placements[nodeIndex];
   2335      // We use aAreaNode here, because if aNode is in a template, its
   2336      // `ownerDocument` is *not* going to be the browser.xhtml document,
   2337      // so we cannot rely on it.
   2338      let nextNode = aAreaNode.ownerDocument.getElementById(nextNodeId);
   2339      // If the next placed widget exists, and is a direct child of the
   2340      // container, or wrapped in a customize mode wrapper (toolbarpaletteitem)
   2341      // inside the container, insert beside it.
   2342      // We have to check the parent to avoid errors when the placement ids
   2343      // are for nodes that are no longer customizable.
   2344      if (
   2345        nextNode &&
   2346        (nextNode.parentNode == container ||
   2347          (nextNode.parentNode.localName == "toolbarpaletteitem" &&
   2348            nextNode.parentNode.parentNode == container))
   2349      ) {
   2350        return [container, nextNode];
   2351      }
   2352    }
   2353 
   2354    return [container, null];
   2355  },
   2356 
   2357  /**
   2358   * Inserts a node associated with a widget before some other node within a
   2359   * container within a customizable area.
   2360   *
   2361   * @param {Element} aNode
   2362   *   The customizable widget node to insert.
   2363   * @param {Element|null} aNextNode
   2364   *   The node that the inserted node should be inserted before, or null if
   2365   *   it should be inserted at the end of aContainer.
   2366   * @param {Element} aContainer
   2367   *   The parent node of both aNode and aNextNode.
   2368   * @param {string} aAreaId
   2369   *   The identifier string of the area that aNode is being inserted into.
   2370   */
   2371  insertWidgetBefore(aNode, aNextNode, aContainer, aAreaId) {
   2372    this.notifyDOMChange(aNode, aNextNode, aContainer, false, () => {
   2373      this.setLocationAttributes(aNode, aAreaId);
   2374      aContainer.insertBefore(aNode, aNextNode);
   2375    });
   2376  },
   2377 
   2378  /**
   2379   * Fires the onWidgetBeforeDOMChange event, then calls aCallback, before
   2380   * firing the onWidgetAfterDOMChange event.
   2381   *
   2382   * @param {Element} aNode
   2383   *   The node that is being changed.
   2384   * @param {Element|null} aNextNode
   2385   *   The node immediately after the one changing, or null if there is no next
   2386   *   node in the container.
   2387   * @param {Element} aContainer
   2388   *   The container of the node that is being changed.
   2389   * @param {boolean} aIsRemove
   2390   *   True iff the action about to happen is the removal of the DOM node.
   2391   * @param {Function} aCallback
   2392   *   A synchronous function that will be called after the
   2393   *   onWidgetBeforeDOMChange event is fired, but before onWidgetAfterDOMChange
   2394   *   is fired.
   2395   */
   2396  notifyDOMChange(aNode, aNextNode, aContainer, aIsRemove, aCallback) {
   2397    this.notifyListeners(
   2398      "onWidgetBeforeDOMChange",
   2399      aNode,
   2400      aNextNode,
   2401      aContainer,
   2402      aIsRemove
   2403    );
   2404    aCallback();
   2405    this.notifyListeners(
   2406      "onWidgetAfterDOMChange",
   2407      aNode,
   2408      aNextNode,
   2409      aContainer,
   2410      aIsRemove
   2411    );
   2412  },
   2413 
   2414  /**
   2415   * General event handler for CustomizableUIInternal that mostly just
   2416   * dispatches to more specialized event handlers based on the event type.
   2417   *
   2418   * @param {CommandEvent|MouseEvent|KeyEvent|Event} aEvent
   2419   */
   2420  handleEvent(aEvent) {
   2421    switch (aEvent.type) {
   2422      case "command":
   2423        if (!this._originalEventInPanel(aEvent)) {
   2424          break;
   2425        }
   2426        aEvent = aEvent.sourceEvent;
   2427      // Fall through
   2428      case "click":
   2429      case "keypress":
   2430        this.maybeAutoHidePanel(aEvent);
   2431        break;
   2432      case "unload":
   2433        this.unregisterBuildWindow(aEvent.currentTarget);
   2434        break;
   2435    }
   2436  },
   2437 
   2438  /**
   2439   * Returns true if the CommandEvent is being fired on a target that exists
   2440   * in one of the panels that CustomizableUI tracks in gPanelsForWindow.
   2441   *
   2442   * @param {CommandEvent} aEvent
   2443   * @returns {boolean}
   2444   */
   2445  _originalEventInPanel(aEvent) {
   2446    let e = aEvent.sourceEvent;
   2447    if (!e) {
   2448      return false;
   2449    }
   2450    let node = this._getPanelForNode(e.target);
   2451    if (!node) {
   2452      return false;
   2453    }
   2454    let win = e.view;
   2455    let panels = gPanelsForWindow.get(win);
   2456    return !!panels && panels.has(node);
   2457  },
   2458 
   2459  /**
   2460   * If passed a DOM node, this will return the ID attribute for the node if it
   2461   * exists. If it doesn't, it will check to see if the element localName starts
   2462   * with "toolbar", and if so, return the rest of the localName after that
   2463   * string. This means that for a <toolbarseparator> without an ID, this will
   2464   * return "separator". If the element does not have a localName that starts
   2465   * with "toolbar", this will return the empty string.
   2466   *
   2467   * If aNode happens to be a string instead of a DOM node, this simply returns
   2468   * the string back.
   2469   *
   2470   * @param {Element|string} aStringOrNode
   2471   *   A node to try to get the special identifier for, or a string that will
   2472   *   be echoed back to the caller.
   2473   * @returns {string}
   2474   *   The special identifier for the node, the empty string, or aStringOrNode
   2475   *   in the event that aStringOrNode happened to already be a string.
   2476   */
   2477  _getSpecialIdForNode(aStringOrNode) {
   2478    if (typeof aStringOrNode == "object" && aStringOrNode.localName) {
   2479      if (aStringOrNode.id) {
   2480        return aStringOrNode.id;
   2481      }
   2482      if (aStringOrNode.localName.startsWith("toolbar")) {
   2483        return aStringOrNode.localName.substring(7);
   2484      }
   2485      return "";
   2486    }
   2487    return aStringOrNode;
   2488  },
   2489 
   2490  /**
   2491   * Returns true if the passed in ID or node happens to be one of the "special"
   2492   * widget types (a separator, a spring, or a spacer).
   2493   *
   2494   * @param {string|Element} aStringOrNode
   2495   *   An ID for a node, or an actual node itself to check for special-ness.
   2496   * @returns {boolean}
   2497   *   True if the ID or node resolves to a "special" widget type.
   2498   */
   2499  isSpecialWidget(aStringOrNode) {
   2500    if (aStringOrNode === null) {
   2501      lazy.log.debug("isSpecialWidget was passed null");
   2502      return false;
   2503    }
   2504    aStringOrNode = this._getSpecialIdForNode(aStringOrNode);
   2505    return (
   2506      aStringOrNode.startsWith(kSpecialWidgetPfx) ||
   2507      aStringOrNode.startsWith("separator") ||
   2508      aStringOrNode.startsWith("spring") ||
   2509      aStringOrNode.startsWith("spacer")
   2510    );
   2511  },
   2512 
   2513  /**
   2514   * Returns true if the passed in strings (or nodes) happen to be the same
   2515   * special widget.
   2516   *
   2517   * @param {string|Element} aId1
   2518   *   The first ID or element to compare to the second.
   2519   * @param {string|Element} aId2
   2520   *   The second ID or element to compare to the first.
   2521   * @returns {boolean}
   2522   *   True if the two strings or elements being compared refer to the same
   2523   *   special widget.
   2524   */
   2525  matchingSpecials(aId1, aId2) {
   2526    aId1 = this._getSpecialIdForNode(aId1);
   2527    aId2 = this._getSpecialIdForNode(aId2);
   2528 
   2529    return (
   2530      this.isSpecialWidget(aId1) &&
   2531      this.isSpecialWidget(aId2) &&
   2532      aId1.match(/spring|spacer|separator/)[0] ==
   2533        aId2.match(/spring|spacer|separator/)[0]
   2534    );
   2535  },
   2536 
   2537  /**
   2538   * If the aId string starts with any of "spring", "spacer" or "separator" (any
   2539   * of the special widget types), this will add a the special widget prefix
   2540   * to the id, as well as a unique numeric suffix at the end and return it.
   2541   *
   2542   * Otherwise, this simply echoes back the aId string.
   2543   *
   2544   * @param {string} aId
   2545   * @returns {string}
   2546   *   The aId string with the special widget prefix and unique numeric suffix
   2547   *   if the aId string is for a special widget, otherwise this just echoes
   2548   *   back aId.
   2549   */
   2550  ensureSpecialWidgetId(aId) {
   2551    let nodeType = aId.match(/spring|spacer|separator/)[0];
   2552    // If the ID we were passed isn't a generated one, generate one now:
   2553    if (nodeType == aId) {
   2554      // Ids are differentiated through a unique count suffix.
   2555      return kSpecialWidgetPfx + aId + ++gNewElementCount;
   2556    }
   2557    return aId;
   2558  },
   2559 
   2560  /**
   2561   * @see CustomizableUI.createSpecialWidget
   2562   * @param {string} aId
   2563   * @param {Document} aDocument
   2564   * @returns {Element}
   2565   */
   2566  createSpecialWidget(aId, aDocument) {
   2567    let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
   2568    let node = aDocument.createXULElement(nodeName);
   2569    node.className = "chromeclass-toolbar-additional";
   2570    node.id = this.ensureSpecialWidgetId(aId);
   2571    return node;
   2572  },
   2573 
   2574  /**
   2575   * Find a XUL-provided widget node in a window. Don't try to use this
   2576   * for an API-provided widget or a special widget.
   2577   *
   2578   * @param {string} aId
   2579   *   The ID of the XUL-provided widget to find the node for in aWindow.
   2580   * @param {DOMWindow} aWindow
   2581   *   The window to find the XUL-provided widget node for.
   2582   * @returns {Element|null}
   2583   *   The found XUL widget node, or null if it cannot be found.
   2584   * @throws {Error}
   2585   *   Throws if aWindow is not a registered build window.
   2586   */
   2587  findXULWidgetInWindow(aId, aWindow) {
   2588    if (!gBuildWindows.has(aWindow)) {
   2589      throw new Error("Build window not registered");
   2590    }
   2591 
   2592    if (!aId) {
   2593      lazy.log.error("findWidgetInWindow was passed an empty string.");
   2594      return null;
   2595    }
   2596 
   2597    let document = aWindow.document;
   2598 
   2599    // look for a node with the same id, as the node may be
   2600    // in a different toolbar.
   2601    let node = document.getElementById(aId);
   2602    if (node) {
   2603      let parent = node.parentNode;
   2604      while (
   2605        parent &&
   2606        !(
   2607          this.getCustomizationTarget(parent) ||
   2608          parent == aWindow.gNavToolbox.palette
   2609        )
   2610      ) {
   2611        parent = parent.parentNode;
   2612      }
   2613 
   2614      if (parent) {
   2615        let nodeInArea =
   2616          node.parentNode.localName == "toolbarpaletteitem"
   2617            ? node.parentNode
   2618            : node;
   2619        // Check if we're in a customization target, or in the palette:
   2620        if (
   2621          (this.getCustomizationTarget(parent) == nodeInArea.parentNode &&
   2622            gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) ||
   2623          aWindow.gNavToolbox.palette == nodeInArea.parentNode
   2624        ) {
   2625          // Normalize the removable attribute. For backwards compat, if
   2626          // the widget is not located in a toolbox palette then absence
   2627          // of the "removable" attribute means it is not removable.
   2628          if (!node.hasAttribute("removable")) {
   2629            // If we first see this in customization mode, it may be in the
   2630            // customization palette instead of the toolbox palette.
   2631            node.setAttribute(
   2632              "removable",
   2633              !this.getCustomizationTarget(parent)
   2634            );
   2635          }
   2636          return node;
   2637        }
   2638      }
   2639    }
   2640 
   2641    let toolboxes = gBuildWindows.get(aWindow);
   2642    for (let toolbox of toolboxes) {
   2643      if (toolbox.palette) {
   2644        // Attempt to locate an element with a matching ID within
   2645        // the palette.
   2646        let element = toolbox.palette.getElementsByAttribute("id", aId)[0];
   2647        if (element) {
   2648          // Normalize the removable attribute. For backwards compat, this
   2649          // is optional if the widget is located in the toolbox palette,
   2650          // and defaults to *true*, unlike if it was located elsewhere.
   2651          if (!element.hasAttribute("removable")) {
   2652            element.setAttribute("removable", true);
   2653          }
   2654          return element;
   2655        }
   2656      }
   2657    }
   2658    return null;
   2659  },
   2660 
   2661  /**
   2662   * Constructs a node for a customizable UI widget that can be placed within
   2663   * aDocument.
   2664   *
   2665   * @param {Document} aDocument
   2666   *   The document that the node will be inserted into.
   2667   * @param {string|WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   2668   *   The customizable UI widget wrapper or widget ID of the widget to
   2669   *   construct.
   2670   * @returns {Element|null}
   2671   *   Returns the constructed node for the widget to be placed in aDocument.
   2672   *   Will return null if the widget node is not allowed to be placed in
   2673   *   aDocument (for example, if aDocument is a private browsing window
   2674   *   document, and the widget is not allowed to be placed in such a window).
   2675   * @throws {Error}
   2676   *   Can throw if the document is not a browser window document, or if
   2677   *   aWidget is null.
   2678   */
   2679  buildWidgetNode(aDocument, aWidget) {
   2680    if (aDocument.documentURI != kExpectedWindowURL) {
   2681      throw new Error("buildWidget was called for a non-browser window!");
   2682    }
   2683    if (typeof aWidget == "string") {
   2684      aWidget = gPalette.get(aWidget);
   2685    }
   2686    if (!aWidget) {
   2687      throw new Error("buildWidget was passed a non-widget to build.");
   2688    }
   2689    if (
   2690      !aWidget.showInPrivateBrowsing &&
   2691      lazy.PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)
   2692    ) {
   2693      return null;
   2694    }
   2695    if (
   2696      aWidget.hideInNonPrivateBrowsing &&
   2697      !lazy.PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)
   2698    ) {
   2699      return null;
   2700    }
   2701 
   2702    lazy.log.debug("Building " + aWidget.id + " of type " + aWidget.type);
   2703 
   2704    let node;
   2705    let button;
   2706    if (aWidget.type == "custom") {
   2707      if (aWidget.onBuild) {
   2708        node = aWidget.onBuild(aDocument);
   2709      }
   2710      if (
   2711        !node ||
   2712        !aDocument.defaultView.XULElement.isInstance(node) ||
   2713        (aWidget.viewId && !node.viewButton)
   2714      ) {
   2715        lazy.log.error(
   2716          "Custom widget with id " +
   2717            aWidget.id +
   2718            " does not return a valid node"
   2719        );
   2720      }
   2721      // A custom widget can define a viewId for the panel and a viewButton
   2722      // property for the panel anchor.  With that, it will be treated as a view
   2723      // type where necessary to hook up the view panel.
   2724      if (aWidget.viewId) {
   2725        button = node.viewButton;
   2726      }
   2727    }
   2728    // Button and view widget types, plus custom widgets that have a viewId and thus a button.
   2729    if (button || aWidget.type != "custom") {
   2730      if (
   2731        aWidget.onBeforeCreated &&
   2732        aWidget.onBeforeCreated(aDocument) === false
   2733      ) {
   2734        return null;
   2735      }
   2736 
   2737      if (!button) {
   2738        button = aDocument.createXULElement("toolbarbutton");
   2739        node = button;
   2740      }
   2741      button.classList.add("toolbarbutton-1");
   2742      button.setAttribute("delegatesanchor", "true");
   2743 
   2744      let viewbutton = null;
   2745      if (aWidget.type == "button-and-view") {
   2746        button.setAttribute("id", aWidget.id + "-button");
   2747        let dropmarker = aDocument.createXULElement("toolbarbutton");
   2748        dropmarker.setAttribute("id", aWidget.id + "-dropmarker");
   2749        dropmarker.setAttribute("delegatesanchor", "true");
   2750        dropmarker.classList.add(
   2751          "toolbarbutton-1",
   2752          "toolbarbutton-combined-buttons-dropmarker"
   2753        );
   2754        node = aDocument.createXULElement("toolbaritem");
   2755        node.classList.add("toolbaritem-combined-buttons");
   2756        node.append(button, dropmarker);
   2757        viewbutton = dropmarker;
   2758      } else if (aWidget.viewId) {
   2759        // Also set viewbutton for anything with a view
   2760        viewbutton = button;
   2761      }
   2762 
   2763      node.setAttribute("id", aWidget.id);
   2764      node.setAttribute("widget-id", aWidget.id);
   2765      node.setAttribute("widget-type", aWidget.type);
   2766      node.toggleAttribute("disabled", !!aWidget.disabled);
   2767      node.setAttribute("removable", aWidget.removable);
   2768      node.setAttribute("overflows", aWidget.overflows);
   2769      if (aWidget.tabSpecific) {
   2770        node.setAttribute("tabspecific", aWidget.tabSpecific);
   2771      }
   2772      if (aWidget.locationSpecific) {
   2773        node.setAttribute("locationspecific", aWidget.locationSpecific);
   2774      }
   2775      if (aWidget.keepBroadcastAttributesWhenCustomizing) {
   2776        node.setAttribute(
   2777          "keepbroadcastattributeswhencustomizing",
   2778          aWidget.keepBroadcastAttributesWhenCustomizing
   2779        );
   2780      }
   2781 
   2782      let shortcut;
   2783      if (aWidget.shortcutId) {
   2784        let keyEl = aDocument.getElementById(aWidget.shortcutId);
   2785        if (keyEl) {
   2786          shortcut = lazy.ShortcutUtils.prettifyShortcut(keyEl);
   2787        } else {
   2788          lazy.log.error(
   2789            "Key element with id '" +
   2790              aWidget.shortcutId +
   2791              "' for widget '" +
   2792              aWidget.id +
   2793              "' not found!"
   2794          );
   2795        }
   2796      }
   2797 
   2798      if (aWidget.l10nId) {
   2799        aDocument.l10n.setAttributes(node, aWidget.l10nId);
   2800        if (button != node) {
   2801          // This is probably a "button-and-view" widget, such as the Profiler
   2802          // button. In that case, "node" is the "toolbaritem" container, and
   2803          // "button" the main button (see above).
   2804          // In this case, the values on the "node" is used in the Customize
   2805          // view, as well as the tooltips over both buttons; the values on the
   2806          // "button" are used in the overflow menu.
   2807          aDocument.l10n.setAttributes(button, aWidget.l10nId);
   2808        }
   2809 
   2810        if (shortcut) {
   2811          node.setAttribute("data-l10n-args", JSON.stringify({ shortcut }));
   2812          if (button != node) {
   2813            // This is probably a "button-and-view" widget.
   2814            button.setAttribute("data-l10n-args", JSON.stringify({ shortcut }));
   2815          }
   2816        }
   2817      } else {
   2818        node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
   2819        if (button != node) {
   2820          // This is probably a "button-and-view" widget.
   2821          button.setAttribute("label", node.getAttribute("label"));
   2822        }
   2823 
   2824        let tooltip = this.getLocalizedProperty(
   2825          aWidget,
   2826          "tooltiptext",
   2827          shortcut ? [shortcut] : []
   2828        );
   2829        if (tooltip) {
   2830          node.setAttribute("tooltiptext", tooltip);
   2831          if (button != node) {
   2832            // This is probably a "button-and-view" widget.
   2833            button.setAttribute("tooltiptext", tooltip);
   2834          }
   2835        }
   2836      }
   2837 
   2838      let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
   2839      node.addEventListener("command", commandHandler);
   2840      let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
   2841      node.addEventListener("click", clickHandler);
   2842 
   2843      node.classList.add("chromeclass-toolbar-additional");
   2844 
   2845      // If the widget has a view, register a keypress handler because opening
   2846      // a view with the keyboard has slightly different focus handling than
   2847      // opening a view with the mouse. (When opened with the keyboard, the
   2848      // first item in the view should be focused after opening.)
   2849      if (viewbutton) {
   2850        lazy.log.debug(
   2851          "Widget " +
   2852            aWidget.id +
   2853            " has a view. Auto-registering event handlers."
   2854        );
   2855 
   2856        if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) {
   2857          node.classList.add("subviewbutton-nav");
   2858        }
   2859      }
   2860 
   2861      if (aWidget.onCreated) {
   2862        aWidget.onCreated(node);
   2863      }
   2864    }
   2865 
   2866    aWidget.instances.set(aDocument, node);
   2867    return node;
   2868  },
   2869 
   2870  /**
   2871   * @see CustomizableUI.ensureSubviewListeners
   2872   * @param {Element} viewNode
   2873   */
   2874  ensureSubviewListeners(viewNode) {
   2875    if (viewNode._addedEventListeners) {
   2876      return;
   2877    }
   2878    let viewId = viewNode.id;
   2879    let widget = [...gPalette.values()].find(w => w.viewId == viewId);
   2880    if (!widget) {
   2881      return;
   2882    }
   2883    for (let eventName of kSubviewEvents) {
   2884      let handler = "on" + eventName;
   2885      if (typeof widget[handler] == "function") {
   2886        viewNode.addEventListener(eventName, widget[handler]);
   2887      }
   2888    }
   2889    viewNode._addedEventListeners = true;
   2890    lazy.log.debug(
   2891      "Widget " + widget.id + " showing and hiding event handlers set."
   2892    );
   2893  },
   2894 
   2895  /**
   2896   * @see CustomizableUI.getLocalizedProperty
   2897   * @param {string|object} aWidget
   2898   * @param {string} aProp
   2899   * @param {string[]} [aFormatArgs]
   2900   * @param {string} [aDef]
   2901   * @returns {string}
   2902   */
   2903  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
   2904    const kReqStringProps = ["label"];
   2905 
   2906    if (typeof aWidget == "string") {
   2907      aWidget = gPalette.get(aWidget);
   2908    }
   2909    if (!aWidget) {
   2910      throw new Error(
   2911        "getLocalizedProperty was passed a non-widget to work with."
   2912      );
   2913    }
   2914    let def, name;
   2915    // Let widgets pass their own string identifiers or strings, so that
   2916    // we can use strings which aren't the default (in case string ids change)
   2917    // and so that non-builtin-widgets can also provide labels, tooltips, etc.
   2918    if (aWidget[aProp] != null) {
   2919      name = aWidget[aProp];
   2920      // By using this as the default, if a widget provides a full string rather
   2921      // than a string ID for localization, we will fall back to that string
   2922      // and return that.
   2923      def = aDef || name;
   2924    } else {
   2925      name = aWidget.id + "." + aProp;
   2926      def = aDef || "";
   2927    }
   2928    if (aWidget.localized === false) {
   2929      return def;
   2930    }
   2931    try {
   2932      if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
   2933        return (
   2934          lazy.gWidgetsBundle.formatStringFromName(name, aFormatArgs) || def
   2935        );
   2936      }
   2937      return lazy.gWidgetsBundle.GetStringFromName(name) || def;
   2938    } catch (ex) {
   2939      // If an empty string was explicitly passed, treat it as an actual
   2940      // value rather than a missing property.
   2941      if (!def && (name != "" || kReqStringProps.includes(aProp))) {
   2942        lazy.log.error("Could not localize property '" + name + "'.");
   2943      }
   2944    }
   2945    return def;
   2946  },
   2947 
   2948  /**
   2949   * @see CustomizableUI.addShortcut
   2950   * @param {Element} aShortcutNode
   2951   * @param {Element|null} aTargetNode
   2952   */
   2953  addShortcut(aShortcutNode, aTargetNode = aShortcutNode) {
   2954    // Detect if we've already been here before.
   2955    if (aTargetNode.hasAttribute("shortcut")) {
   2956      return;
   2957    }
   2958 
   2959    // Use ownerGlobal.document to ensure we get the right doc even for
   2960    // elements in template tags.
   2961    let { document } = aShortcutNode.ownerGlobal;
   2962    let shortcutId = aShortcutNode.getAttribute("key");
   2963    let shortcut;
   2964    if (shortcutId) {
   2965      shortcut = document.getElementById(shortcutId);
   2966    } else {
   2967      let commandId = aShortcutNode.getAttribute("command");
   2968      if (commandId) {
   2969        shortcut = lazy.ShortcutUtils.findShortcut(
   2970          document.getElementById(commandId)
   2971        );
   2972      }
   2973    }
   2974    if (!shortcut) {
   2975      return;
   2976    }
   2977 
   2978    aTargetNode.setAttribute(
   2979      "shortcut",
   2980      lazy.ShortcutUtils.prettifyShortcut(shortcut)
   2981    );
   2982  },
   2983 
   2984  /**
   2985   * Executes a customizable UI widget's command handler to handle aEvent.
   2986   *
   2987   * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   2988   *   The customizable UI widget to call the command handler for.
   2989   * @param {Element} aNode
   2990   *   The node that the aEvent command event fired on.
   2991   * @param {CommandEvent} aEvent
   2992   *   The command event to handle.
   2993   */
   2994  doWidgetCommand(aWidget, aNode, aEvent) {
   2995    if (aWidget.onCommand) {
   2996      try {
   2997        aWidget.onCommand.call(null, aEvent);
   2998      } catch (e) {
   2999        lazy.log.error(e);
   3000      }
   3001    } else {
   3002      // XXXunf Need to think this through more, and formalize.
   3003      Services.obs.notifyObservers(
   3004        aNode,
   3005        "customizedui-widget-command",
   3006        aWidget.id
   3007      );
   3008    }
   3009  },
   3010 
   3011  /**
   3012   * Handles an event on a customizable UI widget node that has a panelview
   3013   * associated with it. This routine will cause the panelview to be displayed.
   3014   *
   3015   * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   3016   *   The customizable UI widget to show the panelview for.
   3017   * @param {Element} aNode
   3018   *   The node that is handling the event that is causing the panelview to
   3019   *   show.
   3020   * @param {Event} aEvent
   3021   *   The event that the node is handlign that is causing the panelview to
   3022   *   show.
   3023   */
   3024  showWidgetView(aWidget, aNode, aEvent) {
   3025    let ownerWindow = aNode.ownerGlobal;
   3026    let area = this.getPlacementOfWidget(aNode.id).area;
   3027    let areaType = CustomizableUI.getAreaType(area);
   3028    let anchor = aNode;
   3029 
   3030    if (
   3031      aWidget.disallowSubView &&
   3032      (areaType == CustomizableUI.TYPE_PANEL ||
   3033        aNode.hasAttribute("overflowedItem"))
   3034    ) {
   3035      // Close the containing panel (e.g. overflow), PanelUI will reopen.
   3036      let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
   3037      if (wrapper?.anchor) {
   3038        this.hidePanelForNode(aNode);
   3039        anchor = wrapper.anchor;
   3040      }
   3041    } else if (areaType != CustomizableUI.TYPE_PANEL) {
   3042      let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
   3043 
   3044      let hasMultiView = !!aNode.closest("panelmultiview");
   3045      if (!hasMultiView && wrapper?.anchor) {
   3046        this.hidePanelForNode(aNode);
   3047        anchor = wrapper.anchor;
   3048      }
   3049    }
   3050    ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent);
   3051  },
   3052 
   3053  /**
   3054   * Handles a command event on a customizable ui widget node, and does the
   3055   * action that best suits the type of widget.
   3056   *
   3057   * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   3058   *   The widget to handle the command event for.
   3059   * @param {Element} aNode
   3060   *   The node that the command event is being fired on.
   3061   * @param {CommandEvent} aEvent
   3062   *   The command event to be handled.
   3063   */
   3064  handleWidgetCommand(aWidget, aNode, aEvent) {
   3065    // Note that aEvent can be a keypress event for widgets of type "view".
   3066    lazy.log.debug("handleWidgetCommand");
   3067 
   3068    let action;
   3069    if (aWidget.onBeforeCommand) {
   3070      try {
   3071        action = aWidget.onBeforeCommand.call(null, aEvent, aNode);
   3072      } catch (e) {
   3073        lazy.log.error(e);
   3074      }
   3075    }
   3076 
   3077    if (aWidget.type == "button" || action == "command") {
   3078      this.doWidgetCommand(aWidget, aNode, aEvent);
   3079    } else if (aWidget.type == "view" || action == "view") {
   3080      this.showWidgetView(aWidget, aNode, aEvent);
   3081    } else if (aWidget.type == "button-and-view") {
   3082      // Do the command if we're in the toolbar and the button was clicked.
   3083      // Otherwise, including when we have currently overflowed out of the
   3084      // toolbar, open the view. There is no way to trigger the command while
   3085      // the widget is in the panel, by design.
   3086      let button = aNode.firstElementChild;
   3087      let area = this.getPlacementOfWidget(aNode.id).area;
   3088      let areaType = CustomizableUI.getAreaType(area);
   3089      if (
   3090        areaType == CustomizableUI.TYPE_TOOLBAR &&
   3091        button.contains(aEvent.target) &&
   3092        !aNode.hasAttribute("overflowedItem")
   3093      ) {
   3094        this.doWidgetCommand(aWidget, aNode, aEvent);
   3095      } else {
   3096        this.showWidgetView(aWidget, aNode, aEvent);
   3097      }
   3098    }
   3099  },
   3100 
   3101  /**
   3102   * Handles a click event on a customizable ui widget node, and redirects to
   3103   * the widgets onClick event handler, if such a handler exists.
   3104   *
   3105   * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   3106   *   The widget to call the onClick event handler for if such a handler
   3107   *   exists. If the handler does not exist, nothing is called.
   3108   * @param {Element} aNode
   3109   *   The node that fired the click event.
   3110   * @param {MouseEvent} aEvent
   3111   *   The click event to be handled.
   3112   */
   3113  handleWidgetClick(aWidget, aNode, aEvent) {
   3114    lazy.log.debug("handleWidgetClick");
   3115    if (aWidget.onClick) {
   3116      try {
   3117        aWidget.onClick.call(null, aEvent);
   3118      } catch (e) {
   3119        console.error(e);
   3120      }
   3121    } else {
   3122      // XXXunf Need to think this through more, and formalize.
   3123      Services.obs.notifyObservers(
   3124        aNode,
   3125        "customizedui-widget-click",
   3126        aWidget.id
   3127      );
   3128    }
   3129  },
   3130 
   3131  /**
   3132   * Returns the closest <xul:panel> element to aNode in its ancestry, or null
   3133   * if no such node can be found.
   3134   *
   3135   * @param {Element} aNode
   3136   *   The node to check the ancestry for.
   3137   * @returns {Element|null}
   3138   */
   3139  _getPanelForNode(aNode) {
   3140    return aNode.closest("panel");
   3141  },
   3142 
   3143  /**
   3144   * If people put things in the panel which need more than single-click
   3145   * interaction, we don't want to close it. Right now we check for text inputs
   3146   * and menu buttons. We also check for being outside of any
   3147   * toolbaritem/toolbarbutton, ie on a blank part of the menu, or on another
   3148   * menu (like a context menu inside the panel).
   3149   *
   3150   * So this returns true if the event being handled is on one of these
   3151   * interactive things that should NOT result in the associated panel closing.
   3152   *
   3153   * @param {Event} aEvent
   3154   *   The event that is occurring that we're evaluating.
   3155   * @returns {boolean}
   3156   *   True if the event occurred on an item we consider "interactive" such
   3157   *   that the enclosing panel should remain open. False if the panel should
   3158   *   close.
   3159   */
   3160  _isOnInteractiveElement(aEvent) {
   3161    let panel = this._getPanelForNode(aEvent.currentTarget);
   3162    // This can happen in e.g. customize mode. If there's no panel,
   3163    // there's clearly nothing for us to close; pretend we're interactive.
   3164    if (!panel) {
   3165      return true;
   3166    }
   3167 
   3168    function getNextTarget(target) {
   3169      if (target.nodeType == target.DOCUMENT_NODE) {
   3170        if (!target.defaultView) {
   3171          // Err, we're done.
   3172          return null;
   3173        }
   3174        // Find containing browser or iframe element in the parent doc.
   3175        return target.defaultView.docShell.chromeEventHandler;
   3176      }
   3177      // Skip any parent shadow roots
   3178      return target.parentNode?.host?.parentNode || target.parentNode;
   3179    }
   3180 
   3181    // While keeping track of that, we go from the original target back up,
   3182    // to the panel if we have to. We bail as soon as we find an input,
   3183    // a toolbarbutton/item, or a menuItem.
   3184    for (
   3185      let target = aEvent.originalTarget;
   3186      target && target != panel;
   3187      target = getNextTarget(target)
   3188    ) {
   3189      if (target.nodeType == target.DOCUMENT_NODE) {
   3190        // Skip out of iframes etc:
   3191        continue;
   3192      }
   3193 
   3194      // Skip out of shadow roots
   3195      if (
   3196        target.nodeType == target.DOCUMENT_FRAGMENT_NODE &&
   3197        target.containingShadowRoot == target
   3198      ) {
   3199        continue;
   3200      }
   3201 
   3202      // Break out of the loop immediately for disabled items, as we need to
   3203      // keep the menu open in that case.
   3204      if (target.getAttribute("disabled") == "true") {
   3205        return true;
   3206      }
   3207 
   3208      let tagName = target.localName;
   3209      if (tagName == "input" || tagName == "searchbar") {
   3210        return true;
   3211      }
   3212      if (tagName == "toolbaritem" || tagName == "toolbarbutton") {
   3213        // If we are in a type="menu" toolbarbutton, we'll now interact with
   3214        // the menu.
   3215        return target.getAttribute("type") == "menu";
   3216      }
   3217      if (tagName == "menuitem") {
   3218        // If we're in a nested menu we don't need to close this panel.
   3219        return true;
   3220      }
   3221    }
   3222 
   3223    // We don't know what we interacted with, assume interactive.
   3224    return true;
   3225  },
   3226 
   3227  /**
   3228   * Finds the associated panel (if any) enclosing a given element, and
   3229   * closes it.
   3230   *
   3231   * @param {Element} aNode
   3232   *   The node to close the panel ancestor for.
   3233   */
   3234  hidePanelForNode(aNode) {
   3235    let panel = this._getPanelForNode(aNode);
   3236    if (panel) {
   3237      lazy.PanelMultiView.hidePopup(panel);
   3238    }
   3239  },
   3240 
   3241  /**
   3242   * For an event that occurs for a node within a panel, this routine will
   3243   * check to see if that event should cause the panel to close.
   3244   *
   3245   * @param {Event} aEvent
   3246   *   The event that is being fired on the node. This could be a keyboard,
   3247   *   mouse or command event, for example.
   3248   */
   3249  maybeAutoHidePanel(aEvent) {
   3250    let eventType = aEvent.type;
   3251    if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) {
   3252      return;
   3253    }
   3254 
   3255    if (eventType == "click" && aEvent.button != 0) {
   3256      return;
   3257    }
   3258 
   3259    // We don't check preventDefault - it makes sense that this was prevented,
   3260    // but we probably still want to close the panel. If consumers don't want
   3261    // this to happen, they should specify the closemenu attribute.
   3262    if (eventType != "command" && this._isOnInteractiveElement(aEvent)) {
   3263      return;
   3264    }
   3265 
   3266    // We can't use event.target because we might have passed an anonymous
   3267    // content boundary as well, and so target points to the outer element in
   3268    // that case. Unfortunately, this means we get anonymous child nodes instead
   3269    // of the real ones, so looking for the 'stoooop, don't close me' attributes
   3270    // is more involved.
   3271    let target = aEvent.originalTarget;
   3272    while (target.parentNode && target.localName != "panel") {
   3273      if (
   3274        target.getAttribute("closemenu") == "none" ||
   3275        target.getAttribute("widget-type") == "view" ||
   3276        target.getAttribute("widget-type") == "button-and-view" ||
   3277        target.hasAttribute("view-button-id")
   3278      ) {
   3279        return;
   3280      }
   3281 
   3282      target = target.parentNode;
   3283 
   3284      // If we reached the shadow boundry, let's cross it while we head up
   3285      // the tree.
   3286      if (ShadowRoot.isInstance(target)) {
   3287        target = target.host;
   3288      }
   3289    }
   3290 
   3291    // If we get here, we can actually hide the popup:
   3292    this.hidePanelForNode(aEvent.target);
   3293  },
   3294 
   3295  /**
   3296   * @see CustomizableUI.getUnusedWidgets
   3297   * @param {DOMElement} aWindowPalette
   3298   * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper>}
   3299   */
   3300  getUnusedWidgets(aWindowPalette) {
   3301    let window = aWindowPalette.ownerGlobal;
   3302    let isWindowPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
   3303    // We use a Set because there can be overlap between the widgets in
   3304    // gPalette and the items in the palette, especially after the first
   3305    // customization, since programmatically generated widgets will remain
   3306    // in the toolbox palette.
   3307    let widgets = new Set();
   3308 
   3309    // It's possible that some widgets have been defined programmatically and
   3310    // have not been overlayed into the palette. We can find those inside
   3311    // gPalette.
   3312    for (let [id, widget] of gPalette) {
   3313      if (!widget.currentArea) {
   3314        if (
   3315          (isWindowPrivate && widget.showInPrivateBrowsing) ||
   3316          (!isWindowPrivate && !widget.hideInNonPrivateBrowsing)
   3317        ) {
   3318          widgets.add(id);
   3319        }
   3320      }
   3321    }
   3322 
   3323    lazy.log.debug("Iterating the actual nodes of the window palette");
   3324    for (let node of aWindowPalette.children) {
   3325      lazy.log.debug("In palette children: " + node.id);
   3326      if (node.id && !this.getPlacementOfWidget(node.id)) {
   3327        widgets.add(node.id);
   3328      }
   3329    }
   3330 
   3331    return [...widgets];
   3332  },
   3333 
   3334  /**
   3335   * @see CustomizableUI.getPlacementOfWidget
   3336   * @param {string} aWidgetId
   3337   * @param {boolean} [aOnlyRegistered=true]
   3338   * @param {boolean} [aDeadAreas=false]
   3339   * @returns {CustomizableUIPlacementInfo|null}
   3340   */
   3341  getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas) {
   3342    if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
   3343      return null;
   3344    }
   3345 
   3346    for (let [area, placements] of gPlacements) {
   3347      if (!gAreas.has(area) && !aDeadAreas) {
   3348        continue;
   3349      }
   3350      let index = placements.indexOf(aWidgetId);
   3351      if (index != -1) {
   3352        return { area, position: index };
   3353      }
   3354    }
   3355 
   3356    return null;
   3357  },
   3358 
   3359  /**
   3360   * Check for the current existance of a widget by ID.
   3361   *
   3362   * @see CustomizableUIInternal.isSpecialWidget
   3363   * @param {string} aWidgetId
   3364   * @returns {boolean}
   3365   *   Returns true if the widget ID belongs to a widget that is registered or
   3366   *   is a "special" widget (see isSpecialWidget). This will return false for
   3367   *   widget IDs belonging to widgets we have seen in the past, but are no
   3368   *   longer registered.
   3369   */
   3370  widgetExists(aWidgetId) {
   3371    if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
   3372      return true;
   3373    }
   3374 
   3375    // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
   3376    // If it's not in gPalette, it doesn't exist.
   3377    if (gSeenWidgets.has(aWidgetId)) {
   3378      return false;
   3379    }
   3380 
   3381    // We're assuming XUL widgets always exist, as it's much harder to check,
   3382    // and checking would be much more error prone.
   3383    return true;
   3384  },
   3385 
   3386  /**
   3387   * @see CustomizableUI.addWidgetToArea
   3388   * @param {string} aWidgetId
   3389   * @param {string} aArea
   3390   * @param {number} aPosition
   3391   * @param {boolean} [aInitialAdd=false]
   3392   *   True if this is the first time the widget is being added to the area
   3393   *   for the first time during initialization, or after a state reset.
   3394   */
   3395  addWidgetToArea(aWidgetId, aArea, aPosition, aInitialAdd = false) {
   3396    if (aArea == CustomizableUI.AREA_NO_AREA) {
   3397      throw new Error(
   3398        "AREA_NO_AREA is only used as an argument for " +
   3399          "canWidgetMoveToArea. Use removeWidgetFromArea instead."
   3400      );
   3401    }
   3402    if (!gAreas.has(aArea)) {
   3403      throw new Error("Unknown customization area: " + aArea);
   3404    }
   3405 
   3406    // Hack: don't want special widgets in the panel (need to check here as well
   3407    // as in canWidgetMoveToArea because the menu panel is lazy):
   3408    if (
   3409      gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL &&
   3410      this.isSpecialWidget(aWidgetId)
   3411    ) {
   3412      return;
   3413    }
   3414 
   3415    // If this is a lazy area that hasn't been restored yet, we can't yet modify
   3416    // it - would would at least like to add to it. So we keep track of it in
   3417    // gFuturePlacements,  and use that to add it when restoring the area. We
   3418    // throw away aPosition though, as that can only be bogus if the area hasn't
   3419    // yet been restorted (caller can't possibly know where its putting the
   3420    // widget in relation to other widgets).
   3421    if (this.isAreaLazy(aArea)) {
   3422      gFuturePlacements.get(aArea).add(aWidgetId);
   3423      return;
   3424    }
   3425 
   3426    if (this.isSpecialWidget(aWidgetId)) {
   3427      aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
   3428    }
   3429 
   3430    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
   3431    if (oldPlacement && oldPlacement.area == aArea) {
   3432      this.moveWidgetWithinArea(aWidgetId, aPosition);
   3433      return;
   3434    }
   3435 
   3436    // Do nothing if the widget is not allowed to move to the target area.
   3437    if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
   3438      return;
   3439    }
   3440 
   3441    if (oldPlacement) {
   3442      this.removeWidgetFromArea(aWidgetId);
   3443    }
   3444 
   3445    if (!gPlacements.has(aArea)) {
   3446      gPlacements.set(aArea, [aWidgetId]);
   3447      aPosition = 0;
   3448    } else {
   3449      let placements = gPlacements.get(aArea);
   3450      if (typeof aPosition != "number") {
   3451        aPosition = placements.length;
   3452      }
   3453      if (aPosition < 0) {
   3454        aPosition = 0;
   3455      }
   3456      placements.splice(aPosition, 0, aWidgetId);
   3457    }
   3458 
   3459    let widget = gPalette.get(aWidgetId);
   3460    if (widget) {
   3461      widget.currentArea = aArea;
   3462      widget.currentPosition = aPosition;
   3463    }
   3464 
   3465    // We initially set placements with addWidgetToArea, so in that case
   3466    // we don't consider the area "dirtied".
   3467    if (!aInitialAdd) {
   3468      gDirtyAreaCache.add(aArea);
   3469    }
   3470 
   3471    gDirty = true;
   3472    this.saveState();
   3473 
   3474    this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
   3475  },
   3476 
   3477  /**
   3478   * @see CustomizableUI.removeWidgetFromArea
   3479   * @param {string} aWidgetId
   3480   */
   3481  removeWidgetFromArea(aWidgetId) {
   3482    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
   3483    if (!oldPlacement) {
   3484      return;
   3485    }
   3486 
   3487    if (!this.isWidgetRemovable(aWidgetId)) {
   3488      return;
   3489    }
   3490 
   3491    let placements = gPlacements.get(oldPlacement.area);
   3492    let position = placements.indexOf(aWidgetId);
   3493    if (position != -1) {
   3494      placements.splice(position, 1);
   3495    }
   3496 
   3497    let widget = gPalette.get(aWidgetId);
   3498    if (widget) {
   3499      widget.currentArea = null;
   3500      widget.currentPosition = null;
   3501    }
   3502 
   3503    gDirty = true;
   3504    this.saveState();
   3505    gDirtyAreaCache.add(oldPlacement.area);
   3506 
   3507    // If we're in vertical tabs, ensure we don't restore the widget when we toggle back
   3508    // to horizontal tabs.
   3509    if (!gInBatchStack && CustomizableUI.verticalTabsEnabled) {
   3510      if (oldPlacement.area == CustomizableUI.AREA_TABSTRIP) {
   3511        this.deleteWidgetInSavedHorizontalTabStripState(aWidgetId);
   3512      } else if (
   3513        oldPlacement.area == CustomizableUI.AREA_NAVBAR &&
   3514        this.getSavedHorizontalSnapshotState().includes(aWidgetId)
   3515      ) {
   3516        this.deleteWidgetInSavedHorizontalTabStripState(aWidgetId);
   3517        this.deleteWidgetInSavedNavBarWhenVerticalTabsState(aWidgetId);
   3518      }
   3519    }
   3520 
   3521    this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
   3522  },
   3523 
   3524  /**
   3525   * @see CustomizableUI.moveWidgetWithinArea
   3526   * @param {string} aWidgetId
   3527   * @param {number} aPosition
   3528   */
   3529  moveWidgetWithinArea(aWidgetId, aPosition) {
   3530    let oldPlacement = this.getPlacementOfWidget(aWidgetId);
   3531    if (!oldPlacement) {
   3532      return;
   3533    }
   3534 
   3535    let placements = gPlacements.get(oldPlacement.area);
   3536    if (typeof aPosition != "number") {
   3537      aPosition = placements.length;
   3538    } else if (aPosition < 0) {
   3539      aPosition = 0;
   3540    } else if (aPosition > placements.length) {
   3541      aPosition = placements.length;
   3542    }
   3543 
   3544    let widget = gPalette.get(aWidgetId);
   3545    if (widget) {
   3546      widget.currentPosition = aPosition;
   3547      widget.currentArea = oldPlacement.area;
   3548    }
   3549 
   3550    if (aPosition == oldPlacement.position) {
   3551      return;
   3552    }
   3553 
   3554    placements.splice(oldPlacement.position, 1);
   3555    // If we just removed the item from *before* where it is now added,
   3556    // we need to compensate the position offset for that:
   3557    if (oldPlacement.position < aPosition) {
   3558      aPosition--;
   3559    }
   3560    placements.splice(aPosition, 0, aWidgetId);
   3561 
   3562    gDirty = true;
   3563    gDirtyAreaCache.add(oldPlacement.area);
   3564 
   3565    this.saveState();
   3566 
   3567    this.notifyListeners(
   3568      "onWidgetMoved",
   3569      aWidgetId,
   3570      oldPlacement.area,
   3571      oldPlacement.position,
   3572      aPosition
   3573    );
   3574  },
   3575 
   3576  /**
   3577   * Returns the horizontal tab strip's placements state that was saved the
   3578   * last time we switched to vertical tabs mode. This state is an array of
   3579   * widget IDs that had been in the tab strip prior to switching to vertical
   3580   * tabs.
   3581   *
   3582   * @returns {string[]}
   3583   */
   3584  getSavedHorizontalSnapshotState() {
   3585    let state = [];
   3586    let prefValue = lazy.horizontalPlacementsPref;
   3587    if (prefValue) {
   3588      try {
   3589        state = JSON.parse(prefValue);
   3590      } catch (e) {
   3591        lazy.log.warn(
   3592          `Failed to parse value of ${kPrefCustomizationHorizontalTabstrip}`,
   3593          e
   3594        );
   3595      }
   3596    }
   3597    return state;
   3598  },
   3599 
   3600  /**
   3601   * Returns the vertical tab strip's placements state that was saved the
   3602   * last time we switched to horizontal tabs mode. This state is an array of
   3603   * widget IDs that had been in the vertical tab strip prior to switching to
   3604   * horizontal tabs.
   3605   *
   3606   * @returns {string[]}
   3607   */
   3608  getSavedVerticalSnapshotState() {
   3609    let state = [];
   3610    let prefValue = lazy.verticalPlacementsPref;
   3611    if (prefValue) {
   3612      try {
   3613        state = JSON.parse(prefValue);
   3614      } catch (e) {
   3615        lazy.log.warn(
   3616          `Failed to parse value of ${kPrefCustomizationNavBarWhenVerticalTabs}`,
   3617          e
   3618        );
   3619      }
   3620    }
   3621    return state;
   3622  },
   3623 
   3624  /**
   3625   * Loads the saved customization state objects from preferences or sets them
   3626   * to their defaults if no such state can be found.
   3627   *
   3628   * Note that this does not populate gPlacements, which is done lazily.
   3629   */
   3630  loadSavedState() {
   3631    let state = Services.prefs.getCharPref(kPrefCustomizationState, "");
   3632    if (!state) {
   3633      lazy.log.debug("No saved state found");
   3634      // Nothing has been customized, so silently fall back to the defaults.
   3635      return;
   3636    }
   3637    try {
   3638      gSavedState = JSON.parse(state);
   3639      if (typeof gSavedState != "object" || gSavedState === null) {
   3640        throw new Error("Invalid saved state");
   3641      }
   3642    } catch (e) {
   3643      Services.prefs.clearUserPref(kPrefCustomizationState);
   3644      gSavedState = {};
   3645      lazy.log.debug(
   3646        "Error loading saved UI customization state, falling back to defaults."
   3647      );
   3648    }
   3649 
   3650    if (!("placements" in gSavedState)) {
   3651      gSavedState.placements = {};
   3652    }
   3653 
   3654    if (!("currentVersion" in gSavedState)) {
   3655      gSavedState.currentVersion = 0;
   3656    }
   3657 
   3658    if (!("currentVersionBaseBrowser" in gSavedState)) {
   3659      gSavedState.currentVersionBaseBrowser = 0;
   3660    }
   3661 
   3662    if (!("currentVersionTorBrowser" in gSavedState)) {
   3663      gSavedState.currentVersionTorBrowser = 0;
   3664    }
   3665 
   3666    gSeenWidgets = new Set(gSavedState.seen || []);
   3667    gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
   3668    gNewElementCount = gSavedState.newElementCount || 0;
   3669  },
   3670 
   3671  /**
   3672   * Restores the placements of widgets within an area with ID aAreaId from
   3673   * saved state, or sets the default placements if no such saved state exists.
   3674   * This should be called during area registration, or after a state reset.
   3675   *
   3676   * @param {string} aAreaId
   3677   *   The ID of the area to restore the state for.
   3678   */
   3679  restoreStateForArea(aAreaId) {
   3680    let placementsPreexisted = gPlacements.has(aAreaId);
   3681 
   3682    this.beginBatchUpdate();
   3683    try {
   3684      gRestoring = true;
   3685 
   3686      let restored = false;
   3687      if (placementsPreexisted) {
   3688        lazy.log.debug(
   3689          "Restoring " + aAreaId + " from pre-existing placements"
   3690        );
   3691        for (let [position, id] of gPlacements.get(aAreaId).entries()) {
   3692          this.moveWidgetWithinArea(id, position);
   3693        }
   3694        gDirty = false;
   3695        restored = true;
   3696      } else {
   3697        gPlacements.set(aAreaId, []);
   3698      }
   3699 
   3700      if (!restored && gSavedState && aAreaId in gSavedState.placements) {
   3701        lazy.log.debug("Restoring " + aAreaId + " from saved state");
   3702        let placements = gSavedState.placements[aAreaId];
   3703        for (let id of placements) {
   3704          this.addWidgetToArea(id, aAreaId);
   3705        }
   3706        gDirty = false;
   3707        restored = true;
   3708      }
   3709 
   3710      if (!restored) {
   3711        lazy.log.debug("Restoring " + aAreaId + " from default state");
   3712        let defaults = gAreas.get(aAreaId).get("defaultPlacements");
   3713        if (
   3714          CustomizableUI.verticalTabsEnabled &&
   3715          gAreas.get(aAreaId).has("verticalTabsDefaultPlacements")
   3716        ) {
   3717          lazy.log.debug(
   3718            "Using verticalTabsDefaultPlacements to restore " + aAreaId
   3719          );
   3720          defaults = gAreas.get(aAreaId).get("verticalTabsDefaultPlacements");
   3721        }
   3722 
   3723        if (defaults) {
   3724          for (let id of defaults) {
   3725            this.addWidgetToArea(id, aAreaId, null, true);
   3726          }
   3727        }
   3728        gDirty = false;
   3729      }
   3730 
   3731      // Finally, add widgets to the area that were added before the it was able
   3732      // to be restored. This can occur when add-ons register widgets for a
   3733      // lazily-restored area before it's been restored.
   3734      if (gFuturePlacements.has(aAreaId)) {
   3735        let areaPlacements = gPlacements.get(aAreaId);
   3736        for (let id of gFuturePlacements.get(aAreaId)) {
   3737          if (areaPlacements.includes(id)) {
   3738            continue;
   3739          }
   3740          this.addWidgetToArea(id, aAreaId);
   3741        }
   3742        gFuturePlacements.delete(aAreaId);
   3743      }
   3744 
   3745      lazy.log.debug(
   3746        "Placements for " +
   3747          aAreaId +
   3748          ":\n\t" +
   3749          gPlacements.get(aAreaId).join("\n\t")
   3750      );
   3751 
   3752      gRestoring = false;
   3753    } finally {
   3754      this.endBatchUpdate();
   3755    }
   3756  },
   3757 
   3758  /**
   3759   * Adds widgets to the AREA_TABSTRIP area that were saved when switching away
   3760   * from horizontal tabs. This will effectively rebuild AREA_TABSTRIP to
   3761   * match the state in savedPlacements, and then clear the saved horizontal
   3762   * tab state.
   3763   *
   3764   * @param {string[]} [savedPlacements=this.getSavedHorizontalSnapshotState()]
   3765   *   The array of widget IDs to add to the AREA_TABSTRIP, in order. If this
   3766   *   set of placements doesn't include "tabbrowser-tabs" for some reason, the
   3767   *   whole set of placements is ignored and the tab strip is reset to the
   3768   *   defaults.
   3769   * @param {boolean} [isInitializing = false]
   3770   *   True if the horizontal tab strip is being rebuilt during CustomizableUI
   3771   *   initialization as opposed to via a pref-flip at runtime.
   3772   */
   3773  restoreSavedHorizontalTabStripState(
   3774    savedPlacements = this.getSavedHorizontalSnapshotState(),
   3775    isInitializing = false
   3776  ) {
   3777    const tabstripAreaId = CustomizableUI.AREA_TABSTRIP;
   3778    lazy.log.debug(
   3779      `restoreSavedHorizontalTabStripState, ${kPrefCustomizationHorizontalTabstrip} contained:`,
   3780      savedPlacements
   3781    );
   3782    // If there's no saved state, or it doesn't pass the sniff test, use
   3783    // default placements instead
   3784    if (!savedPlacements.includes("tabbrowser-tabs")) {
   3785      savedPlacements = gAreas.get(tabstripAreaId).get("defaultPlacements");
   3786      lazy.log.debug(`Using defaultPlacements for ${tabstripAreaId}`);
   3787    }
   3788 
   3789    lazy.log.debug(
   3790      `Replacing existing placements: ${gPlacements.get(
   3791        tabstripAreaId
   3792      )}, with ${savedPlacements}.`
   3793    );
   3794 
   3795    // Restore the tabstrip to either saved or default placements
   3796    this.beginBatchUpdate();
   3797    for (let [index, widgetId] of savedPlacements.entries()) {
   3798      this.addWidgetToArea(widgetId, tabstripAreaId, index, isInitializing);
   3799    }
   3800 
   3801    // Wipe the pref now that state is restored
   3802    Services.prefs.clearUserPref(kPrefCustomizationHorizontalTabstrip);
   3803 
   3804    // The vertical tabstrip area is supposed to be empty when we switch back to horizontal
   3805    if (gPlacements.get(CustomizableUI.AREA_VERTICAL_TABSTRIP)?.length) {
   3806      lazy.log.warn(
   3807        `Widgets remain in ${CustomizableUI.AREA_VERTICAL_TABSTRIP}:`,
   3808        gPlacements.get(CustomizableUI.AREA_VERTICAL_TABSTRIP)
   3809      );
   3810    }
   3811 
   3812    this.endBatchUpdate();
   3813  },
   3814 
   3815  /**
   3816   * Looks for a widget with a matching ID to aWidgetId within the saved
   3817   * horizontal tab strip state, and then deletes it from that state before
   3818   * saving that state to preferences.
   3819   *
   3820   * @see CustomizableUIInternal.saveHorizontalTabStripState
   3821   * @param {string} aWidgetId
   3822   *   The ID of the widget to remove from the saved horizontal tabstrip state.
   3823   */
   3824  deleteWidgetInSavedHorizontalTabStripState(aWidgetId) {
   3825    const savedPlacements = this.getSavedHorizontalSnapshotState();
   3826    let position = savedPlacements.indexOf(aWidgetId);
   3827    if (position != -1) {
   3828      savedPlacements.splice(position, 1);
   3829      this.saveHorizontalTabStripState(savedPlacements);
   3830    }
   3831  },
   3832 
   3833  /**
   3834   * Looks for a widget with a matching ID to aWidgetId within the navbar state
   3835   * that was saved when switching away from vertical tabs mode, and then
   3836   * deletes it from that state before saving that state to preferences.
   3837   *
   3838   * @see CustomizableUIInternal.saveNavBarWhenVerticalTabsState
   3839   * @param {string} aWidgetId
   3840   *   The ID of the widget to remove from the saved navbar state.
   3841   */
   3842  deleteWidgetInSavedNavBarWhenVerticalTabsState(aWidgetId) {
   3843    const savedPlacements = this.getSavedVerticalSnapshotState();
   3844    let position = savedPlacements.indexOf(aWidgetId);
   3845    if (position != -1) {
   3846      savedPlacements.splice(position, 1);
   3847      this.saveNavBarWhenVerticalTabsState(savedPlacements);
   3848    }
   3849  },
   3850 
   3851  /**
   3852   * Takes the current set of widgets placed within the horizontal tab strip
   3853   * and saves their IDs to a preference. This is used just before switching
   3854   * to vertical tabs mode (which moves some widgets around), and the saved
   3855   * state is used to restore the horizontal tab mode state of the tab
   3856   * strip.
   3857   *
   3858   * @param {string[]} [placements=[]]
   3859   *   The placements within the horizontal tab strip to save to preferences.
   3860   *   If this is the empty array, this method will use the current placements
   3861   *   of the tab strip automatically.
   3862   */
   3863  saveHorizontalTabStripState(placements = []) {
   3864    if (!placements.length) {
   3865      placements = this.getAreaPlacementsForSaving(
   3866        CustomizableUI.AREA_TABSTRIP
   3867      );
   3868    }
   3869    let serialized = JSON.stringify(placements, this.serializerHelper);
   3870    lazy.log.debug("Saving horizontal tabstrip state.", serialized);
   3871    Services.prefs.setCharPref(
   3872      kPrefCustomizationHorizontalTabstrip,
   3873      serialized
   3874    );
   3875  },
   3876 
   3877  /**
   3878   * Takes the current set of widgets placed within the navbar while in
   3879   * vertical tabs mode, and and saves their IDs to a preference. This is used
   3880   * just before switching to horizontal tabs mode (which moves some widgets
   3881   * around), and the saved state is used to restore the navbar's widget
   3882   * placements if the user switches back to vertical tabs.
   3883   *
   3884   * @param {string[]} [placements=[]]
   3885   *   The placements within the navbar to save to preferences. If this is the
   3886   *   empty array, this method will use the current placements of the navbar
   3887   *   automatically.
   3888   */
   3889  saveNavBarWhenVerticalTabsState(placements = []) {
   3890    if (!placements.length) {
   3891      placements = this.getAreaPlacementsForSaving(CustomizableUI.AREA_NAVBAR);
   3892    }
   3893    let serialized = JSON.stringify(placements, this.serializerHelper);
   3894    lazy.log.debug("Saving vertical navbar state.", serialized);
   3895    Services.prefs.setCharPref(
   3896      kPrefCustomizationNavBarWhenVerticalTabs,
   3897      serialized
   3898    );
   3899  },
   3900 
   3901  /**
   3902   * Returns the placements of widgets within a known area, regardless of
   3903   * whether or not the area has already been built, or is still registered.
   3904   * This means that we can get the placements for an area that was registered
   3905   * in the past, is no longer registered, but still exists within the
   3906   * saved state.
   3907   *
   3908   * @param {string} aAreaId
   3909   *   The ID of the area to get the placements for.
   3910   * @returns {string[]|undefined}
   3911   *   Returns the placements for the area, or undefined if the area is not
   3912   *   recognized.
   3913   */
   3914  getAreaPlacementsForSaving(aAreaId) {
   3915    // An early call to saveState can occur before all the lazy-area building is complete
   3916    let placements;
   3917    if (this.isAreaLazy(aAreaId) && gFuturePlacements.get(aAreaId)?.size) {
   3918      placements = [...gFuturePlacements.get(aAreaId)];
   3919    } else if (gPlacements.has(aAreaId)) {
   3920      placements = gPlacements.get(aAreaId);
   3921    }
   3922 
   3923    // Merge in previously saved areas if not present in gPlacements/gFuturePlacements.
   3924    // This way, state is still persisted for e.g. temporarily disabled
   3925    // add-ons - see bug 989338.
   3926    if (!placements && gSavedState && gSavedState.placements?.[aAreaId]) {
   3927      placements = gSavedState.placements[aAreaId];
   3928    }
   3929    lazy.log.debug(
   3930      `getAreaPlacementsForSaving for area: ${aAreaId}, gPlacements for area: ${gPlacements.get(
   3931        aAreaId
   3932      )}, returning: ${placements}`
   3933    );
   3934    return placements;
   3935  },
   3936 
   3937  /**
   3938   * Saves the current state of all customizable areas to preferences.
   3939   */
   3940  saveState() {
   3941    if (gInBatchStack || !gDirty) {
   3942      return;
   3943    }
   3944    // Clone because we want to modify this map:
   3945    let placements = new Map();
   3946    // Because of Bug 989338 and the risk of having area ids that aren't yet registered,
   3947    // we collect the areas from both gPlacements and gSavedState rather than gAreas.
   3948    let allAreaIds = new Set([...gPlacements.keys()]);
   3949    if (gSavedState?.placements) {
   3950      for (let area of Object.keys(gSavedState.placements)) {
   3951        allAreaIds.add(area);
   3952      }
   3953    }
   3954    for (let area of allAreaIds) {
   3955      placements.set(area, this.getAreaPlacementsForSaving(area));
   3956    }
   3957    let state = {
   3958      placements,
   3959      seen: gSeenWidgets,
   3960      dirtyAreaCache: gDirtyAreaCache,
   3961      currentVersion: kVersion,
   3962      currentVersionBaseBrowser: kVersionBaseBrowser,
   3963      currentVersionTorBrowser: kVersionTorBrowser,
   3964      newElementCount: gNewElementCount,
   3965    };
   3966 
   3967    lazy.log.debug("Saving state.");
   3968    let serialized = JSON.stringify(state, this.serializerHelper);
   3969    lazy.log.debug("State saved as: " + serialized);
   3970    Services.prefs.setCharPref(kPrefCustomizationState, serialized);
   3971    gDirty = false;
   3972  },
   3973 
   3974  /**
   3975   * This helper is passed to JSON.stringify when serializing the current
   3976   * customizable areas to preferences in saveState(). This does the work
   3977   * of serializing Map and Sets to objects and arrays, respectively.
   3978   *
   3979   * @see CustomizableUIInternal.saveState()
   3980   * @param {string|symbol} _aKey
   3981   * @param {any} aValue
   3982   * @returns {any}
   3983   */
   3984  serializerHelper(_aKey, aValue) {
   3985    if (typeof aValue == "object" && aValue.constructor.name == "Map") {
   3986      let result = {};
   3987      for (let [mapKey, mapValue] of aValue) {
   3988        result[mapKey] = mapValue;
   3989      }
   3990      return result;
   3991    }
   3992 
   3993    if (typeof aValue == "object" && aValue.constructor.name == "Set") {
   3994      return [...aValue];
   3995    }
   3996 
   3997    return aValue;
   3998  },
   3999 
   4000  /**
   4001   * @see CustomizableUI.beginBatchUpdate()
   4002   */
   4003  beginBatchUpdate() {
   4004    gInBatchStack++;
   4005  },
   4006 
   4007  /**
   4008   * @see CustomizableUI.endBatchUpdate()
   4009   * @param {boolean} aForceDirty
   4010   */
   4011  endBatchUpdate(aForceDirty) {
   4012    gInBatchStack--;
   4013    if (aForceDirty === true) {
   4014      gDirty = true;
   4015    }
   4016    if (gInBatchStack == 0) {
   4017      this.saveState();
   4018    } else if (gInBatchStack < 0) {
   4019      throw new Error(
   4020        "The batch editing stack should never reach a negative number."
   4021      );
   4022    }
   4023  },
   4024 
   4025  /**
   4026   * @see CustomizableUI.addListener
   4027   * @param {CustomizableUIListener} aListener
   4028   */
   4029  addListener(aListener) {
   4030    gListeners.add(aListener);
   4031  },
   4032 
   4033  /**
   4034   * @see CustomizableUI.removeListener
   4035   * @param {CustomizableUIListener} aListener
   4036   */
   4037  removeListener(aListener) {
   4038    if (aListener == this) {
   4039      return;
   4040    }
   4041 
   4042    gListeners.delete(aListener);
   4043  },
   4044 
   4045  /**
   4046   * For any listeners registered via `addListener` or `removeListener`, this
   4047   * calls the appropriate listener function for a particular CustomizableUI
   4048   * event if it is defined on the listener, passing along the arguments.
   4049   *
   4050   * @param {string} aListenerName
   4051   *   The name of the listener that should be called. This is a string
   4052   *   identifier of something that can be listened for via a
   4053   *   CustomizableUIListener - for example, "onWidgetCreated".
   4054   * @param  {...any} aArgs
   4055   *   The arguments to pass to the CustomizableUIListener function.
   4056   */
   4057  notifyListeners(aListenerName, ...aArgs) {
   4058    if (gRestoring) {
   4059      return;
   4060    }
   4061 
   4062    for (let listener of gListeners) {
   4063      try {
   4064        if (typeof listener[aListenerName] == "function") {
   4065          listener[aListenerName].apply(listener, aArgs);
   4066        }
   4067      } catch (e) {
   4068        lazy.log.error(e + " -- " + e.fileName + ":" + e.lineNumber);
   4069      }
   4070    }
   4071  },
   4072 
   4073  /**
   4074   * Constructs a CustomEvent with the aEventType type that is bubbling and
   4075   * cancelable, and includes the details passed on aDetails. This event is
   4076   * then dispatched on the gNavToolbox in aWindow.
   4077   *
   4078   * @see CustomizableUIInternal.dispatchToolboxEvent
   4079   * @param {string} aEventType
   4080   *   The type of the CustomEvent to fire.
   4081   * @param {any} aDetails
   4082   *   The details to assign to the event being fired.
   4083   * @param {DOMWindow} aWindow
   4084   *   The browser window containing the gNavToolbox which will have the event
   4085   *   dispatched on it.
   4086   */
   4087  _dispatchToolboxEventToWindow(aEventType, aDetails, aWindow) {
   4088    let evt = new aWindow.CustomEvent(aEventType, {
   4089      bubbles: true,
   4090      cancelable: true,
   4091      detail: aDetails,
   4092    });
   4093    aWindow.gNavToolbox.dispatchEvent(evt);
   4094  },
   4095 
   4096  /**
   4097   * Constructs a CustomEvent with the aEventType type that is bubbling and
   4098   * cancelable, and includes the details passed on aDetails. This event is
   4099   * then dispatched on the gNavToolbox in aWindow if one is provided. If no
   4100   * window is provided, this is dispatched on the gNavToolbox for all
   4101   * registered windows.
   4102   *
   4103   * @param {string} aEventType
   4104   *   The type of the CustomEvent to fire.
   4105   * @param {any} [aDetails={}]
   4106   *   The details to assign to the event being fired.
   4107   * @param {DOMWindow} [aWindow=null]
   4108   *   The browser window containing the gNavToolbox which will have the event
   4109   *   dispatched on it, or null to dispatch to all gNavToolbox elements in
   4110   *   all registered windows.
   4111   */
   4112  dispatchToolboxEvent(aEventType, aDetails = {}, aWindow = null) {
   4113    if (aWindow) {
   4114      this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
   4115      return;
   4116    }
   4117    for (let [win] of gBuildWindows) {
   4118      this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
   4119    }
   4120  },
   4121 
   4122  /**
   4123   * @see CustomizableUI.createWidget
   4124   * @param {CustomizableUICreateWidgetProperties} aProperties
   4125   * @returns {string}
   4126   *   The ID of the created widget.
   4127   */
   4128  createWidget(aProperties) {
   4129    let widget = this.normalizeWidget(
   4130      aProperties,
   4131      CustomizableUI.SOURCE_EXTERNAL
   4132    );
   4133    // XXXunf This should probably throw.
   4134    if (!widget) {
   4135      lazy.log.error("unable to normalize widget");
   4136      return undefined;
   4137    }
   4138 
   4139    gPalette.set(widget.id, widget);
   4140 
   4141    // Clear our caches:
   4142    gGroupWrapperCache.delete(widget.id);
   4143    for (let [win] of gBuildWindows) {
   4144      let cache = gSingleWrapperCache.get(win);
   4145      if (cache) {
   4146        cache.delete(widget.id);
   4147      }
   4148    }
   4149 
   4150    this.notifyListeners("onWidgetCreated", widget.id);
   4151 
   4152    if (widget.defaultArea) {
   4153      let addToDefaultPlacements = false;
   4154      let area = gAreas.get(widget.defaultArea);
   4155      if (
   4156        !CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
   4157        widget.defaultArea != CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
   4158      ) {
   4159        addToDefaultPlacements = true;
   4160      }
   4161 
   4162      if (addToDefaultPlacements) {
   4163        if (area.has("defaultPlacements")) {
   4164          area.get("defaultPlacements").push(widget.id);
   4165        } else {
   4166          area.set("defaultPlacements", [widget.id]);
   4167        }
   4168      }
   4169    }
   4170 
   4171    // Look through previously saved state to see if we're restoring a widget.
   4172    let seenAreas = new Set();
   4173    let widgetMightNeedAutoAdding = true;
   4174    for (let [area] of gPlacements) {
   4175      seenAreas.add(area);
   4176      let areaIsRegistered = gAreas.has(area);
   4177      let index = gPlacements.get(area).indexOf(widget.id);
   4178      if (index != -1) {
   4179        widgetMightNeedAutoAdding = false;
   4180        if (areaIsRegistered) {
   4181          widget.currentArea = area;
   4182          widget.currentPosition = index;
   4183        }
   4184        break;
   4185      }
   4186    }
   4187 
   4188    // Also look at saved state data directly in areas that haven't yet been
   4189    // restored. Can't rely on this for restored areas, as they may have
   4190    // changed.
   4191    if (widgetMightNeedAutoAdding && gSavedState) {
   4192      for (let area of Object.keys(gSavedState.placements)) {
   4193        if (seenAreas.has(area)) {
   4194          continue;
   4195        }
   4196 
   4197        let areaIsRegistered = gAreas.has(area);
   4198        let index = gSavedState.placements[area].indexOf(widget.id);
   4199        if (index != -1) {
   4200          widgetMightNeedAutoAdding = false;
   4201          if (areaIsRegistered) {
   4202            widget.currentArea = area;
   4203            widget.currentPosition = index;
   4204          }
   4205          break;
   4206        }
   4207      }
   4208    }
   4209 
   4210    // If we're restoring the widget to it's old placement, fire off the
   4211    // onWidgetAdded event - our own handler will take care of adding it to
   4212    // any build areas.
   4213    this.beginBatchUpdate();
   4214    try {
   4215      if (widget.currentArea) {
   4216        this.notifyListeners(
   4217          "onWidgetAdded",
   4218          widget.id,
   4219          widget.currentArea,
   4220          widget.currentPosition
   4221        );
   4222      } else if (widgetMightNeedAutoAdding) {
   4223        let autoAdd = Services.prefs.getBoolPref(
   4224          kPrefCustomizationAutoAdd,
   4225          true
   4226        );
   4227 
   4228        // If the widget doesn't have an existing placement, and it hasn't been
   4229        // seen before, then add it to its default area so it can be used.
   4230        // If the widget is not removable, we *have* to add it to its default
   4231        // area here.
   4232        let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
   4233        if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
   4234          if (widget.defaultArea) {
   4235            if (this.isAreaLazy(widget.defaultArea)) {
   4236              gFuturePlacements.get(widget.defaultArea).add(widget.id);
   4237            } else {
   4238              this.addWidgetToArea(widget.id, widget.defaultArea);
   4239            }
   4240          }
   4241        }
   4242 
   4243        // Extension widgets cannot enter the customization palette, so if
   4244        // at this point, we haven't found an area for them, move them into
   4245        // AREA_ADDONS.
   4246        if (
   4247          !widget.currentArea &&
   4248          CustomizableUI.isWebExtensionWidget(widget.id)
   4249        ) {
   4250          this.addWidgetToArea(widget.id, CustomizableUI.AREA_ADDONS);
   4251        }
   4252      }
   4253    } finally {
   4254      // Ensure we always have this widget in gSeenWidgets, and save
   4255      // state in case this needs to be done here.
   4256      gSeenWidgets.add(widget.id);
   4257      this.endBatchUpdate(true);
   4258    }
   4259 
   4260    this.notifyListeners(
   4261      "onWidgetAfterCreation",
   4262      widget.id,
   4263      widget.currentArea
   4264    );
   4265    return widget.id;
   4266  },
   4267 
   4268  /**
   4269   * Creates the widgets that are defined statically within the browser in
   4270   * CustomizableWidgets.
   4271   *
   4272   * @param {CustomizableUICreateWidgetProperties} aData
   4273   */
   4274  createBuiltinWidget(aData) {
   4275    // This should only ever be called on startup, before any windows are
   4276    // opened - so we know there's no build areas to handle. Also, builtin
   4277    // widgets are expected to be (mostly) static, so shouldn't affect the
   4278    // current placement settings.
   4279 
   4280    // This allows a widget to be both built-in by default but also able to be
   4281    // destroyed and removed from the area based on criteria that may not be
   4282    // available when the widget is created -- for example, because some other
   4283    // feature in the browser supersedes the widget.
   4284    let conditionalDestroyPromise = aData.conditionalDestroyPromise || null;
   4285    delete aData.conditionalDestroyPromise;
   4286 
   4287    let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
   4288    if (!widget) {
   4289      lazy.log.error("Error creating builtin widget: " + aData.id);
   4290      return;
   4291    }
   4292 
   4293    lazy.log.debug("Creating built-in widget with id: " + widget.id);
   4294    gPalette.set(widget.id, widget);
   4295 
   4296    if (conditionalDestroyPromise) {
   4297      conditionalDestroyPromise.then(
   4298        shouldDestroy => {
   4299          if (shouldDestroy) {
   4300            this.destroyWidget(widget.id);
   4301            this.removeWidgetFromArea(widget.id);
   4302          }
   4303        },
   4304        err => {
   4305          console.error(err);
   4306        }
   4307      );
   4308    }
   4309  },
   4310 
   4311  /**
   4312   * Returns true if the associated area will eventually lazily restore (but
   4313   * hasn't yet).
   4314   *
   4315   * @param {string} aAreaId
   4316   * @returns {boolean}
   4317   */
   4318  isAreaLazy(aAreaId) {
   4319    if (gPlacements.has(aAreaId) || !gAreas.has(aAreaId)) {
   4320      return false;
   4321    }
   4322    return gAreas.get(aAreaId).get("type") == CustomizableUI.TYPE_TOOLBAR;
   4323  },
   4324 
   4325  /**
   4326   * Given a set of CustomizableUICreateWidgetProperties, attempts to
   4327   * create a "normalized" version of that object with default values where
   4328   * aData failed to define values, as well as properly wrapped event handlers.
   4329   *
   4330   * @param {CustomizableUICreateWidgetProperties} aData
   4331   * @param {string} aSource
   4332   *   One of the CustomizableUI.SOURCE_* constants, for example
   4333   *   CustomizableUI.SOURCE_EXTERNAL.
   4334   * @returns {object}
   4335   *   The normalized widget representation. Notably, the `implementation`
   4336   *   property of this widget will point to the original aData structure.
   4337   */
   4338  normalizeWidget(aData, aSource) {
   4339    let widget = {
   4340      implementation: aData,
   4341      source: aSource || CustomizableUI.SOURCE_EXTERNAL,
   4342      instances: new Map(),
   4343      currentArea: null,
   4344      localized: true,
   4345      removable: true,
   4346      overflows: true,
   4347      defaultArea: null,
   4348      shortcutId: null,
   4349      tabSpecific: false,
   4350      locationSpecific: false,
   4351      tooltiptext: null,
   4352      l10nId: null,
   4353      showInPrivateBrowsing: true,
   4354      hideInNonPrivateBrowsing: false,
   4355      _introducedInVersion: -1,
   4356      _introducedByPref: null,
   4357      keepBroadcastAttributesWhenCustomizing: false,
   4358      disallowSubView: false,
   4359      webExtension: false,
   4360    };
   4361 
   4362    if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
   4363      lazy.log.error("Given an illegal id in normalizeWidget: " + aData.id);
   4364      return null;
   4365    }
   4366 
   4367    delete widget.implementation.currentArea;
   4368    widget.implementation.__defineGetter__(
   4369      "currentArea",
   4370      () => widget.currentArea
   4371    );
   4372 
   4373    const kReqStringProps = ["id"];
   4374    for (let prop of kReqStringProps) {
   4375      if (typeof aData[prop] != "string") {
   4376        lazy.log.error(
   4377          "Missing required property '" +
   4378            prop +
   4379            "' in normalizeWidget: " +
   4380            aData.id
   4381        );
   4382        return null;
   4383      }
   4384      widget[prop] = aData[prop];
   4385    }
   4386 
   4387    const kOptStringProps = ["l10nId", "label", "tooltiptext", "shortcutId"];
   4388    for (let prop of kOptStringProps) {
   4389      if (typeof aData[prop] == "string") {
   4390        widget[prop] = aData[prop];
   4391      }
   4392    }
   4393 
   4394    const kOptBoolProps = [
   4395      "removable",
   4396      "showInPrivateBrowsing",
   4397      "hideInNonPrivateBrowsing",
   4398      "overflows",
   4399      "tabSpecific",
   4400      "locationSpecific",
   4401      "localized",
   4402      "keepBroadcastAttributesWhenCustomizing",
   4403      "disallowSubView",
   4404      "webExtension",
   4405    ];
   4406    for (let prop of kOptBoolProps) {
   4407      if (typeof aData[prop] == "boolean") {
   4408        widget[prop] = aData[prop];
   4409      }
   4410    }
   4411 
   4412    // When we normalize builtin widgets, areas have not yet been registered:
   4413    if (
   4414      aData.defaultArea &&
   4415      (aSource == CustomizableUI.SOURCE_BUILTIN ||
   4416        gAreas.has(aData.defaultArea))
   4417    ) {
   4418      widget.defaultArea = aData.defaultArea;
   4419    } else if (!widget.removable) {
   4420      lazy.log.error(
   4421        "Widget '" +
   4422          widget.id +
   4423          "' is not removable but does not specify " +
   4424          "a valid defaultArea. That's not possible; it must specify a " +
   4425          "valid defaultArea as well."
   4426      );
   4427      return null;
   4428    }
   4429 
   4430    if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
   4431      widget.type = aData.type;
   4432    } else {
   4433      widget.type = "button";
   4434    }
   4435 
   4436    widget.disabled = aData.disabled === true;
   4437 
   4438    if (aSource == CustomizableUI.SOURCE_BUILTIN) {
   4439      widget._introducedInVersion = aData.introducedInVersion || 0;
   4440 
   4441      if (aData._introducedByPref) {
   4442        widget._introducedByPref = aData._introducedByPref;
   4443      }
   4444    }
   4445 
   4446    this.wrapWidgetEventHandler("onBeforeCreated", widget);
   4447    this.wrapWidgetEventHandler("onClick", widget);
   4448    this.wrapWidgetEventHandler("onCreated", widget);
   4449    this.wrapWidgetEventHandler("onDestroyed", widget);
   4450 
   4451    if (typeof aData.onBeforeCommand == "function") {
   4452      widget.onBeforeCommand = aData.onBeforeCommand;
   4453    }
   4454 
   4455    if (typeof aData.onCommand == "function") {
   4456      widget.onCommand = aData.onCommand;
   4457    }
   4458    if (
   4459      widget.type == "view" ||
   4460      widget.type == "button-and-view" ||
   4461      aData.viewId
   4462    ) {
   4463      if (typeof aData.viewId != "string") {
   4464        lazy.log.error(
   4465          "Expected a string for widget " +
   4466            widget.id +
   4467            " viewId, but got " +
   4468            aData.viewId
   4469        );
   4470        return null;
   4471      }
   4472      widget.viewId = aData.viewId;
   4473 
   4474      this.wrapWidgetEventHandler("onViewShowing", widget);
   4475      this.wrapWidgetEventHandler("onViewHiding", widget);
   4476    }
   4477    if (widget.type == "custom") {
   4478      this.wrapWidgetEventHandler("onBuild", widget);
   4479    }
   4480 
   4481    if (gPalette.has(widget.id)) {
   4482      return null;
   4483    }
   4484 
   4485    return widget;
   4486  },
   4487 
   4488  /**
   4489   * Given some widget definition object, forwards calls to functions with the
   4490   * name aEventName to the underlying implementation objects copy of that
   4491   * function.
   4492   *
   4493   * @param {string} aEventName
   4494   *   The name of the function to redirect to the underlying implementations
   4495   *   version of that same named function.
   4496   * @param {object} aWidget
   4497   *   A "normalized" widget definition as computed by `normalizeWidget()`.
   4498   */
   4499  wrapWidgetEventHandler(aEventName, aWidget) {
   4500    if (typeof aWidget.implementation[aEventName] != "function") {
   4501      aWidget[aEventName] = null;
   4502      return;
   4503    }
   4504    aWidget[aEventName] = function (...aArgs) {
   4505      try {
   4506        // Don't copy the function to the normalized widget object, instead
   4507        // keep it on the original object provided to the API so that
   4508        // additional methods can be implemented and used by the event
   4509        // handlers.
   4510        return aWidget.implementation[aEventName].apply(
   4511          aWidget.implementation,
   4512          aArgs
   4513        );
   4514      } catch (e) {
   4515        console.error(e);
   4516        return undefined;
   4517      }
   4518    };
   4519  },
   4520 
   4521  /**
   4522   * @see CustomizableUI.destroyWidget
   4523   * @param {string} aWidgetId
   4524   */
   4525  destroyWidget(aWidgetId) {
   4526    let widget = gPalette.get(aWidgetId);
   4527    if (!widget) {
   4528      gGroupWrapperCache.delete(aWidgetId);
   4529      for (let [window] of gBuildWindows) {
   4530        let windowCache = gSingleWrapperCache.get(window);
   4531        if (windowCache) {
   4532          windowCache.delete(aWidgetId);
   4533        }
   4534      }
   4535      return;
   4536    }
   4537 
   4538    // Remove it from the default placements of an area if it was added there:
   4539    if (widget.defaultArea) {
   4540      let area = gAreas.get(widget.defaultArea);
   4541      if (area) {
   4542        let defaultPlacements = area.get("defaultPlacements");
   4543        // We can assume this is present because if a widget has a defaultArea,
   4544        // we automatically create a defaultPlacements array for that area.
   4545        let widgetIndex = defaultPlacements.indexOf(aWidgetId);
   4546        if (widgetIndex != -1) {
   4547          defaultPlacements.splice(widgetIndex, 1);
   4548        }
   4549      }
   4550    }
   4551 
   4552    // This will not remove the widget from gPlacements - we want to keep the
   4553    // setting so the widget gets put back in it's old position if/when it
   4554    // returns.
   4555    for (let [window] of gBuildWindows) {
   4556      let windowCache = gSingleWrapperCache.get(window);
   4557      if (windowCache) {
   4558        windowCache.delete(aWidgetId);
   4559      }
   4560      let widgetNode =
   4561        window.document.getElementById(aWidgetId) ||
   4562        window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
   4563      if (widgetNode) {
   4564        let container = widgetNode.parentNode;
   4565        this.notifyListeners(
   4566          "onWidgetBeforeDOMChange",
   4567          widgetNode,
   4568          null,
   4569          container,
   4570          true
   4571        );
   4572        widgetNode.remove();
   4573        this.notifyListeners(
   4574          "onWidgetAfterDOMChange",
   4575          widgetNode,
   4576          null,
   4577          container,
   4578          true
   4579        );
   4580      }
   4581      if (
   4582        widget.type == "view" ||
   4583        widget.type == "button-and-view" ||
   4584        widget.viewId
   4585      ) {
   4586        let viewNode = window.document.getElementById(widget.viewId);
   4587        if (viewNode) {
   4588          for (let eventName of kSubviewEvents) {
   4589            let handler = "on" + eventName;
   4590            if (typeof widget[handler] == "function") {
   4591              viewNode.removeEventListener(eventName, widget[handler]);
   4592            }
   4593          }
   4594          viewNode._addedEventListeners = false;
   4595        }
   4596      }
   4597      if (widgetNode && widget.onDestroyed) {
   4598        widget.onDestroyed(window.document);
   4599      }
   4600    }
   4601 
   4602    gPalette.delete(aWidgetId);
   4603    gGroupWrapperCache.delete(aWidgetId);
   4604 
   4605    this.notifyListeners("onWidgetDestroyed", aWidgetId);
   4606  },
   4607 
   4608  /**
   4609   * @see CustomizableUI.getCustomizeTargetForArea
   4610   * @param {string} aArea
   4611   * @param {DOMWindow} aWindow
   4612   * @returns {Element}
   4613   */
   4614  getCustomizeTargetForArea(aArea, aWindow) {
   4615    let buildAreaNodes = gBuildAreas.get(aArea);
   4616    if (!buildAreaNodes) {
   4617      return null;
   4618    }
   4619 
   4620    for (let node of buildAreaNodes) {
   4621      if (node.ownerGlobal == aWindow) {
   4622        return this.getCustomizationTarget(node) || node;
   4623      }
   4624    }
   4625 
   4626    return null;
   4627  },
   4628 
   4629  /**
   4630   * @see CustomizableUI.reset()
   4631   */
   4632  reset() {
   4633    gResetting = true;
   4634    // CUI reset also implies resetting verticalTabs back to false.
   4635    // We do this before the rest of the reset so widgets are reset to their non-vertical
   4636    // positions.
   4637    Services.prefs.setBoolPref("sidebar.verticalTabs", false);
   4638    this._resetUIState();
   4639 
   4640    // Rebuild each registered area (across windows) to reflect the state that
   4641    // was reset above.
   4642    this._rebuildRegisteredAreas();
   4643 
   4644    for (let [widgetId, widget] of gPalette) {
   4645      if (widget.source == CustomizableUI.SOURCE_EXTERNAL) {
   4646        gSeenWidgets.add(widgetId);
   4647      }
   4648    }
   4649    if (gSeenWidgets.size || gNewElementCount) {
   4650      gDirty = true;
   4651      this.saveState();
   4652    }
   4653 
   4654    gResetting = false;
   4655  },
   4656 
   4657  /**
   4658   * Persists the current state to gUIStateBeforeReset (in order to temporarily
   4659   * allow for undoing CustomizableUI resets) and then blows away the current
   4660   * CustomizableUI state (including saved prefs) and sets them to their
   4661   * defaults. This also rebuilds all of the registered areas to reflect the
   4662   * defaults.
   4663   */
   4664  _resetUIState() {
   4665    try {
   4666      gUIStateBeforeReset.drawInTitlebar =
   4667        Services.prefs.getIntPref(kPrefDrawInTitlebar);
   4668      gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(
   4669        kPrefCustomizationState
   4670      );
   4671      gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity);
   4672      gUIStateBeforeReset.autoTouchMode =
   4673        Services.prefs.getBoolPref(kPrefAutoTouchMode);
   4674      gUIStateBeforeReset.currentTheme = gSelectedTheme;
   4675      gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref(
   4676        kPrefAutoHideDownloadsButton
   4677      );
   4678      gUIStateBeforeReset.newElementCount = gNewElementCount;
   4679      gUIStateBeforeReset.sidebarPositionStart = Services.prefs.getBoolPref(
   4680        kPrefSidebarPositionStartEnabled
   4681      );
   4682    } catch (e) {}
   4683 
   4684    Services.prefs.clearUserPref(kPrefCustomizationState);
   4685    Services.prefs.clearUserPref(kPrefDrawInTitlebar);
   4686    Services.prefs.clearUserPref(kPrefUIDensity);
   4687    Services.prefs.clearUserPref(kPrefAutoTouchMode);
   4688    Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton);
   4689    Services.prefs.clearUserPref(kPrefSidebarPositionStartEnabled);
   4690    gDefaultTheme.enable();
   4691    gNewElementCount = 0;
   4692    lazy.log.debug("State reset");
   4693 
   4694    // Later in the function, we're going to add any area-less extension
   4695    // buttons to the AREA_ADDONS area. We'll remember the old placements
   4696    // for that area so that we don't need to re-add widgets that are already
   4697    // in there in the DOM.
   4698    let oldAddonPlacements = gPlacements[CustomizableUI.AREA_ADDONS] || [];
   4699 
   4700    // Reset placements to make restoring default placements possible.
   4701    gPlacements = new Map();
   4702    gDirtyAreaCache = new Set();
   4703    gSeenWidgets = new Set();
   4704    // Clear the saved state to ensure that defaults will be used.
   4705    gSavedState = null;
   4706    // Restore the state for each area to its defaults
   4707    for (let [areaId] of gAreas) {
   4708      // If the Unified Extensions UI is enabled, we'll be adding any
   4709      // extension buttons that aren't already in AREA_ADDONS there,
   4710      // so we can skip restoring the state for it.
   4711      if (areaId != CustomizableUI.AREA_ADDONS) {
   4712        this.restoreStateForArea(areaId);
   4713      }
   4714    }
   4715 
   4716    // restoreStateForArea will have normally set an array for the placements
   4717    // for each area, but since we skip AREA_ADDONS intentionally, that array
   4718    // doesn't get set, so we do that manually here.
   4719    gPlacements.set(CustomizableUI.AREA_ADDONS, []);
   4720 
   4721    for (let [widgetId] of gPalette) {
   4722      if (
   4723        CustomizableUI.isWebExtensionWidget(widgetId) &&
   4724        !oldAddonPlacements.includes(widgetId)
   4725      ) {
   4726        // When resetting, NoScript goes to the toolbar instead. This matches
   4727        // its initial placement anyway. And since the button may be hidden by
   4728        // default by extensions.hideNoScript, we want to make sure that if it
   4729        // becomes unhidden it is shown rather than in the unified extensions
   4730        // panel. See tor-browser#41581.
   4731        this.addWidgetToArea(
   4732          widgetId,
   4733          widgetId === NoScriptId
   4734            ? CustomizableUI.AREA_NAVBAR
   4735            : CustomizableUI.AREA_ADDONS
   4736        );
   4737      }
   4738    }
   4739  },
   4740 
   4741  /**
   4742   * For all registered areas, builds those areas to reflect the current
   4743   * placement state of all widgets.
   4744   */
   4745  _rebuildRegisteredAreas() {
   4746    for (let [areaId, areaNodes] of gBuildAreas) {
   4747      let placements = gPlacements.get(areaId);
   4748      let isFirstChangedToolbar = true;
   4749      for (let areaNode of areaNodes) {
   4750        this.buildArea(areaId, placements, areaNode);
   4751 
   4752        let area = gAreas.get(areaId);
   4753        if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
   4754          let defaultCollapsed = area.get("defaultCollapsed");
   4755          let win = areaNode.ownerGlobal;
   4756          if (defaultCollapsed !== null) {
   4757            win.setToolbarVisibility(
   4758              areaNode,
   4759              typeof defaultCollapsed == "string"
   4760                ? defaultCollapsed
   4761                : !defaultCollapsed,
   4762              isFirstChangedToolbar
   4763            );
   4764          }
   4765        }
   4766        isFirstChangedToolbar = false;
   4767      }
   4768    }
   4769  },
   4770 
   4771  /**
   4772   * Undoes a previous reset, restoring the state of the UI to the state prior
   4773   * to the reset.
   4774   */
   4775  undoReset() {
   4776    if (
   4777      gUIStateBeforeReset.uiCustomizationState == null ||
   4778      gUIStateBeforeReset.drawInTitlebar == null
   4779    ) {
   4780      return;
   4781    }
   4782    gUndoResetting = true;
   4783 
   4784    const {
   4785      uiCustomizationState,
   4786      drawInTitlebar,
   4787      currentTheme,
   4788      uiDensity,
   4789      autoTouchMode,
   4790      autoHideDownloadsButton,
   4791      sidebarPositionStart,
   4792    } = gUIStateBeforeReset;
   4793    gNewElementCount = gUIStateBeforeReset.newElementCount;
   4794 
   4795    // Need to clear the previous state before setting the prefs
   4796    // because pref observers may check if there is a previous UI state.
   4797    this._clearPreviousUIState();
   4798 
   4799    Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
   4800    Services.prefs.setIntPref(kPrefDrawInTitlebar, drawInTitlebar);
   4801    Services.prefs.setIntPref(kPrefUIDensity, uiDensity);
   4802    Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode);
   4803    Services.prefs.setBoolPref(
   4804      kPrefAutoHideDownloadsButton,
   4805      autoHideDownloadsButton
   4806    );
   4807    Services.prefs.setBoolPref(
   4808      kPrefSidebarPositionStartEnabled,
   4809      sidebarPositionStart
   4810    );
   4811    currentTheme.enable();
   4812    this.loadSavedState();
   4813    // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
   4814    // and we don't need to do anything else here:
   4815    if (gSavedState) {
   4816      for (let areaId of Object.keys(gSavedState.placements)) {
   4817        let placements = gSavedState.placements[areaId];
   4818        gPlacements.set(areaId, placements);
   4819      }
   4820      this._rebuildRegisteredAreas();
   4821    }
   4822 
   4823    gUndoResetting = false;
   4824  },
   4825 
   4826  /**
   4827   * Clears the persisted state that was snapshotted just before the most
   4828   * recent reset of the state.
   4829   */
   4830  _clearPreviousUIState() {
   4831    Object.getOwnPropertyNames(gUIStateBeforeReset).forEach(prop => {
   4832      gUIStateBeforeReset[prop] = null;
   4833    });
   4834  },
   4835 
   4836  /**
   4837   * @param {string|Node} aWidget
   4838   *   Widget ID or a widget node (preferred for performance).
   4839   * @returns {boolean}
   4840   *   True if the widget is removable.
   4841   */
   4842  isWidgetRemovable(aWidget) {
   4843    let widgetId;
   4844    let widgetNode;
   4845    if (typeof aWidget == "string") {
   4846      widgetId = aWidget;
   4847    } else {
   4848      // Skipped items could just not have ids.
   4849      if (!aWidget.id && aWidget.getAttribute("skipintoolbarset") == "true") {
   4850        return false;
   4851      }
   4852      if (
   4853        !aWidget.id &&
   4854        !["toolbarspring", "toolbarspacer", "toolbarseparator"].includes(
   4855          aWidget.nodeName
   4856        )
   4857      ) {
   4858        throw new Error(
   4859          "No nodes without ids that aren't special widgets should ever come into contact with CUI"
   4860        );
   4861      }
   4862      // Use "spring" / "spacer" / "separator" for special widgets without ids
   4863      widgetId =
   4864        aWidget.id || aWidget.nodeName.substring(7 /* "toolbar".length */);
   4865      widgetNode = aWidget;
   4866    }
   4867    let provider = this.getWidgetProvider(widgetId);
   4868 
   4869    if (provider == CustomizableUI.PROVIDER_API) {
   4870      return gPalette.get(widgetId).removable;
   4871    }
   4872 
   4873    if (provider == CustomizableUI.PROVIDER_XUL) {
   4874      if (gBuildWindows.size == 0) {
   4875        // We don't have any build windows to look at, so just assume for now
   4876        // that its removable.
   4877        return true;
   4878      }
   4879 
   4880      if (!widgetNode) {
   4881        // Pick any of the build windows to look at.
   4882        let [window] = [...gBuildWindows][0];
   4883        [, widgetNode] = this.getWidgetNode(widgetId, window);
   4884      }
   4885      // If we don't have a node, we assume it's removable. This can happen because
   4886      // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
   4887      // for API-provided widgets which have been destroyed.
   4888      if (!widgetNode) {
   4889        return true;
   4890      }
   4891      return widgetNode.getAttribute("removable") == "true";
   4892    }
   4893 
   4894    // Otherwise this is either a special widget, which is always removable, or
   4895    // an API widget which has already been removed from gPalette. Returning true
   4896    // here allows us to then remove its ID from any placements where it might
   4897    // still occur.
   4898    return true;
   4899  },
   4900 
   4901  /**
   4902   * @see CustomizableUI.canWidgetMoveToArea
   4903   * @param {string} aWidgetId
   4904   * @param {string} aArea
   4905   * @returns {boolean}
   4906   */
   4907  canWidgetMoveToArea(aWidgetId, aArea) {
   4908    // Special widgets can't move to the menu panel.
   4909    if (
   4910      this.isSpecialWidget(aWidgetId) &&
   4911      gAreas.has(aArea) &&
   4912      gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL
   4913    ) {
   4914      return false;
   4915    }
   4916 
   4917    if (
   4918      aArea == CustomizableUI.AREA_ADDONS &&
   4919      !CustomizableUI.isWebExtensionWidget(aWidgetId)
   4920    ) {
   4921      return false;
   4922    }
   4923 
   4924    if (CustomizableUI.isWebExtensionWidget(aWidgetId)) {
   4925      // Extension widgets cannot move to the customization palette.
   4926      if (aArea == CustomizableUI.AREA_NO_AREA) {
   4927        return false;
   4928      }
   4929 
   4930      // Extension widgets cannot move to panels, with the exception of the
   4931      // AREA_ADDONS area.
   4932      if (
   4933        gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL &&
   4934        aArea != CustomizableUI.AREA_ADDONS
   4935      ) {
   4936        return false;
   4937      }
   4938    }
   4939 
   4940    let placement = this.getPlacementOfWidget(aWidgetId);
   4941    // Items in the palette can move, and items can move within their area:
   4942    if (!placement || placement.area == aArea) {
   4943      return true;
   4944    }
   4945    // For everything else, just return whether the widget is removable.
   4946    return this.isWidgetRemovable(aWidgetId);
   4947  },
   4948 
   4949  /**
   4950   * @see CustomizableUI.ensureWidgetPlacedInWindow
   4951   * @param {string} aWidgetId
   4952   * @param {DOMWindow} aWindow
   4953   * @returns {boolean}
   4954   */
   4955  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
   4956    let placement = this.getPlacementOfWidget(aWidgetId);
   4957    if (!placement) {
   4958      return false;
   4959    }
   4960    let areaNodes = gBuildAreas.get(placement.area);
   4961    if (!areaNodes) {
   4962      return false;
   4963    }
   4964    let container = [...areaNodes].filter(n => n.ownerGlobal == aWindow);
   4965    if (!container.length) {
   4966      return false;
   4967    }
   4968    let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
   4969    if (existingNode) {
   4970      return true;
   4971    }
   4972 
   4973    this.insertNodeInWindow(aWidgetId, container[0], true);
   4974    return true;
   4975  },
   4976 
   4977  /**
   4978   * Returns a list of all the widget IDs actively in this container, including
   4979   * any that are overflown for overflowable containers. Notably, this does NOT
   4980   * include IDs of widgets that have been previously placed within this
   4981   * container but are not currently registered (for example, for uninstalled
   4982   * extensions).
   4983   *
   4984   * @param {Element} container
   4985   * @returns {string[]}
   4986   *   The list of widget IDs that currently exist within container.
   4987   */
   4988  _getCurrentWidgetsInContainer(container) {
   4989    let currentWidgets = new Set();
   4990    function addUnskippedChildren(parent) {
   4991      for (let node of parent.children) {
   4992        let realNode =
   4993          node.localName == "toolbarpaletteitem"
   4994            ? node.firstElementChild
   4995            : node;
   4996        if (realNode.getAttribute("skipintoolbarset") != "true") {
   4997          currentWidgets.add(realNode.id);
   4998        }
   4999      }
   5000    }
   5001    addUnskippedChildren(this.getCustomizationTarget(container));
   5002    if (container.getAttribute("overflowing") == "true") {
   5003      let overflowTarget = container.getAttribute("default-overflowtarget");
   5004      addUnskippedChildren(
   5005        container.ownerDocument.getElementById(overflowTarget)
   5006      );
   5007      let webExtOverflowTarget = container.getAttribute(
   5008        "addon-webext-overflowtarget"
   5009      );
   5010      addUnskippedChildren(
   5011        container.ownerDocument.getElementById(webExtOverflowTarget)
   5012      );
   5013    }
   5014    // Then get the sorted list of placements, and filter based on the nodes
   5015    // that are present. This avoids including items that don't exist (e.g. ids
   5016    // of add-on items that the user has uninstalled).
   5017    let orderedPlacements = CustomizableUI.getWidgetIdsInArea(container.id);
   5018    return orderedPlacements.filter(w => {
   5019      return (
   5020        currentWidgets.has(w) ||
   5021        this.getWidgetProvider(w) == CustomizableUI.PROVIDER_API
   5022      );
   5023    });
   5024  },
   5025 
   5026  /**
   5027   * @type {boolean}
   5028   *   True if the CustomizableUI state of the browser is in the stock state
   5029   *   that is shipped by default.
   5030   */
   5031  get inDefaultState() {
   5032    if (CustomizableUI.verticalTabsEnabled) {
   5033      return false;
   5034    }
   5035    for (let [areaId, props] of gAreas) {
   5036      let defaultPlacements = props
   5037        .get("defaultPlacements")
   5038        .filter(item => this.widgetExists(item));
   5039      let currentPlacements = gPlacements.get(areaId);
   5040      // We're excluding all of the placement IDs for items that do not exist,
   5041      // and items that have removable="false",
   5042      // because we don't want to consider them when determining if we're
   5043      // in the default state. This way, if an add-on introduces a widget
   5044      // and is then uninstalled, the leftover placement doesn't cause us to
   5045      // automatically assume that the buttons are not in the default state.
   5046      let buildAreaNodes = gBuildAreas.get(areaId);
   5047      if (buildAreaNodes && buildAreaNodes.size) {
   5048        let container = [...buildAreaNodes][0];
   5049        let removableOrDefault = itemNodeOrItem => {
   5050          let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
   5051          let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
   5052          let isInDefault = defaultPlacements.includes(item);
   5053          return isRemovable || isInDefault;
   5054        };
   5055        // Toolbars need to deal with overflown widgets (if any) - so
   5056        // specialcase them:
   5057        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
   5058          currentPlacements =
   5059            this._getCurrentWidgetsInContainer(container).filter(
   5060              removableOrDefault
   5061            );
   5062        } else {
   5063          currentPlacements = currentPlacements.filter(item => {
   5064            let itemNode = container.getElementsByAttribute("id", item)[0];
   5065            return itemNode && removableOrDefault(itemNode || item);
   5066          });
   5067        }
   5068 
   5069        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
   5070          let collapsed = null;
   5071          let defaultCollapsed = props.get("defaultCollapsed");
   5072          let nondefaultState = false;
   5073          if (areaId == CustomizableUI.AREA_BOOKMARKS) {
   5074            collapsed = Services.prefs.getCharPref(
   5075              "browser.toolbars.bookmarks.visibility"
   5076            );
   5077            nondefaultState = Services.prefs.prefHasUserValue(
   5078              "browser.toolbars.bookmarks.visibility"
   5079            );
   5080          } else {
   5081            let attribute =
   5082              container.getAttribute("type") == "menubar"
   5083                ? "autohide"
   5084                : "collapsed";
   5085            collapsed = container.hasAttribute(attribute);
   5086            nondefaultState = collapsed != defaultCollapsed;
   5087          }
   5088          if (defaultCollapsed !== null && nondefaultState) {
   5089            lazy.log.debug(
   5090              "Found " +
   5091                areaId +
   5092                " had non-default toolbar visibility" +
   5093                "(expected " +
   5094                defaultCollapsed +
   5095                ", was " +
   5096                collapsed +
   5097                ")"
   5098            );
   5099            return false;
   5100          }
   5101        }
   5102      }
   5103      lazy.log.debug(
   5104        "Checking default state for " +
   5105          areaId +
   5106          ":\n" +
   5107          currentPlacements.join(",") +
   5108          "\nvs.\n" +
   5109          defaultPlacements.join(",")
   5110      );
   5111 
   5112      if (currentPlacements.length != defaultPlacements.length) {
   5113        return false;
   5114      }
   5115 
   5116      for (let i = 0; i < currentPlacements.length; ++i) {
   5117        if (
   5118          currentPlacements[i] != defaultPlacements[i] &&
   5119          !this.matchingSpecials(currentPlacements[i], defaultPlacements[i])
   5120        ) {
   5121          lazy.log.debug(
   5122            "Found " +
   5123              currentPlacements[i] +
   5124              " in " +
   5125              areaId +
   5126              " where " +
   5127              defaultPlacements[i] +
   5128              " was expected!"
   5129          );
   5130          return false;
   5131        }
   5132      }
   5133    }
   5134 
   5135    if (Services.prefs.prefHasUserValue(kPrefUIDensity)) {
   5136      lazy.log.debug(kPrefUIDensity + " pref is non-default");
   5137      return false;
   5138    }
   5139 
   5140    if (Services.prefs.prefHasUserValue(kPrefAutoTouchMode)) {
   5141      lazy.log.debug(kPrefAutoTouchMode + " pref is non-default");
   5142      return false;
   5143    }
   5144 
   5145    if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
   5146      lazy.log.debug(kPrefDrawInTitlebar + " pref is non-default");
   5147      return false;
   5148    }
   5149 
   5150    // This should just be `gDefaultTheme.isActive`, but bugs...
   5151    if (gDefaultTheme && gDefaultTheme.id != gSelectedTheme.id) {
   5152      lazy.log.debug(gSelectedTheme.id + " theme is non-default");
   5153      return false;
   5154    }
   5155 
   5156    if (Services.prefs.prefHasUserValue(kPrefSidebarPositionStartEnabled)) {
   5157      lazy.log.debug(kPrefSidebarPositionStartEnabled + " pref is non-default");
   5158      return false;
   5159    }
   5160 
   5161    return true;
   5162  },
   5163 
   5164  /**
   5165   * @see CustomizableUI.getCollapsedToolbarIds
   5166   * @param {Window} window
   5167   * @returns {Set<string>}
   5168   */
   5169  getCollapsedToolbarIds(window) {
   5170    let collapsedToolbars = new Set();
   5171    for (let toolbarId of CustomizableUIInternal.builtinToolbars) {
   5172      let toolbar = window.document.getElementById(toolbarId);
   5173 
   5174      // Menubar toolbars are special in that they're hidden with the autohide
   5175      // attribute.
   5176      let hidingAttribute =
   5177        toolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
   5178 
   5179      if (toolbar.hasAttribute(hidingAttribute)) {
   5180        collapsedToolbars.add(toolbarId);
   5181      }
   5182    }
   5183 
   5184    return collapsedToolbars;
   5185  },
   5186 
   5187  /**
   5188   * @see CustomizableUI.setToolbarVisibility
   5189   * @param {string} aToolbarId
   5190   * @param {boolean} aIsVisible
   5191   */
   5192  setToolbarVisibility(aToolbarId, aIsVisible) {
   5193    // We only persist the attribute the first time.
   5194    let isFirstChangedToolbar = true;
   5195    for (let window of CustomizableUI.windows) {
   5196      let toolbar = window.document.getElementById(aToolbarId);
   5197      if (toolbar) {
   5198        window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
   5199        isFirstChangedToolbar = false;
   5200      }
   5201    }
   5202  },
   5203 
   5204  /**
   5205   * @see CustomizableUI.widgetIsLikelyVisible
   5206   * @param {string} aWidgetId
   5207   * @param {Window} window
   5208   * @returns {boolean}
   5209   */
   5210  widgetIsLikelyVisible(aWidgetId, window) {
   5211    let placement = this.getPlacementOfWidget(aWidgetId);
   5212 
   5213    if (!placement) {
   5214      return false;
   5215    }
   5216 
   5217    switch (placement.area) {
   5218      case CustomizableUI.AREA_NAVBAR:
   5219        return true;
   5220      case CustomizableUI.AREA_MENUBAR:
   5221        return !this.getCollapsedToolbarIds(window).has(
   5222          CustomizableUI.AREA_MENUBAR
   5223        );
   5224      case CustomizableUI.AREA_TABSTRIP:
   5225        return !CustomizableUI.verticalTabsEnabled;
   5226      case CustomizableUI.AREA_BOOKMARKS:
   5227        return (
   5228          Services.prefs.getCharPref(
   5229            "browser.toolbars.bookmarks.visibility"
   5230          ) === "always"
   5231        );
   5232      default:
   5233        return false;
   5234    }
   5235  },
   5236 
   5237  /**
   5238   * nsIObserver implementation that observes for toolbar visibility changes
   5239   * or preference changes.
   5240   *
   5241   * @param {nsISupports} aSubject
   5242   * @param {string} aTopic
   5243   * @param {string} aData
   5244   */
   5245  observe(aSubject, aTopic, aData) {
   5246    if (aTopic == "browser-set-toolbar-visibility") {
   5247      let [toolbar, visibility] = JSON.parse(aData);
   5248      CustomizableUI.setToolbarVisibility(toolbar, visibility == "true");
   5249    }
   5250 
   5251    if (aTopic === "nsPref:changed") {
   5252      this.reconcileSidebarPrefs(aData);
   5253    }
   5254  },
   5255 
   5256  /**
   5257   * Initializes CustomizableUI for the current tab orientation.
   5258   *
   5259   * @param {boolean} toVertical
   5260   *   True if the tab orientation is vertical, false if horizontal.
   5261   */
   5262  initializeForTabsOrientation(toVertical) {
   5263    lazy.log.debug(
   5264      `initializeForTabsOrientation, toVertical: ${toVertical}, gCurrentVerticalTabs: ${gCurrentVerticalTabs}`
   5265    );
   5266    if (!toVertical) {
   5267      const savedPlacements = this.getSavedHorizontalSnapshotState();
   5268      lazy.log.debug(
   5269        "initializeForTabsOrientation, savedPlacements",
   5270        savedPlacements
   5271      );
   5272      if (savedPlacements.length) {
   5273        // We're startup up with horizontal tabs, but there are saved placements for the
   5274        // horizontal tab strip, so its possible the verticalTabs pref was updated outside
   5275        // of normal use. Make sure to restore those tabstrip widget placements
   5276        this.restoreSavedHorizontalTabStripState(savedPlacements, true);
   5277      } else {
   5278        // This is the default state and normal initialization will do everything necessary
   5279      }
   5280      gCurrentVerticalTabs = false;
   5281      return;
   5282    }
   5283 
   5284    // If the UI was already customized and saved, the earlier call to loadSavedState will
   5285    // have populated gSavedState from the pref. If not, we need to move the tabs into the
   5286    // vertical tabs area in the gSavedState. Then, the normal build-areas lifecycle
   5287    // can populate the needed toolbar placements and elements.
   5288    lazy.log.debug(
   5289      "initializeForTabsOrientation, toVertical=true, gSavedState",
   5290      gSavedState
   5291    );
   5292 
   5293    // If there are saved placement customizations, we need to manually move widgets
   5294    // around before we restore this state
   5295    let savedPlacements = gSavedState?.placements || {};
   5296    if (!savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP]?.length) {
   5297      savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP] =
   5298        gAreas
   5299          .get(CustomizableUI.AREA_VERTICAL_TABSTRIP)
   5300          .get("verticalTabsDefaultPlacements") || [];
   5301      lazy.log.debug(
   5302        "initializeForTabsOrientation, using defaults for AREA_VERTICAL_TABSTRIP",
   5303        savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP]
   5304      );
   5305    }
   5306    let tabstripPlacements =
   5307      savedPlacements[CustomizableUI.AREA_TABSTRIP] || [];
   5308    // also pick up any widgets already in gFuturePlacements so we can wipe that
   5309    if (gFuturePlacements.has(CustomizableUI.AREA_TABSTRIP)) {
   5310      for (let id of gFuturePlacements.get(CustomizableUI.AREA_TABSTRIP)) {
   5311        if (!tabstripPlacements.includes(id)) {
   5312          tabstripPlacements.push(id);
   5313        }
   5314      }
   5315      gFuturePlacements.delete(CustomizableUI.AREA_TABSTRIP);
   5316    }
   5317    // Take a copy we can save and restore to, ensuring there's a sane default
   5318    let savedTabstripPlacements = tabstripPlacements.length
   5319      ? [...tabstripPlacements]
   5320      : gAreas.get(CustomizableUI.AREA_TABSTRIP).get("defaultPlacements");
   5321 
   5322    // now we can remove the saved placements so they don't get picked back up again later in startup
   5323    delete savedPlacements[CustomizableUI.AREA_TABSTRIP];
   5324 
   5325    let widgetsMoved = [];
   5326    for (let widgetId of tabstripPlacements) {
   5327      if (widgetId == "tabbrowser-tabs") {
   5328        lazy.log.debug(
   5329          `Moving saved tabbrowser-tabs to AREA_VERTICAL_TABSTRIP`
   5330        );
   5331        this.addWidgetToArea(
   5332          widgetId,
   5333          CustomizableUI.AREA_VERTICAL_TABSTRIP,
   5334          null,
   5335          true
   5336        );
   5337        continue;
   5338      }
   5339      // if this is a extension, those are handled in a toolbarvisibilitychange handler in browser-addons.js
   5340      if (CustomizableUI.isWebExtensionWidget(widgetId)) {
   5341        lazy.log.debug(`Skipping a webextension saved placement ${widgetId}`);
   5342        continue;
   5343      }
   5344      // Everything else gets moved to the nav-bar area while tabs are vertical
   5345      lazy.log.debug(`Moving saved placement ${widgetId} to nav-bar`);
   5346      this.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR, null, true);
   5347      widgetsMoved.push(widgetId);
   5348    }
   5349    lazy.log.debug(
   5350      "initializeForTabsOrientation, widgets moved:",
   5351      widgetsMoved
   5352    );
   5353    if (widgetsMoved.length) {
   5354      // We've updated the areas, so we don't need to do this again post-initialization
   5355      gCurrentVerticalTabs = true;
   5356    }
   5357 
   5358    // Remove new tab from AREA_NAVBAR when vertical tabs enabled.
   5359    this.removeWidgetFromArea("new-tab-button");
   5360 
   5361    // If we've ended up with a non-default CUI state and vertical tabs enabled, ensure
   5362    // there's a sane snapshot to revert to
   5363    if (!lazy.horizontalPlacementsPref) {
   5364      lazy.log.debug(
   5365        `verticalTabsEnabled but ${kPrefCustomizationHorizontalTabstrip} is empty`
   5366      );
   5367      CustomizableUIInternal.saveHorizontalTabStripState(
   5368        savedTabstripPlacements
   5369      );
   5370    }
   5371  },
   5372 
   5373  /**
   5374   * Currently, the new sidebar and vertical tabs have a tight relationship
   5375   * with one another (specifically, the new sidebar is a dependency for
   5376   * vertical tabs). They are, however, controlled by two separate preferences.
   5377   * This function does the work of changing the vertical tabs state if the
   5378   * sidebar pref changes, and vice-versa.
   5379   *
   5380   * @param {string} prefChanged
   5381   *   The key for the preference that changed.
   5382   */
   5383  reconcileSidebarPrefs(prefChanged) {
   5384    let sidebarRevampEnabled = Services.prefs.getBoolPref(
   5385      kPrefSidebarRevampEnabled,
   5386      false
   5387    );
   5388    let verticalTabsEnabled = Services.prefs.getBoolPref(
   5389      kPrefSidebarVerticalTabsEnabled,
   5390      false
   5391    );
   5392    let positionStartEnabled = Services.prefs.getBoolPref(
   5393      kPrefSidebarPositionStartEnabled,
   5394      true
   5395    );
   5396    lazy.log.debug(
   5397      `reconcileSidebarPrefs, kPrefSidebarRevampEnabled: {sidebarRevampEnabled}, kPrefSidebarVerticalTabsEnabled: ${verticalTabsEnabled}`
   5398    );
   5399    switch (prefChanged) {
   5400      case kPrefSidebarVerticalTabsEnabled: {
   5401        // We need to also enable sidebar.revamp if vertical tabs gets enabled
   5402        if (verticalTabsEnabled && !sidebarRevampEnabled) {
   5403          Services.prefs.setBoolPref(kPrefSidebarRevampEnabled, true);
   5404        }
   5405        break;
   5406      }
   5407      case kPrefSidebarRevampEnabled: {
   5408        // If we are changing the pref after startup, update the nav bar defaultPlacements to include/exclude sidebar-button
   5409        let props = gAreas.get(CustomizableUI.AREA_NAVBAR);
   5410        let defaults = props.get("defaultPlacements");
   5411        let sidebarButtonIndex = defaults.indexOf("sidebar-button");
   5412        if (sidebarRevampEnabled && sidebarButtonIndex < 0) {
   5413          defaults.unshift("sidebar-button");
   5414        } else if (!sidebarRevampEnabled && sidebarButtonIndex > -1) {
   5415          defaults.splice(sidebarButtonIndex, 1);
   5416        }
   5417        props.set("defaultPlacements", defaults);
   5418        gAreas.set(CustomizableUI.AREA_NAVBAR, props);
   5419        // We need to also disable vertical tabs if sidebar.revamp is no longer enabled
   5420        if (!sidebarRevampEnabled && verticalTabsEnabled) {
   5421          lazy.log.debug(
   5422            `{kPrefSidebarRevampEnabled} disabled, so also disabling ${kPrefSidebarVerticalTabsEnabled}`
   5423          );
   5424          Services.prefs.setBoolPref(kPrefSidebarVerticalTabsEnabled, false);
   5425        }
   5426        break;
   5427      }
   5428      case kPrefSidebarPositionStartEnabled: {
   5429        // If the sidebar moves to the left or right, move the toolbar button along with it.
   5430        const navbarPlacements = gPlacements.get(CustomizableUI.AREA_NAVBAR);
   5431        const index = navbarPlacements.indexOf("sidebar-button");
   5432        if (!positionStartEnabled && index === 0) {
   5433          this.moveWidgetWithinArea("sidebar-button", navbarPlacements.length);
   5434        }
   5435        if (positionStartEnabled && index === navbarPlacements.length - 1) {
   5436          this.moveWidgetWithinArea("sidebar-button", 0);
   5437        }
   5438      }
   5439    }
   5440  },
   5441 
   5442  /**
   5443   * @type {boolean}
   5444   *   True if the horizontal and vertical tabstrips have been registered.
   5445   */
   5446  get tabstripAreasReady() {
   5447    return (
   5448      gBuildAreas.get(CustomizableUI.AREA_TABSTRIP)?.size &&
   5449      gBuildAreas.get(CustomizableUI.AREA_VERTICAL_TABSTRIP)?.size
   5450    );
   5451  },
   5452 
   5453  /**
   5454   * Updates the vertical or horizontal state of the tabstrip to best match
   5455   * the current preference value.
   5456   */
   5457  updateTabStripOrientation() {
   5458    if (!this.tabstripAreasReady) {
   5459      lazy.log.debug("tabstrip build areas not yet ready");
   5460      return;
   5461    }
   5462    let toVertical = CustomizableUI.verticalTabsEnabled;
   5463    if (toVertical === gCurrentVerticalTabs) {
   5464      lazy.log.debug("early return as the value hasn't changed");
   5465      return;
   5466    }
   5467    lazy.log.debug(
   5468      `verticalTabs changed, from ${gCurrentVerticalTabs}, to ${toVertical}`
   5469    );
   5470 
   5471    if (toVertical && gCurrentVerticalTabs !== null) {
   5472      // Stash current placements as a state we can restore to when going back to horizontal tabs
   5473      lazy.log.debug(
   5474        "Switching to vertical tabs post-initialization, so capturing tabstrip placements snapshot"
   5475      );
   5476      CustomizableUIInternal.saveHorizontalTabStripState();
   5477    }
   5478    gCurrentVerticalTabs = toVertical;
   5479 
   5480    function changeWidgetRemovability(widgetId, removable) {
   5481      let widget = CustomizableUI.getWidget(widgetId);
   5482      for (let { node } of widget.instances) {
   5483        if (node) {
   5484          node.setAttribute("removable", removable.toString());
   5485        }
   5486      }
   5487    }
   5488 
   5489    // Normally these aren't removable, but for this operation only we need to move them
   5490    changeWidgetRemovability("tabbrowser-tabs", true);
   5491 
   5492    if (toVertical) {
   5493      lazy.log.debug(
   5494        `Switching to verticalTabs=true in updateTabStripOrientation`
   5495      );
   5496      gDirty = true;
   5497 
   5498      if (
   5499        !Services.prefs.getCharPref(kPrefCustomizationHorizontalTabsBackup, "")
   5500      ) {
   5501        // Before we switch for the first time, take a back up just in case we need an escape hatch
   5502        Services.prefs.setCharPref(
   5503          kPrefCustomizationHorizontalTabsBackup,
   5504          Services.prefs.getCharPref(kPrefCustomizationState, "")
   5505        );
   5506      }
   5507 
   5508      CustomizableUI.beginBatchUpdate();
   5509      let customVerticalNavbarPlacements = this.getSavedVerticalSnapshotState();
   5510      let tabstripPlacements = this.getSavedHorizontalSnapshotState();
   5511      const isSidebarLast =
   5512        gPlacements.get(CustomizableUI.AREA_NAVBAR).at(-1) === "sidebar-button";
   5513      // Remove non-default widgets to the nav-bar
   5514      for (let id of CustomizableUI.getWidgetIdsInArea("TabsToolbar")) {
   5515        if (id == "tabbrowser-tabs") {
   5516          CustomizableUI.addWidgetToArea(
   5517            id,
   5518            CustomizableUI.AREA_VERTICAL_TABSTRIP
   5519          );
   5520          continue;
   5521        }
   5522        // We add the tab strip placements later in the case they have a custom position
   5523        if (
   5524          tabstripPlacements.includes(id) &&
   5525          customVerticalNavbarPlacements.includes(id)
   5526        ) {
   5527          continue;
   5528        }
   5529        if (!CustomizableUI.isWidgetRemovable(id)) {
   5530          continue;
   5531        }
   5532        // if this is a extension, those are handled in a toolbarvisibilitychange handler in browser-addons.js
   5533        if (CustomizableUI.isWebExtensionWidget(id)) {
   5534          continue;
   5535        }
   5536        // Everything else gets moved to the nav-bar area while tabs are vertical
   5537        CustomizableUI.addWidgetToArea(id, CustomizableUI.AREA_NAVBAR);
   5538      }
   5539      // Remove new tab from nav-bar when vertical tabs enabled
   5540      this.removeWidgetFromArea("new-tab-button");
   5541      customVerticalNavbarPlacements.forEach((id, index) => {
   5542        if (tabstripPlacements.includes(id)) {
   5543          CustomizableUI.addWidgetToArea(id, CustomizableUI.AREA_NAVBAR, index);
   5544        }
   5545      });
   5546      // If sidebar was previously the last widget in navbar, carry it over to
   5547      // the end of the newly constructed navbar.
   5548      if (isSidebarLast) {
   5549        this.addWidgetToArea("sidebar-button", CustomizableUI.AREA_NAVBAR);
   5550      }
   5551      CustomizableUI.endBatchUpdate();
   5552    } else {
   5553      this.saveNavBarWhenVerticalTabsState();
   5554      // We're switching to vertical in this session; pull saved state from pref and update placements
   5555      this.restoreSavedHorizontalTabStripState();
   5556    }
   5557    // Give the sidebar a chance to adjust before we show/hide the toolbars
   5558    lazy.log.debug("CustomizableUI notifying tabstrip-orientation-change");
   5559    Services.obs.notifyObservers(null, "tabstrip-orientation-change", {
   5560      isVertical: toVertical,
   5561    });
   5562 
   5563    this.setToolbarVisibility(
   5564      CustomizableUI.AREA_VERTICAL_TABSTRIP,
   5565      toVertical
   5566    );
   5567    this.setToolbarVisibility(CustomizableUI.AREA_TABSTRIP, !toVertical);
   5568    changeWidgetRemovability("tabbrowser-tabs", false);
   5569 
   5570    for (let [win] of gBuildWindows) {
   5571      win.TabBarVisibility.update(true);
   5572    }
   5573  },
   5574 };
   5575 Object.freeze(CustomizableUIInternal);
   5576 
   5577 /**
   5578 * This is the publicly exposed interface CustomizableUI. It uses old-school
   5579 * encapsulation by forwarding most method calls to CustomizableUIInternal,
   5580 * which is not exported.
   5581 */
   5582 export var CustomizableUI = {
   5583  /**
   5584   * Constant reference to the ID of the navigation toolbar.
   5585   */
   5586  AREA_NAVBAR: "nav-bar",
   5587  /**
   5588   * Constant reference to the ID of the menubar's toolbar.
   5589   */
   5590  AREA_MENUBAR: "toolbar-menubar",
   5591  /**
   5592   * Constant reference to the ID of the tabstrip toolbar.
   5593   */
   5594  AREA_TABSTRIP: "TabsToolbar",
   5595 
   5596  /**
   5597   * Constant reference to the ID of the vertical tabstrip toolbar.
   5598   */
   5599  AREA_VERTICAL_TABSTRIP: "vertical-tabs",
   5600 
   5601  /**
   5602   * Constant reference to the ID of the bookmarks toolbar.
   5603   */
   5604  AREA_BOOKMARKS: "PersonalToolbar",
   5605  /**
   5606   * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
   5607   */
   5608  AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
   5609  /**
   5610   * Constant reference to the ID of the addons area.
   5611   */
   5612  AREA_ADDONS: "unified-extensions-area",
   5613  /**
   5614   * Constant reference to the ID of the customization palette, which is
   5615   * where widgets go when they're not assigned to an area. Note that this
   5616   * area is "virtual" in that it's never set as a value for a widgets
   5617   * currentArea or defaultArea. It's only used for the `canWidgetMoveToArea`
   5618   * function to check if widgets can be moved to the palette. Callers who
   5619   * wish to move items to the palette should use `removeWidgetFromArea`.
   5620   */
   5621  AREA_NO_AREA: "customization-palette",
   5622  /**
   5623   * Constant indicating the area is a panel.
   5624   */
   5625  TYPE_PANEL: "panel",
   5626  /**
   5627   * Constant indicating the area is a toolbar.
   5628   */
   5629  TYPE_TOOLBAR: "toolbar",
   5630 
   5631  /**
   5632   * Constant indicating a XUL-type provider.
   5633   */
   5634  PROVIDER_XUL: "xul",
   5635  /**
   5636   * Constant indicating an API-type provider.
   5637   */
   5638  PROVIDER_API: "api",
   5639  /**
   5640   * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
   5641   */
   5642  PROVIDER_SPECIAL: "special",
   5643 
   5644  /**
   5645   * Constant indicating the widget is built-in
   5646   */
   5647  SOURCE_BUILTIN: "builtin",
   5648  /**
   5649   * Constant indicating the widget is externally provided
   5650   * (e.g. by add-ons or other items not part of the builtin widget set).
   5651   */
   5652  SOURCE_EXTERNAL: "external",
   5653 
   5654  /**
   5655   * Constant indicating the reason the event was fired was a window closing
   5656   */
   5657  REASON_WINDOW_CLOSED: "window-closed",
   5658  /**
   5659   * Constant indicating the reason the event was fired was an area being
   5660   * unregistered separately from window closing mechanics.
   5661   */
   5662  REASON_AREA_UNREGISTERED: "area-unregistered",
   5663 
   5664  /**
   5665   * An iteratable property of windows managed by CustomizableUI.
   5666   * Note that this can *only* be used as an iterator. ie:
   5667   *     for (let window of CustomizableUI.windows) { ... }
   5668   */
   5669  windows: {
   5670    *[Symbol.iterator]() {
   5671      for (let [window] of gBuildWindows) {
   5672        yield window;
   5673      }
   5674    },
   5675  },
   5676 
   5677  get verticalTabsEnabled() {
   5678    return lazy.verticalTabsPref;
   5679  },
   5680 
   5681  /**
   5682   * Fired when a widget is added to an area.
   5683   *
   5684   * @callback CustomizableUIOnWidgetAddedCallback
   5685   * @param {string} aWidgetId
   5686   *   The ID of the widget that was added to an area.
   5687   * @param {string} aArea
   5688   *   The ID of the area that the widget was added to.
   5689   * @param {number} aPosition
   5690   *   The position of the widget in the area that it was added to.
   5691   */
   5692 
   5693  /**
   5694   * Fired when a widget is moved within its area.
   5695   *
   5696   * @callback CustomizableUIOnWidgetMovedCallback
   5697   * @param {string} aWidgetId
   5698   *   The ID of the widget that was moved.
   5699   * @param {string} aArea
   5700   *   The ID of the area that the widget was moved within.
   5701   * @param {number} aOldPosition
   5702   *   The original position of the widget before being moved.
   5703   * @param {number} aNewPosition
   5704   *   The new position of the widget after being moved.
   5705   */
   5706 
   5707  /**
   5708   * Fired when a widget is removed from an area.
   5709   *
   5710   * @callback CustomizableUIOnWidgetRemovedCallback
   5711   * @param {string} aWidgetId
   5712   *   The ID of the widget that was removed.
   5713   * @param {string} aArea
   5714   *   The ID of the area that the widget was removed from.
   5715   */
   5716 
   5717  /**
   5718   * Fired *before* a widget's DOM node is acted upon by CustomizableUI
   5719   * (to add, move or remove it).
   5720   *
   5721   * @callback CustomizableUIOnWidgetBeforeDOMChange
   5722   * @param {Element} aNode
   5723   *   The DOM node being acted upon.
   5724   * @param {Element|null} aNextNode
   5725   *   The DOM node (if any) before which a widget will be inserted.
   5726   * @param {Element} aContainer
   5727   *   The *actual* DOM container for the widget (could be an overflow panel in
   5728   *   case of an overflowable toolbar).
   5729   * @param {boolean} aWasRemoval
   5730   *   True iff the action about to happen is the removal of the DOM node.
   5731   */
   5732 
   5733  /**
   5734   * Fired *after* a widget's DOM node is acted upon by CustomizableUI
   5735   * (to add, move or remove it).
   5736   *
   5737   * @callback CustomizableUIOnWidgetAfterDOMChange
   5738   * @param {Element} aNode
   5739   *   The DOM node that was acted upon.
   5740   * @param {Element|null} aNextNode
   5741   *   The DOM node (if any) that the widget was inserted before.
   5742   * @param {Element} aContainer
   5743   *   The *actual* DOM container for the widget (could be an overflow panel in
   5744   *   case of an overflowable toolbar).
   5745   * @param {boolean} aWasRemoval
   5746   *   True iff the action that happened was the removal of the DOM node.
   5747   */
   5748 
   5749  /**
   5750   * Fired after a reset to default placements moves a widget's node to a
   5751   * different location.
   5752   *
   5753   * @callback CustomizableUIOnWidgetReset
   5754   * @param {Element} aNode
   5755   *   The DOM node for the widget that was moved.
   5756   * @param {Element} aContainer
   5757   *   The *actual* DOM container for the widget (could be an overflow panel in
   5758   *   case of an overflowable toolbar) after the reset. (NB: it might already
   5759   *   have been there and been moved to a different position!)
   5760   */
   5761 
   5762  /**
   5763   * Fired after undoing a reset to default placements moves a widget's
   5764   * node to a different location.
   5765   *
   5766   * @callback CustomizableUIOnWidgetUndoMove
   5767   * @param {Element} aNode
   5768   *   The DOM node for the widget that was moved after the undo.
   5769   * @param {Element} aContainer
   5770   *   The *actual* DOM container for the widget (could be an overflow panel in
   5771   *   case of an overflowable toolbar) after the undo-move. (NB: it might
   5772   *   already have been there and been moved to a different position!)
   5773   */
   5774 
   5775  /**
   5776   * Fired when a widget with id aWidgetId has been created, but before it
   5777   * is added to any placements or any DOM nodes have been constructed.
   5778   * Only fired for API-based widgets.
   5779   *
   5780   * @callback CustomizableUIOnWidgetCreated
   5781   * @param {string} aWidgetId
   5782   *   The ID of the widget that was created.
   5783   */
   5784 
   5785  /**
   5786   * Fired after a reset to default placements is complete on an area's
   5787   * DOM node. Note that this is fired for each DOM node across all windows.
   5788   *
   5789   * @callback CustomizableUIOnAreaReset
   5790   * @param {string} aArea
   5791   *   The ID for the area that was reset.
   5792   * @param {Element} aContainer
   5793   *   The DOM node for the area that was reset.
   5794   */
   5795 
   5796  /**
   5797   * Fired after a widget with id aWidgetId has been created, and has been
   5798   * added to either its default area or the area in which it was placed
   5799   * previously. If the widget has no default area and/or it has never
   5800   * been placed anywhere, aArea may be null. Only fired for API-based
   5801   * widgets.
   5802   *
   5803   * @callback CustomizableUIOnWidgetAfterCreation
   5804   * @param {string} aWidgetId
   5805   *   The ID of the widget that was just created.
   5806   * @param {string|null} aArea
   5807   *   The ID of the area that the widget was placed in, or null if it is
   5808   *   now in the customization palette.
   5809   */
   5810 
   5811  /**
   5812   * Fired when a widget is destroyed. Only fired for API-based widgets.
   5813   *
   5814   * @callback CustomizableUIOnWidgetDestroyed
   5815   * @param {string} aWidgetId
   5816   *   The ID of the widget that was destroyed.
   5817   */
   5818 
   5819  /**
   5820   * Fired when a window is unloaded and a widget's instance is destroyed
   5821   * because of this. Only fired for API-based widgets.
   5822   *
   5823   * @callback CustomizableUIOnWidgetInstanceRemoved
   5824   * @param {string} aWidgetId
   5825   *   The ID of the widget that was just removed.
   5826   * @param {Document} aDocument
   5827   *   The Document that the widget belonged to that was just unloaded.
   5828   */
   5829 
   5830  /**
   5831   * Fired when entering customize mode in aWindow.
   5832   *
   5833   * @callback CustomizableUIOnCustomizeStart
   5834   * @param {DOMWindow} aWindow
   5835   *   The window in which customize mode was entered.
   5836   */
   5837 
   5838  /**
   5839   * Fired when exiting customize mode in aWindow.
   5840   *
   5841   * @callback CustomizableUIOnCustomizeEnd
   5842   * @param {DOMWindow} aWindow
   5843   *   The window in which customize mode was exited.
   5844   */
   5845 
   5846  /**
   5847   * Fired when a widget's DOM node is overflowing its toolbar and will be
   5848   * displayed in an overflow panel.
   5849   *
   5850   * @callback CustomizableUIOnWidgetOverflow
   5851   * @param {Element} aNode
   5852   *   The DOM node for the widget that overflowed.
   5853   * @param {Element} aContainer
   5854   *   The DOM container that the widget just overflowed out of.
   5855   */
   5856 
   5857  /**
   5858   * Fired when a widget that was overflowed out of its toolbar container
   5859   * "underflows" back.
   5860   *
   5861   * @callback CustomizableUIOnWidgetUnderflow
   5862   * @param {Element} aNode
   5863   *   The DOM node for the widget that had overflowed out.
   5864   * @param {Element} aContainer
   5865   *   The DOM container that the widget is underflowing back into.
   5866   */
   5867 
   5868  /**
   5869   * Fired when a window has been opened that is managed by CustomizableUI,
   5870   * once all of the prerequisite setup has been done.
   5871   *
   5872   * @callback CustomizableUIOnWindowOpened
   5873   * @param {DOMWindow} aWindow
   5874   *   The window that opened.
   5875   */
   5876 
   5877  /**
   5878   * Fired when a window that has been managed by CustomizableUI has been
   5879   * closed.
   5880   *
   5881   * @callback CustomizableUIOnWindowClosed
   5882   * @param {DOMWindow} aWindow
   5883   *   The window that closed.
   5884   */
   5885 
   5886  /**
   5887   * Fired after an area node is first built when it is registered. This is
   5888   * often when the window has opened, but in the case of add-ons, could fire
   5889   * when the node has just been registered with CustomizableUI after an add-on
   5890   * update or disable/enable sequence.
   5891   *
   5892   * @callback CustomizableUIOnAreaNodeRegistered
   5893   * @param {string} aArea
   5894   *   The ID for the area that was just registered.
   5895   * @param {Element} aContainer
   5896   *   The DOM node for the customizable area.
   5897   */
   5898 
   5899  /**
   5900   * Fired when an area node is explicitly unregistered by an API caller, or by
   5901   * a window closing. The aReason parameter indicates which of these is the
   5902   * case.
   5903   *
   5904   * @callback CustomizableUIOnAreaNodeUnregistered
   5905   * @param {string} aArea
   5906   *   The ID for the area that was just registered.
   5907   * @param {Element} aContainer
   5908   *   The DOM node for the customizable area.
   5909   * @param {string} aReason
   5910   *   One of either CustomizableUI.REASON_WINDOW_CLOSED or
   5911   *   CustomizableUI.REASON_AREA_UNREGISTERED.
   5912   */
   5913 
   5914  /**
   5915   * @typedef {object} CustomizableUIListener
   5916   * @property {CustomizableUIOnWidgetAddedCallback} [onWidgetAdded]
   5917   * @property {CustomizableUIOnWidgetMovedCallback} [onWidgetMoved]
   5918   * @property {CustomizableUIOnWidgetRemovedCallback} [onWidgetRemoved]
   5919   * @property {CustomizableUIOnWidgetBeforeDOMChange} [onWidgetBeforeDOMChange]
   5920   * @property {CustomizableUIOnWidgetAfterDOMChange} [onWidgetAfterDOMChange]
   5921   * @property {CustomizableUIOnWidgetReset} [onWidgetReset]
   5922   * @property {CustomizableUIOnWidgetUndoMove} [onWidgetUndoMove]
   5923   * @property {CustomizableUIOnWidgetCreated} [onWidgetCreated]
   5924   * @property {CustomizableUIOnAreaReset} [onAreaReset]
   5925   * @property {CustomizableUIOnWidgetAfterCreation} [onWidgetAfterCreation]
   5926   * @property {CustomizableUIOnWidgetDestroyed} [onWidgetDestroyed]
   5927   * @property {CustomizableUIOnWidgetInstanceRemoved} [onWidgetInstanceRemoved]
   5928   * @property {CustomizableUIOnWidgetDrag} [onWidgetDrag]
   5929   * @property {CustomizableUIOnCustomizeStart} [onCustomizeStart]
   5930   * @property {CustomizableUIOnCustomizeEnd} [onCustomizeEnd]
   5931   * @property {CustomizableUIOnWidgetOverflow} [onWidgetOverflow]
   5932   * @property {CustomizableUIOnWindowOpened} [onWindowOpened]
   5933   * @property {CustomizableUIOnWindowClosed} [onWindowClosed]
   5934   * @property {CustomizableUIOnAreaNodeRegistered} [onAreaNodeRegistered]
   5935   * @property {CustomizableUIOnAreaNodeUnregistered} [onAreaNodeUnregistered]
   5936   */
   5937 
   5938  /**
   5939   * Add a listener object that will get fired for various events regarding
   5940   * window, area, and window lifetimes / events, as well as customization
   5941   * events.
   5942   *
   5943   * @param {CustomizableUIListener} aListener
   5944   *   The listener object to add. Not all event handler methods need to be
   5945   *   defined. CustomizableUI will catch exceptions. Events are dispatched
   5946   *   synchronously on the UI thread, so if you can delay any/some of your
   5947   *   processing, that is advisable.
   5948   */
   5949  addListener(aListener) {
   5950    CustomizableUIInternal.addListener(aListener);
   5951  },
   5952 
   5953  /**
   5954   * Remove a listener that was previously added with addListener.
   5955   *
   5956   * @param {CustomizableUIListener} aListener
   5957   *   The listener object to remove.
   5958   */
   5959  removeListener(aListener) {
   5960    CustomizableUIInternal.removeListener(aListener);
   5961  },
   5962 
   5963  /**
   5964   * Register a customizable area with CustomizableUI.
   5965   *
   5966   * @param {string} aName
   5967   *   The name of the area to register. Can only contain alphanumeric
   5968   *   characters, dashes (-) and underscores (_).
   5969   * @param {object} aProperties
   5970   *   The properties of the area to register.
   5971   * @param {string} [aProperties.type=CustomizableUI.TYPE_TOOLBAR]
   5972   *   The type of area being registered. Either CustomizableUI.TYPE_TOOLBAR
   5973   *   (default) or CustomizableUI.TYPE_PANEL.
   5974   * @param {Element|undefined} [aProperties.anchor]
   5975   *   For a menu panel or overflowable toolbar area, the anchoring node for the
   5976   *   panel.
   5977   * @param {boolean} [aProperties.overflowable]
   5978   *   Set to true if your toolbar is overflowable. This requires an anchor, and
   5979   *   only has an effect for toolbars.
   5980   * @param {string[]} [aProperties.defaultPlacements]
   5981   *   An array of widget IDs making up the default contents of the area.
   5982   * @param {boolean|null} [aProperties.defaultCollapsed=true]
   5983   *   (INTERNAL ONLY) applies if the type is CustomizableUI.TYPE_TOOLBAR,
   5984   *   specifies if the toolbar is collapsed by default (defaults to true).
   5985   *   Specify `null` to ensure that reset/inDefaultArea don't care
   5986   *   about a toolbar's collapsed state
   5987   */
   5988  registerArea(aName, aProperties) {
   5989    CustomizableUIInternal.registerArea(aName, aProperties);
   5990  },
   5991  /**
   5992   * Register a concrete node for a registered area. This method needs to be called
   5993   * with any toolbar in the main browser window that has its "customizable" attribute
   5994   * set to true.
   5995   *
   5996   * Note that ideally, you should register your toolbar using registerArea
   5997   * before calling this. If you don't, the node will be saved for processing when
   5998   * you call registerArea. Note that CustomizableUI won't restore state in the area,
   5999   * allow the user to customize it in customize mode, or otherwise deal
   6000   * with it, until the area has been registered.
   6001   *
   6002   * @param {Element} aToolbar
   6003   *   The <xul:toolbar> node to register.
   6004   */
   6005  registerToolbarNode(aToolbar) {
   6006    CustomizableUIInternal.registerToolbarNode(aToolbar);
   6007  },
   6008  /**
   6009   * Register a panel node. A panel treated slightly differently from a toolbar in
   6010   * terms of what items can be moved into it. For example, a panel cannot have a
   6011   * spacer or a spring put into it.
   6012   *
   6013   * @param {Element} aNode
   6014   *   The panel contents DOM node being registered.
   6015   * @param {string} aArea
   6016   *   The name of the area for which to register this node.
   6017   */
   6018  registerPanelNode(aNode, aArea) {
   6019    CustomizableUIInternal.registerPanelNode(aNode, aArea);
   6020  },
   6021  /**
   6022   * Unregister a customizable area. The inverse of registerArea.
   6023   *
   6024   * Unregistering an area will remove all the (removable) widgets in the
   6025   * area, which will return to the panel, and destroy all other traces
   6026   * of the area within CustomizableUI. Note that this means the *contents*
   6027   * of the area's DOM nodes will be moved to the panel or removed, but
   6028   * the area's DOM nodes *themselves* will stay.
   6029   *
   6030   * Furthermore, by default the placements of the area will be kept in the
   6031   * saved state (!) and restored if you re-register the area at a later
   6032   * point. This is useful for e.g. add-ons that get disabled and then
   6033   * re-enabled (e.g. when they update).
   6034   *
   6035   * You can override this last behaviour (and destroy the placements
   6036   * information in the saved state) by passing true for aDestroyPlacements.
   6037   *
   6038   * @param {string} aName
   6039   *   The name of the area to unregister.
   6040   * @param {boolean} [aDestroyPlacements]
   6041   *   True if the placements information for the area should be destroyed
   6042   *   too. Defaults to not destroying the placements information.
   6043   */
   6044  unregisterArea(aName, aDestroyPlacements) {
   6045    CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
   6046  },
   6047  /**
   6048   * Add a widget to an area.
   6049   * If the area to which you try to add is not known to CustomizableUI,
   6050   * this will throw.
   6051   * If the area to which you try to add is the same as the area in which
   6052   * the widget is currently placed, this will do the same as
   6053   * moveWidgetWithinArea.
   6054   * If the widget cannot be removed from its original location, this will
   6055   * no-op.
   6056   *
   6057   * This will fire an onWidgetAdded notification,
   6058   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
   6059   * for each window CustomizableUI knows about.
   6060   *
   6061   * @param {string} aWidgetId
   6062   *   The ID of the widget to add to the area.
   6063   * @param {string} aArea
   6064   *   The name of the area to add the widget to.
   6065   * @param {number} [aPosition]
   6066   *   The position at which to add the widget. If you do not pass a position,
   6067   *   the widget will be added to the end of the area.
   6068   */
   6069  addWidgetToArea(aWidgetId, aArea, aPosition) {
   6070    CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
   6071  },
   6072  /**
   6073   * Remove a widget from its area. If the widget cannot be removed from its
   6074   * area, or is not in any area, this will no-op. Otherwise, this will fire an
   6075   * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
   6076   * onWidgetAfterDOMChange notification for each window CustomizableUI knows
   6077   * about.
   6078   *
   6079   * @param {string} aWidgetId
   6080   *   The ID of the widget to remove from its area.
   6081   */
   6082  removeWidgetFromArea(aWidgetId) {
   6083    CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
   6084  },
   6085  /**
   6086   * Move a widget within an area.
   6087   * If the widget is not in any area, this will no-op.
   6088   * If the widget is already at the indicated position, this will no-op.
   6089   *
   6090   * Otherwise, this will move the widget and fire an onWidgetMoved notification,
   6091   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
   6092   * each window CustomizableUI knows about.
   6093   *
   6094   * @param {string} aWidgetId
   6095   *   The ID of the widget to move.
   6096   * @param {number} aPosition
   6097   *   The position to move the widget to. Negative values or values greater
   6098   *   than the number of widgets will be interpreted to mean moving the widget
   6099   *   to respectively the first or last position.
   6100   */
   6101  moveWidgetWithinArea(aWidgetId, aPosition) {
   6102    CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
   6103  },
   6104  /**
   6105   * Ensure a XUL-based widget created in a window after areas were
   6106   * initialized moves to its correct position.
   6107   * Always prefer this over moving items in the DOM yourself.
   6108   *
   6109   * NB: why is this API per-window, you wonder? Because if you need this,
   6110   * presumably you yourself need to create the widget in all the windows
   6111   * and need to loop through them anyway.
   6112   *
   6113   * @param {string} aWidgetId
   6114   *   The ID of the widget that was just created.
   6115   * @param {DOMWindow} aWindow
   6116   *   The window in which you want to ensure it was added.
   6117   * @returns {boolean}
   6118   *   True if the widget was successfully placed in the window (or was already
   6119   *   placed in the window). False if something goes wrong with checking for
   6120   *   the presence of the widget in the window.
   6121   */
   6122  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
   6123    return CustomizableUIInternal.ensureWidgetPlacedInWindow(
   6124      aWidgetId,
   6125      aWindow
   6126    );
   6127  },
   6128  /**
   6129   * Start a batch update of items.
   6130   * During a batch update, the customization state is not saved to the user's
   6131   * preferences file, in order to reduce (possibly sync) IO.
   6132   * Calls to begin/endBatchUpdate may be nested.
   6133   *
   6134   * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
   6135   * for each call to beginBatchUpdate, even if there are exceptions in the
   6136   * code in the batch update. Otherwise, for the duration of the
   6137   * Firefox session, customization state is never saved. Typically, you
   6138   * would do this using a try...finally block.
   6139   */
   6140  beginBatchUpdate() {
   6141    CustomizableUIInternal.beginBatchUpdate();
   6142  },
   6143  /**
   6144   * End a batch update. See the documentation for beginBatchUpdate above.
   6145   *
   6146   * State is not saved if we believe it is identical to the last known
   6147   * saved state. State is only ever saved when all batch updates have
   6148   * finished (ie there has been 1 endBatchUpdate call for each
   6149   * beginBatchUpdate call). If any of the endBatchUpdate calls pass
   6150   * aForceDirty=true, we will flush to the prefs file.
   6151   *
   6152   * @param {boolean} [aForceDirty=false]
   6153   *   Force CustomizableUI to flush to the prefs file when all batch updates
   6154   *   have finished. Defaults to false.
   6155   */
   6156  endBatchUpdate(aForceDirty = false) {
   6157    CustomizableUIInternal.endBatchUpdate(aForceDirty);
   6158  },
   6159 
   6160  /**
   6161   * A function that will be invoked with the document in which to build a
   6162   * widget. Should return the DOM node that has been constructed.
   6163   *
   6164   * @callback CustomizableUICreateWidgetOnBuild
   6165   * @param {Document} aDoc
   6166   *   The document to create the widget in.
   6167   * @returns {Element}
   6168   *   The DOM node that was constructed for the widget.
   6169   */
   6170 
   6171  /**
   6172   * Invoked before the widget gets a DOM node constructed for it, passing the
   6173   * document in which that will happen. This is useful especially for 'view'
   6174   * type widgets that need to construct their views on the fly (e.g. from
   6175   * bootstrapped add-ons). If the function returns `false`, the widget will
   6176   * not be created.
   6177   *
   6178   * @callback CustomizableUICreateWidgetOnBeforeCreated
   6179   * @param {Document} aDoc
   6180   *   The document that the widget might be created in.
   6181   * @returns {boolean}
   6182   *   True if the widget should be created.
   6183   */
   6184 
   6185  /**
   6186   * A function that will be invoked whenever the widget has a DOM node
   6187   * constructed, passing the constructed node as an argument.
   6188   *
   6189   * @callback CustomizableUICreateWidgetOnCreated
   6190   * @param {Element} aNode
   6191   *   The DOM node that was constructed for the widget in a document.
   6192   */
   6193 
   6194  /**
   6195   * A function that will be invoked after the widget has a DOM node destroyed,
   6196   * passing the document from which it was removed. This is useful especially
   6197   * for 'view' type widgets that need to cleanup after views that were
   6198   * constructed on the fly.
   6199   *
   6200   * @callback CustomizableUICreateWidgetOnDestroyed
   6201   * @param {Document} aDoc
   6202   *   The document that the widget was destroyed in.
   6203   */
   6204 
   6205  /**
   6206   * A function that will be invoked when the user activates the button but
   6207   * before the command is evaluated. Useful if code needs to run to change the
   6208   * button's icon in preparation to the pending command action. Called for any
   6209   * type that supports the handler.  The command type, either "view" or
   6210   * "command", may be returned to force the action that will occur.  View will
   6211   * open the panel and command will result in calling onCommand.
   6212   *
   6213   * @callback CustomizableUICreateWidgetOnBeforeCommand
   6214   * @param {Event} aEvent
   6215   *   The command event that occurred on the button.
   6216   * @param {Element} aNode
   6217   *   The element upon which the command event occurred.
   6218   * @returns {string}
   6219   *   One of "action" or "view".
   6220   */
   6221 
   6222  /**
   6223   * Useful for custom, button and button-and-view widgets; a function that will
   6224   * be invoked when the user activates the button.
   6225   *
   6226   * @callback CustomizableUICreateWidgetOnCommand
   6227   * @param {Event} aEvent
   6228   *   The command event that was fired for the node.
   6229   */
   6230 
   6231  /**
   6232   * A function that will be invoked when the user clicks a widget node.
   6233   *
   6234   * @callback CustomizableUICreateWidgetOnClick
   6235   * @param {Event} aEvent
   6236   *   The click event that was fired for the widget node.
   6237   */
   6238 
   6239  /**
   6240   * A function that will be invoked when a user shows your view. If any event
   6241   * handler calls aEvt.preventDefault(), the view will not be shown.
   6242   *
   6243   * The event's `detail` property is an object with an `addBlocker` method.
   6244   * Handlers which need to perform asynchronous operations before the view is
   6245   * shown may pass this method a Promise, which will prevent the view from
   6246   * showing until it resolves. Additionally, if the promise resolves to the
   6247   * exact value `false`, the view will not be shown.
   6248   *
   6249   * @callback CustomizableUICreateWidgetOnViewShowing
   6250   * @param {Event} aEvent
   6251   *   The ViewShowing event. See PanelMultiView.sys.mjs.
   6252   */
   6253 
   6254  /**
   6255   * A function that will be invoked when a user hides your view.
   6256   *
   6257   * @callback CustomizableUICreateWidgetOnViewHiding
   6258   * @param {Event} aEvent
   6259   *   The ViewHiding event. See PanelMultiView.sys.mjs.
   6260   */
   6261 
   6262  /**
   6263   * @typedef {object} CustomizableUICreateWidgetProperties
   6264   * @property {string} id
   6265   *   The ID of the widget to be created.
   6266   * @property {string} [type="button"]
   6267   *   The type of widget to create. The valid types are:
   6268   *     'button' - for simple button widgets (the default)
   6269   *     'view'   - for buttons that open a panel or subview,
   6270   *                depending on where they are placed.
   6271   *     'button-and-view' - A combination of 'button' and 'view',
   6272   *                which looks different depending on whether it's
   6273   *                located in the toolbar or in the panel: When
   6274   *                located in the toolbar, the widget is shown as
   6275   *                a combined item of a button and a dropmarker
   6276   *                button. The button triggers the command and the
   6277   *                dropmarker button opens the view. When located
   6278   *                in the panel, shown as one item which opens the
   6279   *                view, and the button command cannot be
   6280   *                triggered separately.
   6281   *     'custom' - for fine-grained control over the creation
   6282   *                of the widget.
   6283   * @property {string} [viewId]
   6284   *   Only useful for views and button-and-view widgets (and required in those
   6285   *   cases). Should be set to the id of the <panelview> that should be shown
   6286   *   when clicking the widget.  If used with a custom widget, the widget must
   6287   *   also provide a toolbaritem where the first child is the view button.
   6288   * @property {CustomizableUICreateWidgetOnBuild} [onBuild]
   6289   *   Only useful for custom widgets (and required there).
   6290   * @property {CustomizableUICreateWidgetOnBeforeCreated} [onBeforeCreated]
   6291   *   Called for all button and non-custom widgets.
   6292   * @property {CustomizableUICreateWidgetOnCreated} [onCreated]
   6293   * @property {CustomizableUICreateWidgetOnDestroyed} [onDestroyed]
   6294   * @property {CustomizableUICreateWidgetOnBeforeCommand} [onBeforeCommand]
   6295   * @property {CustomizableUICreateWidgetOnCommand} [onCommand]
   6296   * @property {CustomizableUICreateWidgetOnClick} [onClick]
   6297   * @property {CustomizableUICreateWidgetOnViewShowing} [onViewShowing]
   6298   *   Only useful for view and button-and-view widgets.
   6299   * @property {CustomizableUICreateWidgetOnViewHiding} [onViewHiding]
   6300   *   Only useful for view and button-and-view widgets.
   6301   * @property {string} [l10nId]
   6302   *   A Fluent string identifier to use for localizing attributes on the
   6303   *   widget. If present, preferred over the label/tooltiptext parameters.
   6304   * @property {string} [tooltiptext]
   6305   *   **Deprecated** - use l10nId and Fluent instead. A string to use for the
   6306   *   tooltip of the widget.
   6307   * @property {string} [label]
   6308   *   **Deprecated** - use l10nId and Fluent instead. A string to use for the
   6309   *   label of the widget.
   6310   * @property {string} [localized]
   6311   *   **Deprecated** - use l10nId and Fluent instead. If true, or undefined,
   6312   *   attempt to retrieve the widget's string properties from the customizable
   6313   *   widgets string bundle.
   6314   * @property {boolean} [removable=true]
   6315   *   Whether the widget can be removed from a customizable area.
   6316   *   Note: if you specify false here, you must provide a defaultArea, too.
   6317   * @property {boolean} [overflows=true]
   6318   *   Whether widget can overflow when placed within an overflowable toolbar.
   6319   * @property {string} [defaultArea]
   6320   *   The default area to add the widget to. If not supplied, this widget will
   6321   *   be placed in the palette by default. A valid default area is required if
   6322   *   the widget is not removable.
   6323   * @property {string} [shortcutId]
   6324   *   The id of an element that has a shortcut for this widget. This is only
   6325   *   used to display the shortcut as part of the tooltip for builtin widgets
   6326   *   (which have strings inside customizableWidgets.properties). If you're in
   6327   *   an add-on, you should not set this property. If l10nId is provided, the
   6328   *   resulting shortcut is passed as the "$shortcut" variable to the Fluent
   6329   *   message.
   6330   * @property {boolean} [showInPrivateBrowsing=true]
   6331   *   True to show the widget in private browsing mode windows.
   6332   * @property {boolean} [hideInNonPrivateBrowsing=false]
   6333   *   True to hide the widget in non-private browsing mode windows.
   6334   * @property {boolean} [tabSpecific]
   6335   *   True to close any widget view panels if the selected tab changes.
   6336   * @property {boolean} [locationSpecific]
   6337   *   True to close any widget view panels if the location changes.
   6338   * @property {boolean} [webExtension]
   6339   *   True if this widget is being created on behalf of a WebExtension.
   6340   */
   6341 
   6342  /**
   6343   * Create a widget.
   6344   *
   6345   * To create a widget, you should pass an object with its desired
   6346   * properties.
   6347   *
   6348   * @param {CustomizableUICreateWidgetProperties} aProperties
   6349   *   The properties for the widget to be created.
   6350   * @returns {WidgetGroupWrapper|XULWidgetGroupWrapper}
   6351   */
   6352  createWidget(aProperties) {
   6353    return CustomizableUIInternal.wrapWidget(
   6354      CustomizableUIInternal.createWidget(aProperties)
   6355    );
   6356  },
   6357  /**
   6358   * Destroy a widget
   6359   *
   6360   * If the widget is part of the default placements in an area, this will
   6361   * remove it from there. It will also remove any DOM instances. However,
   6362   * it will keep the widget in the placements for whatever area it was
   6363   * in at the time. You can remove it from there yourself by calling
   6364   * CustomizableUI.removeWidgetFromArea(aWidgetId).
   6365   *
   6366   * @param {string} aWidgetId
   6367   *   The ID of the widget to destroy.
   6368   */
   6369  destroyWidget(aWidgetId) {
   6370    CustomizableUIInternal.destroyWidget(aWidgetId);
   6371  },
   6372  /**
   6373   * Get a wrapper object with information about the widget.
   6374   * The object provides the following properties
   6375   * (all read-only unless otherwise indicated):
   6376   *
   6377   * - id:            the widget's ID;
   6378   * - type:          the type of widget (button, view, custom). For
   6379   *                  XUL-provided widgets, this is always 'custom';
   6380   * - provider:      the provider type of the widget, id est one of
   6381   *                  PROVIDER_API or PROVIDER_XUL;
   6382   * - forWindow(w):  a method to obtain a single window wrapper for a widget,
   6383   *                  in the window w passed as the only argument;
   6384   * - instances:     an array of all instances (single window wrappers)
   6385   *                  of the widget. This array is NOT live;
   6386   * - areaType:      the type of the widget's current area
   6387   * - isGroup:       true; will be false for wrappers around single widget nodes;
   6388   * - source:        for API-provided widgets, whether they are built-in to
   6389   *                  Firefox or add-on-provided;
   6390   * - disabled:      for API-provided widgets, whether the widget is currently
   6391   *                  disabled. NB: this property is writable, and will toggle
   6392   *                  all the widgets' nodes' disabled states;
   6393   * - label:         for API-provied widgets, the label of the widget;
   6394   * - tooltiptext:   for API-provided widgets, the tooltip of the widget;
   6395   * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
   6396   *                          visible in private browsing;
   6397   * - hideInNonPrivateBrowsing: for API-provided widgets, whether the widget is
   6398   *                             hidden in non-private browsing;
   6399   *
   6400   * Single window wrappers obtained through forWindow(someWindow) or from the
   6401   * instances array have the following properties
   6402   * (all read-only unless otherwise indicated):
   6403   *
   6404   * - id:            the widget's ID;
   6405   * - type:          the type of widget (button, view, custom). For
   6406   *                  XUL-provided widgets, this is always 'custom';
   6407   * - provider:      the provider type of the widget, id est one of
   6408   *                  PROVIDER_API or PROVIDER_XUL;
   6409   * - node:          reference to the corresponding DOM node;
   6410   * - anchor:        the anchor on which to anchor panels opened from this
   6411   *                  node. This will point to the overflow chevron on
   6412   *                  overflowable toolbars if and only if your widget node
   6413   *                  is overflowed, to the anchor for the panel menu
   6414   *                  if your widget is inside the panel menu, and to the
   6415   *                  node itself in all other cases;
   6416   * - overflowed:    boolean indicating whether the node is currently in the
   6417   *                  overflow panel of the toolbar;
   6418   * - isGroup:       false; will be true for the group widget;
   6419   * - label:         for API-provided widgets, convenience getter for the
   6420   *                  label attribute of the DOM node;
   6421   * - tooltiptext:   for API-provided widgets, convenience getter for the
   6422   *                  tooltiptext attribute of the DOM node;
   6423   * - disabled:      for API-provided widgets, convenience getter *and setter*
   6424   *                  for the disabled state of this single widget. Note that
   6425   *                  you may prefer to use the group wrapper's getter/setter
   6426   *                  instead.
   6427   *
   6428   * @param {string} aWidgetId
   6429   *   The ID of the widget whose information you need.
   6430   * @returns {WidgetGroupWrapper|XULWidgetGroupWrapper|null}
   6431   *   A wrapper around the widget as described above, or null if the widget is
   6432   *   known not to exist (anymore). NB: A non-null return is no guarantee the
   6433   *   widget exists because we cannot know in advance if a XUL widget exists or
   6434   *   not.
   6435   */
   6436  getWidget(aWidgetId) {
   6437    return CustomizableUIInternal.wrapWidget(aWidgetId);
   6438  },
   6439  /**
   6440   * Get an array of widget wrappers (see getWidget) for all the widgets
   6441   * which are currently not in any area (so which are in the palette).
   6442   *
   6443   * @param {DOMElement} aWindowPalette
   6444   *   The palette element (and by extension, the window) in which
   6445   *   CustomizableUI should look. This matters because of course XUL-provided
   6446   *   widgets could be available in some windows but not others, and likewise
   6447   *   API-provided widgets might not exist in a private window (because of the
   6448   *   showInPrivateBrowsing property).
   6449   * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper>}
   6450   *   An array of widget wrappers (see getWidget)
   6451   */
   6452  getUnusedWidgets(aWindowPalette) {
   6453    return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
   6454      CustomizableUIInternal.wrapWidget,
   6455      CustomizableUIInternal
   6456    );
   6457  },
   6458  /**
   6459   * Get an array of all the widget IDs placed in an area.
   6460   * Modifying the array will not affect CustomizableUI.
   6461   *
   6462   * NB: will throw if called too early (before placements have been fetched)
   6463   *     or if the area is not currently known to CustomizableUI.
   6464   *
   6465   * @param {string} aArea
   6466   *   The name of the area whose placements you want to obtain.
   6467   * @returns {string[]}
   6468   *   An array containing the widget IDs that are in the area.
   6469   */
   6470  getWidgetIdsInArea(aArea) {
   6471    if (!gAreas.has(aArea)) {
   6472      throw new Error("Unknown customization area: " + aArea);
   6473    }
   6474    if (!gPlacements.has(aArea)) {
   6475      throw new Error(`Area ${aArea} not yet restored`);
   6476    }
   6477 
   6478    // We need to clone this, as we don't want to let consumers muck with placements
   6479    return [...gPlacements.get(aArea)];
   6480  },
   6481  /**
   6482   * Get an array of all the widget IDs in the default placements for an area.
   6483   * Modifying the array will not affect CustomizableUI.
   6484   *
   6485   * @param {string} aArea
   6486   *   The ID of the area whose default placements you want to obtain.
   6487   * @returns {string[]}
   6488   *   An array containing the widget IDs that are in the default placements for
   6489   *   that area.
   6490   */
   6491  getDefaultPlacementsForArea(aArea) {
   6492    return [...gAreas.get(aArea).get("defaultPlacements")];
   6493  },
   6494  /**
   6495   * Get an array of widget wrappers for all the widgets in an area. This is
   6496   * the same as calling getWidgetIdsInArea and .map() ing the result through
   6497   * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
   6498   * which don't have corresponding DOM nodes, there might be nulls in this array,
   6499   * or items for which wrapper.forWindow(win) will return null.
   6500   *
   6501   * @param {string} aArea
   6502   *   The ID of the area whose widgets you want to obtain.
   6503   * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper|null>}
   6504   *   An array of widget wrappers and/or null values for the widget IDs
   6505   *   placed in an area.
   6506   *
   6507   * NB: will throw if called too early (before placements have been fetched)
   6508   *     or if the area is not currently known to CustomizableUI.
   6509   */
   6510  getWidgetsInArea(aArea) {
   6511    return this.getWidgetIdsInArea(aArea).map(
   6512      CustomizableUIInternal.wrapWidget,
   6513      CustomizableUIInternal
   6514    );
   6515  },
   6516 
   6517  /**
   6518   * Ensure the customizable widget that matches up with this view node
   6519   * will get the right subview showing/shown/hiding/hidden events when
   6520   * they fire.
   6521   *
   6522   * @param {Element} aViewNode
   6523   *   The view node to add listeners to if they haven't been added already.
   6524   */
   6525  ensureSubviewListeners(aViewNode) {
   6526    return CustomizableUIInternal.ensureSubviewListeners(aViewNode);
   6527  },
   6528  /**
   6529   * Obtain an array of all the area IDs known to CustomizableUI.
   6530   * This array is created for you, so is modifiable without CustomizableUI
   6531   * being affected.
   6532   */
   6533  get areas() {
   6534    return [...gAreas.keys()];
   6535  },
   6536  /**
   6537   * Check what kind of area (toolbar or menu panel) an area is. This is
   6538   * useful if you have a widget that needs to behave differently depending
   6539   * on its location. Note that widget wrappers have a convenience getter
   6540   * property (areaType) for this purpose.
   6541   *
   6542   * @param {string} aArea
   6543   *   The ID of the area whose type you want to know
   6544   * @returns {string}
   6545   *   Returns CustomizableUI.TYPE_TOOLBAR or CustomizableUI.TYPE_PANEL
   6546   *   depending on the area, null if the area is unknown.
   6547   */
   6548  getAreaType(aArea) {
   6549    let area = gAreas.get(aArea);
   6550    return area ? area.get("type") : null;
   6551  },
   6552  /**
   6553   * Check if a toolbar is collapsed by default.
   6554   *
   6555   * @param {string} aArea
   6556   *   The ID of the area whose default-collapsed state you want to know.
   6557   * @returns {boolean}
   6558   *   Returns true if the toolbar area is collapsed by default, false if
   6559   *   not collapsed by default, and null if the area is unknown its collapsed
   6560   *   state cannot normally be controlled by the user.
   6561   */
   6562  isToolbarDefaultCollapsed(aArea) {
   6563    let area = gAreas.get(aArea);
   6564    return area ? area.get("defaultCollapsed") : null;
   6565  },
   6566  /**
   6567   * Obtain the DOM node that is the customize target for an area in a
   6568   * specific window.
   6569   *
   6570   * Areas can have a customization target that does not correspond to the
   6571   * node itself. In particular, toolbars that have a customizationtarget
   6572   * attribute set will have their customization target set to that node.
   6573   * This means widgets will end up in the customization target, not in the
   6574   * DOM node with the ID that corresponds to the area ID. This is useful
   6575   * because it lets you have fixed content in a toolbar (e.g. the panel
   6576   * menu item in the navbar) and have all the customizable widgets use
   6577   * the customization target.
   6578   *
   6579   * Using this API yourself is discouraged; you should generally not need
   6580   * to be asking for the DOM container node used for a particular area.
   6581   * In particular, if you're wanting to check it in relation to a widget's
   6582   * node, your DOM node might not be a direct child of the customize target
   6583   * in a window if, for instance, the window is in customization mode, or if
   6584   * this is an overflowable toolbar and the widget has been overflowed.
   6585   *
   6586   * @param {string} aArea
   6587   *   The ID of the area whose customize target you want to have
   6588   * @param {DOMWindow} aWindow
   6589   *   The window where you want to fetch the DOM node.
   6590   * @returns {Element}
   6591   *   The customize target DOM node for aArea in aWindow
   6592   */
   6593  getCustomizeTargetForArea(aArea, aWindow) {
   6594    return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
   6595  },
   6596  /**
   6597   * Reset the customization state back to its default.
   6598   *
   6599   * This is the nuclear option. You should never call this except if the user
   6600   * explicitly requests it. Firefox does this when the user clicks the
   6601   * "Restore Defaults" button in customize mode.
   6602   */
   6603  reset() {
   6604    CustomizableUIInternal.reset();
   6605  },
   6606 
   6607  /**
   6608   * Undo the previous reset, can only be called immediately after a reset.
   6609   *
   6610   * @returns {Promise<undefined>}
   6611   *   A promise that will be resolved when the operation is complete.
   6612   */
   6613  undoReset() {
   6614    CustomizableUIInternal.undoReset();
   6615  },
   6616 
   6617  /**
   6618   * Remove a custom toolbar added in a previous version of Firefox or using
   6619   * an add-on. NB: only works on the customizable toolbars generated by
   6620   * the toolbox itself. Intended for use from CustomizeMode, not by
   6621   * other consumers.
   6622   *
   6623   * @param {string} aToolbarId
   6624   *   The ID of the toolbar to remove.
   6625   */
   6626  removeExtraToolbar(aToolbarId) {
   6627    CustomizableUIInternal.removeExtraToolbar(aToolbarId);
   6628  },
   6629 
   6630  /**
   6631   * Can the last Restore Defaults operation be undone.
   6632   *
   6633   * @returns {boolean}
   6634   *   True if the last Restore Defaults operation can be undone.
   6635   */
   6636  get canUndoReset() {
   6637    return (
   6638      gUIStateBeforeReset.uiCustomizationState != null ||
   6639      gUIStateBeforeReset.drawInTitlebar != null ||
   6640      gUIStateBeforeReset.currentTheme != null ||
   6641      gUIStateBeforeReset.autoTouchMode != null ||
   6642      gUIStateBeforeReset.uiDensity != null ||
   6643      gUIStateBeforeReset.sidebarPositionStart != null
   6644    );
   6645  },
   6646 
   6647  /**
   6648   * @typedef {object} CustomizableUIPlacementInfo
   6649   * @param {string} area
   6650   *   The ID of the area where the widget is placed.
   6651   * @param {number} position
   6652   *   The 0-indexed position of the widget according to the placements area
   6653   *   of the area that it's in.
   6654   */
   6655 
   6656  /**
   6657   * Get the placement of a widget. This is by far the best way to obtain
   6658   * information about what the state of your widget is. The internals of
   6659   * this call are cheap (no DOM necessary) and you will know where the user
   6660   * has put your widget.
   6661   *
   6662   * @param {string} aWidgetId
   6663   *   The ID of the widget whose placement you want to know.
   6664   * @param {boolean} [aOnlyRegistered=true]
   6665   *   Set to false to return placements for widgets that aren't registered,
   6666   *   but still exist within the placements state, having been registered and
   6667   *   placed in the past.
   6668   * @param {boolean} [aDeadAreas=false]
   6669   *   Set to true to include placements within "dead" areas that are no longer
   6670   *   registered, but still exist in the placement state.
   6671   * @returns {CustomizableUIPlacementInfo|null}
   6672   *   Returns null if the widget is not placed anywhere (ie in the palette).
   6673   */
   6674  getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
   6675    return CustomizableUIInternal.getPlacementOfWidget(
   6676      aWidgetId,
   6677      aOnlyRegistered,
   6678      aDeadAreas
   6679    );
   6680  },
   6681  /**
   6682   * Check if a widget can be removed from the area it's in.
   6683   *
   6684   * Note that if you're wanting to move the widget somewhere, you should
   6685   * generally be checking canWidgetMoveToArea, because that will return
   6686   * true if the widget is already in the area where you want to move it (!).
   6687   *
   6688   * NB: oh, also, this method might lie if the widget in question is a
   6689   *     XUL-provided widget and there are no windows open, because it
   6690   *     can obviously not check anything in this case. It will return
   6691   *     true. You will be able to move the widget elsewhere. However,
   6692   *     once the user reopens a window, the widget will move back to its
   6693   *     'proper' area automagically.
   6694   *
   6695   * @param {string} aWidgetId
   6696   *   A widget ID or DOM node to check.
   6697   * @returns {boolean}
   6698   *   True if the widget can be removed from its area.
   6699   */
   6700  isWidgetRemovable(aWidgetId) {
   6701    return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
   6702  },
   6703  /**
   6704   * Check if a widget can be moved to a particular area. Like
   6705   * isWidgetRemovable but better, because it'll return true if the widget
   6706   * is already in the right area.
   6707   *
   6708   * @param {string} aWidgetId
   6709   *   The ID of the widget that you want to move somewhere.
   6710   * @param {string} aArea
   6711   *   The area ID you want to move the widget to. This can also be
   6712   *   CustomizableUI.AREA_NO_AREA to see if the widget can move to the
   6713   *   customization palette, whether it's removable or not.
   6714   * @returns {boolean}
   6715   *   True if this is possible. The same caveats as for isWidgetRemovable
   6716   *   apply, however, if no windows are open.
   6717   */
   6718  canWidgetMoveToArea(aWidgetId, aArea) {
   6719    return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
   6720  },
   6721  /**
   6722   * Whether we're in a default state. Note that non-removable non-default
   6723   * widgets and non-existing widgets are not taken into account in determining
   6724   * whether we're in the default state.
   6725   *
   6726   * NB: this is a property with a getter. The getter is NOT cheap, because
   6727   * it does smart things with non-removable non-default items, non-existent
   6728   * items, and so forth. Please don't call unless necessary.
   6729   */
   6730  get inDefaultState() {
   6731    return CustomizableUIInternal.inDefaultState;
   6732  },
   6733 
   6734  /**
   6735   * Set a toolbar's visibility state in all windows.
   6736   *
   6737   * @param {string} aToolbarId
   6738   *   The toolbar whose visibility should be adjusted.
   6739   * @param {boolean} aIsVisible
   6740   *   Whether the toolbar should be made visible.
   6741   */
   6742  setToolbarVisibility(aToolbarId, aIsVisible) {
   6743    CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
   6744  },
   6745 
   6746  /**
   6747   * Returns a Set with the IDs of any registered toolbar areas that are
   6748   * currently collapsed in a particular window. Menubars that are set to
   6749   * autohide and are in the temporary "open" state are still considered
   6750   * collapsed by default.
   6751   *
   6752   * @param {Window} window The browser window to check for collapsed toolbars.
   6753   * @returns {Set<string>}
   6754   */
   6755  getCollapsedToolbarIds(window) {
   6756    return CustomizableUIInternal.getCollapsedToolbarIds(window);
   6757  },
   6758 
   6759  /**
   6760   * Checks if a widget is likely visible in a given window.
   6761   *
   6762   * This method returns true when a widget is:
   6763   *  - Not pinned to the overflow menu
   6764   *  - Not in a collapsed toolbar (e.g. bookmarks toolbar, menu bar)
   6765   *  - Not in the customization palette
   6766   *
   6767   * Note: A widget that is moved into the overflow menu due to
   6768   *       the window being small might be considered visible by
   6769   *       this method, because a widget's placement does not
   6770   *       change when it overflows into the overflow menu.
   6771   *
   6772   * @param {string} aWidgetId the widget ID to check.
   6773   * @param {Window} window The browser window to check for widget visibility.
   6774   * @returns {boolean} whether the given widget is likely visible or not.
   6775   */
   6776  widgetIsLikelyVisible(aWidgetId, window) {
   6777    return CustomizableUIInternal.widgetIsLikelyVisible(aWidgetId, window);
   6778  },
   6779 
   6780  /**
   6781   * DEPRECATED! Use fluent instead.
   6782   *
   6783   * Get a localized property off a (widget?) object.
   6784   *
   6785   * NB: this is unlikely to be useful unless you're in Firefox code, because
   6786   *     this code uses the builtin widget stringbundle, and can't be told
   6787   *     to use add-on-provided strings. It's mainly here as convenience for
   6788   *     custom builtin widgets that build their own DOM but use the same
   6789   *     stringbundle as the other builtin widgets.
   6790   *
   6791   * @param {string|object} aWidget
   6792   *   The ID of a widget, or a widget object whose properties we should use to
   6793   *   fetch a localizable string.
   6794   * @param {string} aProp
   6795   *   The property on the object to use for the fetching from
   6796   *   customizableWidgets.properties.
   6797   * @param {string[]} [aFormatArgs]
   6798   *   Any extra arguments to use for a formatted string.
   6799   * @param {string} [aDef]
   6800   *   The default to return if we don't find the string in the stringbundle.
   6801   * @returns {string}
   6802   *   The localized string, or aDef if the string isn't in the bundle. If no
   6803   *   default is provided, if aProp exists on aWidget, we'll return that,
   6804   *   otherwise we'll return the empty string.
   6805   */
   6806  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
   6807    return CustomizableUIInternal.getLocalizedProperty(
   6808      aWidget,
   6809      aProp,
   6810      aFormatArgs,
   6811      aDef
   6812    );
   6813  },
   6814  /**
   6815   * Utility function to detect, find and set a keyboard shortcut for a menuitem
   6816   * or (toolbar)button.
   6817   *
   6818   * @param {Element} aShortcutNode
   6819   *   The XUL node where the shortcut will be derived from;
   6820   * @param {Element|null} aTargetNode
   6821   *   The XUL node on which the `shortcut` attribute will be set. If NULL, the
   6822   *   shortcut will be set on aShortcutNode.
   6823   */
   6824  addShortcut(aShortcutNode, aTargetNode) {
   6825    return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode);
   6826  },
   6827  /**
   6828   * Given a node, walk up to the first panel in its ancestor chain, and
   6829   * close it.
   6830   *
   6831   * @param {Element} aNode a node whose panel should be closed.
   6832   */
   6833  hidePanelForNode(aNode) {
   6834    CustomizableUIInternal.hidePanelForNode(aNode);
   6835  },
   6836  /**
   6837   * Check if a widget is a "special" widget: a spring, spacer or separator.
   6838   *
   6839   * @param {string} aWidgetId the widget ID to check.
   6840   * @returns {boolean} true if the widget is 'special', false otherwise.
   6841   */
   6842  isSpecialWidget(aWidgetId) {
   6843    return CustomizableUIInternal.isSpecialWidget(aWidgetId);
   6844  },
   6845  /**
   6846   * Check if a widget is provided by an extension. This effectively checks
   6847   * whether `webExtension: true` passed when the widget was being created.
   6848   *
   6849   * If the widget being referred to hasn't yet been created, or has been
   6850   * destroyed, we fallback to checking the ID for the "-browser-action"
   6851   * suffix.
   6852   *
   6853   * @param {string} aWidgetId the widget ID to check.
   6854   * @returns {boolean}
   6855   *   True if the widget was provided by an extension, false otherwise.
   6856   */
   6857  isWebExtensionWidget(aWidgetId) {
   6858    if (typeof aWidgetId !== "string") {
   6859      return false;
   6860    }
   6861    let widget = CustomizableUI.getWidget(aWidgetId);
   6862    return widget?.webExtension || aWidgetId.endsWith("-browser-action");
   6863  },
   6864  /**
   6865   * Add listeners to a panel that will close it. For use from the menu panel
   6866   * and overflowable toolbar implementations, unlikely to be useful for other
   6867   * consumers.
   6868   *
   6869   * @param {Element} aPanel
   6870   *   The panel to which listeners should be attached.
   6871   */
   6872  addPanelCloseListeners(aPanel) {
   6873    CustomizableUIInternal.addPanelCloseListeners(aPanel);
   6874  },
   6875  /**
   6876   * Remove close listeners that have been added to a panel with
   6877   * addPanelCloseListeners. For use from the menu panel and overflowable
   6878   * toolbar implementations, unlikely to be useful for consumers.
   6879   *
   6880   * @param {Element} aPanel
   6881   *   The panel from which listeners should be removed.
   6882   */
   6883  removePanelCloseListeners(aPanel) {
   6884    CustomizableUIInternal.removePanelCloseListeners(aPanel);
   6885  },
   6886  /**
   6887   * Notify listeners a widget is about to be dragged to an area. For use from
   6888   * Customize Mode only, do not use otherwise.
   6889   *
   6890   * @param {string} aWidgetId
   6891   *   The ID of the widget that is being dragged to an area.
   6892   * @param {string} aArea
   6893   *   The ID of the area to which the widget is being dragged.
   6894   */
   6895  onWidgetDrag(aWidgetId, aArea) {
   6896    CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
   6897  },
   6898  /**
   6899   * Notify listeners that a window is entering customize mode. For use from
   6900   * Customize Mode only, do not use otherwise.
   6901   *
   6902   * @param {DOMWindow} aWindow
   6903   *   The window entering customize mode.
   6904   */
   6905  notifyStartCustomizing(aWindow) {
   6906    CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
   6907  },
   6908  /**
   6909   * Notify listeners that a window is exiting customize mode. For use from
   6910   * Customize Mode only, do not use otherwise.
   6911   *
   6912   * @param {DOMWindow} aWindow
   6913   *   The window exiting customize mode.
   6914   */
   6915  notifyEndCustomizing(aWindow) {
   6916    CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
   6917  },
   6918 
   6919  /**
   6920   * Notify toolbox(es) of a particular event. If you don't pass aWindow,
   6921   * all toolboxes will be notified. For use from Customize Mode only,
   6922   * do not use otherwise.
   6923   *
   6924   * @param {string} aEvent
   6925   *   The name of the event to send.
   6926   * @param {object} [aDetails={}]
   6927   *   The details of the event.
   6928   * @param {DOMWindow|null} [aWindow=null]
   6929   *   The window in which to send the event.
   6930   */
   6931  dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {
   6932    CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
   6933  },
   6934 
   6935  /**
   6936   * Check whether an area is overflowable.
   6937   *
   6938   * @param {string} aAreaId
   6939   *   The ID of an area to check for overflowable-ness.
   6940   * @returns {boolean}
   6941   *   True if the area is overflowable, false otherwise.
   6942   */
   6943  isAreaOverflowable(aAreaId) {
   6944    let area = gAreas.get(aAreaId);
   6945    return area
   6946      ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
   6947      : false;
   6948  },
   6949  /**
   6950   * Obtain a string indicating the place of an element. This is intended
   6951   * for use from customize mode; You should generally use getPlacementOfWidget
   6952   * instead, which is cheaper because it does not use the DOM.
   6953   *
   6954   * @param {DOMElement} aElement
   6955   *   The DOM node whose place we need to check.
   6956   * @returns {string|undefined}
   6957   *   "toolbar" if the node is in a toolbar, "panel" if it is in the menu
   6958   *   panel, "palette" if it is in the (visible!) customization palette,
   6959   *   undefined otherwise.
   6960   */
   6961  getPlaceForItem(aElement) {
   6962    let place;
   6963    let node = aElement;
   6964    while (node && !place) {
   6965      if (node.localName == "toolbar") {
   6966        place = "toolbar";
   6967      } else if (node.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
   6968        place = "panel";
   6969      } else if (node.id == "customization-palette") {
   6970        place = "palette";
   6971      }
   6972 
   6973      node = node.parentNode;
   6974    }
   6975    return place;
   6976  },
   6977 
   6978  /**
   6979   * Check if a toolbar is builtin or not.
   6980   *
   6981   * @param {string} aToolbarId
   6982   *   The ID of the toolbar you want to check.
   6983   */
   6984  isBuiltinToolbar(aToolbarId) {
   6985    return CustomizableUIInternal.builtinToolbars.has(aToolbarId);
   6986  },
   6987 
   6988  /**
   6989   * Create an instance of a spring, spacer or separator.
   6990   *
   6991   * @param {string} aId
   6992   *   The type of special widget (spring, spacer or separator).
   6993   * @param {Document} aDocument
   6994   *   The document in which to create it.
   6995   * @returns {Element}
   6996   *   The created spring, spacer or separator node.
   6997   */
   6998  createSpecialWidget(aId, aDocument) {
   6999    return CustomizableUIInternal.createSpecialWidget(aId, aDocument);
   7000  },
   7001 
   7002  /**
   7003   * Fills a submenu with menu items.
   7004   *
   7005   * @param {Element[]} aMenuItems
   7006   *   The array of menu items to display.
   7007   * @param {Element} aSubview
   7008   *   The subview to fill with the menu items.
   7009   */
   7010  fillSubviewFromMenuItems(aMenuItems, aSubview) {
   7011    let attrs = [
   7012      "oncommand",
   7013      "onclick",
   7014      "label",
   7015      "key",
   7016      "disabled",
   7017      "command",
   7018      "observes",
   7019      "hidden",
   7020      "class",
   7021      "origin",
   7022      "image",
   7023      "checked",
   7024      "style",
   7025    ];
   7026 
   7027    // Use ownerGlobal.document to ensure we get the right doc even for
   7028    // elements in template tags.
   7029    let doc = aSubview.ownerGlobal.document;
   7030    let fragment = doc.createDocumentFragment();
   7031    for (let menuChild of aMenuItems) {
   7032      if (menuChild.hidden) {
   7033        continue;
   7034      }
   7035 
   7036      let subviewItem;
   7037      if (menuChild.localName == "menuseparator") {
   7038        // Don't insert duplicate or leading separators. This can happen if there are
   7039        // menus (which we don't copy) above the separator.
   7040        if (
   7041          !fragment.lastElementChild ||
   7042          fragment.lastElementChild.localName == "toolbarseparator"
   7043        ) {
   7044          continue;
   7045        }
   7046        subviewItem = doc.createXULElement("toolbarseparator");
   7047      } else if (menuChild.localName == "menuitem") {
   7048        subviewItem = doc.createXULElement("toolbarbutton");
   7049        CustomizableUI.addShortcut(menuChild, subviewItem);
   7050 
   7051        let item = menuChild;
   7052        if (!item.hasAttribute("onclick")) {
   7053          subviewItem.addEventListener("click", event => {
   7054            let newEvent = new doc.ownerGlobal.PointerEvent("click", event);
   7055 
   7056            // Telemetry should only pay attention to the original event.
   7057            lazy.BrowserUsageTelemetry.ignoreEvent(newEvent);
   7058            item.dispatchEvent(newEvent);
   7059          });
   7060        }
   7061 
   7062        if (!item.hasAttribute("oncommand")) {
   7063          subviewItem.addEventListener("command", event => {
   7064            let newEvent = doc.createEvent("XULCommandEvent");
   7065            newEvent.initCommandEvent(
   7066              event.type,
   7067              event.bubbles,
   7068              event.cancelable,
   7069              event.view,
   7070              event.detail,
   7071              event.ctrlKey,
   7072              event.altKey,
   7073              event.shiftKey,
   7074              event.metaKey,
   7075              0,
   7076              event.sourceEvent,
   7077              0
   7078            );
   7079 
   7080            // Telemetry should only pay attention to the original event.
   7081            lazy.BrowserUsageTelemetry.ignoreEvent(newEvent);
   7082            item.dispatchEvent(newEvent);
   7083          });
   7084        }
   7085      } else {
   7086        continue;
   7087      }
   7088      for (let attr of attrs) {
   7089        let attrVal = menuChild.getAttribute(attr);
   7090        if (attrVal) {
   7091          subviewItem.setAttribute(attr, attrVal);
   7092        }
   7093      }
   7094      // We do this after so the .subviewbutton class doesn't get overriden.
   7095      if (menuChild.localName == "menuitem") {
   7096        subviewItem.classList.add("subviewbutton");
   7097      }
   7098 
   7099      // We make it possible to supply an alternative Fluent key when cloning
   7100      // this menuitem into the AppMenu or panel contexts. This is because
   7101      // we often use Title Case in menuitems in native menus, but want to use
   7102      // Sentence case in the AppMenu / panels.
   7103      let l10nId = menuChild.getAttribute("appmenu-data-l10n-id");
   7104      if (l10nId) {
   7105        doc.l10n.setAttributes(subviewItem, l10nId);
   7106      }
   7107 
   7108      fragment.appendChild(subviewItem);
   7109    }
   7110    aSubview.appendChild(fragment);
   7111  },
   7112 
   7113  /**
   7114   * A helper function for clearing subviews.
   7115   *
   7116   * @param {Element} aSubview
   7117   *   The subview to clear.
   7118   */
   7119  clearSubview(aSubview) {
   7120    let parent = aSubview.parentNode;
   7121    // We'll take the container out of the document before cleaning it out
   7122    // to avoid reflowing each time we remove something.
   7123    parent.removeChild(aSubview);
   7124 
   7125    while (aSubview.firstChild) {
   7126      aSubview.firstChild.remove();
   7127    }
   7128 
   7129    parent.appendChild(aSubview);
   7130  },
   7131 
   7132  /**
   7133   * Called when DOMContentLoaded fires for a new browser window.
   7134   *
   7135   * @param {DOMWindow} aWindow
   7136   *   The DOM Window that has just opened.
   7137   */
   7138  handleNewBrowserWindow(aWindow) {
   7139    return CustomizableUIInternal.handleNewBrowserWindow(aWindow);
   7140  },
   7141 
   7142  /**
   7143   * Given a DOM node with the `customizable` attribute, will attempt to resolve
   7144   * it to the associated "customization target" for that DOM node via its
   7145   * `customizationtarget` attribute. If no such attribute exists, the DOM node
   7146   * itself is returned.
   7147   *
   7148   * If the DOM node is null, is not customizable, or cannot be resolved to
   7149   * a customization target, then null is returned.
   7150   *
   7151   * @param {Element|null} aElement
   7152   *   The DOM node to resolve to a customization target DOM node.
   7153   * @returns {Element|null}
   7154   *   The customization target DOM node, or null if one cannot be found.
   7155   */
   7156  getCustomizationTarget(aElement) {
   7157    return CustomizableUIInternal.getCustomizationTarget(aElement);
   7158  },
   7159 
   7160  /**
   7161   * This is a test-only method that allows tests to violate encapsulation and
   7162   * gain access to some state internal to this module. If not running in test
   7163   * automation, this will always return null.
   7164   *
   7165   * @param {string} aProp
   7166   *   The string representation of the internal property to retrieve. Only some
   7167   *   properties are supported - see the method code.
   7168   * @returns {any|null}
   7169   */
   7170  getTestOnlyInternalProp(aProp) {
   7171    if (!Cu.isInAutomation) {
   7172      return null;
   7173    }
   7174    switch (aProp) {
   7175      case "CustomizableUIInternal":
   7176        return CustomizableUIInternal;
   7177      case "gAreas":
   7178        return gAreas;
   7179      case "gFuturePlacements":
   7180        return gFuturePlacements;
   7181      case "gPalette":
   7182        return gPalette;
   7183      case "gPlacements":
   7184        return gPlacements;
   7185      case "gSavedState":
   7186        return gSavedState;
   7187      case "gSeenWidgets":
   7188        return gSeenWidgets;
   7189      case "kVersion":
   7190        return kVersion;
   7191    }
   7192    return null;
   7193  },
   7194 
   7195  /**
   7196   * This is a test-only method that allows tests to violate encapsulation and
   7197   * write to some state internal to this module. If not running in test
   7198   * automation, this will always just immediately return without making any
   7199   * changes.
   7200   *
   7201   * @param {string} aProp
   7202   *   The string representation of the internal property to change. Only some
   7203   *   properties are supported - see the method code.
   7204   * @param {any} aValue
   7205   *   The value to set the property to.
   7206   */
   7207  setTestOnlyInternalProp(aProp, aValue) {
   7208    if (!Cu.isInAutomation) {
   7209      return;
   7210    }
   7211    switch (aProp) {
   7212      case "gSavedState":
   7213        gSavedState = aValue;
   7214        break;
   7215      case "kVersion":
   7216        kVersion = aValue;
   7217        break;
   7218      case "gDirty":
   7219        gDirty = aValue;
   7220        break;
   7221    }
   7222  },
   7223 };
   7224 
   7225 Object.freeze(CustomizableUI);
   7226 Object.freeze(CustomizableUI.windows);
   7227 
   7228 /**
   7229 * All external consumers of widgets are really interacting with these wrappers
   7230 * which provide a common interface.
   7231 */
   7232 
   7233 /**
   7234 * WidgetGroupWrapper is the common interface for interacting with an entire
   7235 * widget group - AKA, all instances of a widget across a series of windows.
   7236 * This particular wrapper is only used for widgets created via the provider
   7237 * API.
   7238 */
   7239 function WidgetGroupWrapper(aWidget) {
   7240  this.isGroup = true;
   7241 
   7242  const kBareProps = [
   7243    "id",
   7244    "source",
   7245    "type",
   7246    "disabled",
   7247    "label",
   7248    "tooltiptext",
   7249    "showInPrivateBrowsing",
   7250    "hideInNonPrivateBrowsing",
   7251    "viewId",
   7252    "disallowSubView",
   7253    "webExtension",
   7254  ];
   7255  for (let prop of kBareProps) {
   7256    let propertyName = prop;
   7257    this.__defineGetter__(propertyName, () => aWidget[propertyName]);
   7258  }
   7259 
   7260  this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API);
   7261 
   7262  this.__defineSetter__("disabled", function (aValue) {
   7263    aValue = !!aValue;
   7264    aWidget.disabled = aValue;
   7265    for (let [, instance] of aWidget.instances) {
   7266      instance.disabled = aValue;
   7267    }
   7268  });
   7269 
   7270  this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
   7271    let wrapperMap;
   7272    if (!gSingleWrapperCache.has(aWindow)) {
   7273      wrapperMap = new Map();
   7274      gSingleWrapperCache.set(aWindow, wrapperMap);
   7275    } else {
   7276      wrapperMap = gSingleWrapperCache.get(aWindow);
   7277    }
   7278    if (wrapperMap.has(aWidget.id)) {
   7279      return wrapperMap.get(aWidget.id);
   7280    }
   7281 
   7282    let instance = aWidget.instances.get(aWindow.document);
   7283    if (!instance) {
   7284      instance = CustomizableUIInternal.buildWidgetNode(
   7285        aWindow.document,
   7286        aWidget
   7287      );
   7288    }
   7289 
   7290    let wrapper = new WidgetSingleWrapper(aWidget, instance);
   7291    wrapperMap.set(aWidget.id, wrapper);
   7292    return wrapper;
   7293  };
   7294 
   7295  this.__defineGetter__("instances", function () {
   7296    // Can't use gBuildWindows here because some areas load lazily:
   7297    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
   7298    if (!placement) {
   7299      return [];
   7300    }
   7301    let area = placement.area;
   7302    let buildAreas = gBuildAreas.get(area);
   7303    if (!buildAreas) {
   7304      return [];
   7305    }
   7306    return Array.from(buildAreas, node => this.forWindow(node.ownerGlobal));
   7307  });
   7308 
   7309  this.__defineGetter__("areaType", function () {
   7310    let areaProps = gAreas.get(aWidget.currentArea);
   7311    return areaProps && areaProps.get("type");
   7312  });
   7313 
   7314  Object.freeze(this);
   7315 }
   7316 
   7317 /**
   7318 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
   7319 * a particular window.
   7320 */
   7321 function WidgetSingleWrapper(aWidget, aNode) {
   7322  this.isGroup = false;
   7323 
   7324  this.node = aNode;
   7325  this.provider = CustomizableUI.PROVIDER_API;
   7326 
   7327  const kGlobalProps = ["id", "type"];
   7328  for (let prop of kGlobalProps) {
   7329    this[prop] = aWidget[prop];
   7330  }
   7331 
   7332  const kNodeProps = ["label", "tooltiptext"];
   7333  for (let prop of kNodeProps) {
   7334    let propertyName = prop;
   7335    // Look at the node for these, instead of the widget data, to ensure the
   7336    // wrapper always reflects this live instance.
   7337    this.__defineGetter__(propertyName, () => aNode.getAttribute(propertyName));
   7338  }
   7339 
   7340  this.__defineGetter__("disabled", () => aNode.disabled);
   7341  this.__defineSetter__("disabled", function (aValue) {
   7342    aNode.disabled = !!aValue;
   7343  });
   7344 
   7345  this.__defineGetter__("anchor", function () {
   7346    let anchorId;
   7347    // First check for an anchor for the area:
   7348    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
   7349    if (placement) {
   7350      anchorId = gAreas.get(placement.area).get("anchor");
   7351    }
   7352    if (!anchorId) {
   7353      anchorId = aNode.getAttribute("cui-anchorid");
   7354    }
   7355    if (!anchorId) {
   7356      anchorId = aNode.getAttribute("view-button-id");
   7357    }
   7358    if (anchorId) {
   7359      return aNode.ownerDocument.getElementById(anchorId);
   7360    }
   7361    if (aWidget.type == "button-and-view") {
   7362      return aNode.lastElementChild;
   7363    }
   7364    return aNode;
   7365  });
   7366 
   7367  this.__defineGetter__("overflowed", function () {
   7368    return aNode.getAttribute("overflowedItem") == "true";
   7369  });
   7370 
   7371  Object.freeze(this);
   7372 }
   7373 
   7374 /**
   7375 * XULWidgetGroupWrapper is the common interface for interacting with an entire
   7376 * widget group - AKA, all instances of a widget across a series of windows.
   7377 * This particular wrapper is only used for widgets created via the old-school
   7378 * XUL method (overlays, or programmatically injecting toolbaritems, or other
   7379 * such things).
   7380 */
   7381 // XXXunf Going to need to hook this up to some events to keep it all live.
   7382 function XULWidgetGroupWrapper(aWidgetId) {
   7383  this.isGroup = true;
   7384  this.id = aWidgetId;
   7385  this.type = "custom";
   7386  // XUL Widgets can never be provided by extensions.
   7387  this.webExtension = false;
   7388  this.provider = CustomizableUI.PROVIDER_XUL;
   7389 
   7390  this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
   7391    let wrapperMap;
   7392    if (!gSingleWrapperCache.has(aWindow)) {
   7393      wrapperMap = new Map();
   7394      gSingleWrapperCache.set(aWindow, wrapperMap);
   7395    } else {
   7396      wrapperMap = gSingleWrapperCache.get(aWindow);
   7397    }
   7398    if (wrapperMap.has(aWidgetId)) {
   7399      return wrapperMap.get(aWidgetId);
   7400    }
   7401 
   7402    let instance = aWindow.document.getElementById(aWidgetId);
   7403    if (!instance) {
   7404      // Toolbar palettes aren't part of the document, so elements in there
   7405      // won't be found via document.getElementById().
   7406      instance = aWindow.gNavToolbox.palette.getElementsByAttribute(
   7407        "id",
   7408        aWidgetId
   7409      )[0];
   7410    }
   7411 
   7412    let wrapper = new XULWidgetSingleWrapper(
   7413      aWidgetId,
   7414      instance,
   7415      aWindow.document
   7416    );
   7417    wrapperMap.set(aWidgetId, wrapper);
   7418    return wrapper;
   7419  };
   7420 
   7421  this.__defineGetter__("areaType", function () {
   7422    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
   7423    if (!placement) {
   7424      return null;
   7425    }
   7426 
   7427    let areaProps = gAreas.get(placement.area);
   7428    return areaProps && areaProps.get("type");
   7429  });
   7430 
   7431  this.__defineGetter__("instances", function () {
   7432    return Array.from(gBuildWindows, wins => this.forWindow(wins[0]));
   7433  });
   7434 
   7435  Object.freeze(this);
   7436 }
   7437 
   7438 /**
   7439 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL
   7440 * widget in a particular window.
   7441 */
   7442 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
   7443  this.isGroup = false;
   7444 
   7445  this.id = aWidgetId;
   7446  this.type = "custom";
   7447  this.provider = CustomizableUI.PROVIDER_XUL;
   7448 
   7449  let weakDoc = Cu.getWeakReference(aDocument);
   7450  // If we keep a strong ref, the weak ref will never die, so null it out:
   7451  aDocument = null;
   7452 
   7453  this.__defineGetter__("node", function () {
   7454    // If we've set this to null (further down), we're sure there's nothing to
   7455    // be gotten here, so bail out early:
   7456    if (!weakDoc) {
   7457      return null;
   7458    }
   7459    if (aNode) {
   7460      // Return the last known node if it's still in the DOM...
   7461      if (aNode.isConnected) {
   7462        return aNode;
   7463      }
   7464      // ... or the toolbox
   7465      let toolbox = aNode.ownerGlobal.gNavToolbox;
   7466      if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
   7467        return aNode;
   7468      }
   7469      // If it isn't, clear the cached value and fall through to the "slow" case:
   7470      aNode = null;
   7471    }
   7472 
   7473    let doc = weakDoc.get();
   7474    if (doc) {
   7475      // Store locally so we can cache the result:
   7476      aNode = CustomizableUIInternal.findXULWidgetInWindow(
   7477        aWidgetId,
   7478        doc.defaultView
   7479      );
   7480      return aNode;
   7481    }
   7482    // The weakref to the document is dead, we're done here forever more:
   7483    weakDoc = null;
   7484    return null;
   7485  });
   7486 
   7487  this.__defineGetter__("anchor", function () {
   7488    let anchorId;
   7489    // First check for an anchor for the area:
   7490    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
   7491    if (placement) {
   7492      anchorId = gAreas.get(placement.area).get("anchor");
   7493    }
   7494 
   7495    let node = this.node;
   7496    if (!anchorId && node) {
   7497      anchorId = node.getAttribute("cui-anchorid");
   7498    }
   7499 
   7500    return anchorId && node
   7501      ? node.ownerDocument.getElementById(anchorId)
   7502      : node;
   7503  });
   7504 
   7505  this.__defineGetter__("overflowed", function () {
   7506    let node = this.node;
   7507    if (!node) {
   7508      return false;
   7509    }
   7510    return node.getAttribute("overflowedItem") == "true";
   7511  });
   7512 
   7513  Object.freeze(this);
   7514 }
   7515 
   7516 /**
   7517 * OverflowableToolbar is a class that gives a <xul:toolbar> the ability to send
   7518 * toolbar items that are "overflowable" to lists in separate panels if and
   7519 * when the toolbar shrinks enough so that those items overflow out of bounds.
   7520 * Secondly, this class manages moving things out from those panels and back
   7521 * into the toolbar once it underflows and has the space to accommodate the
   7522 * items that had originally overflowed out.
   7523 *
   7524 * There are two panels that toolbar items can be overflowed to:
   7525 *
   7526 * 1. The default items overflow panel
   7527 *   This is where built-in default toolbar items will go to.
   7528 * 2. The Unified Extensions panel
   7529 *   This is where browser_action toolbar buttons created by extensions will
   7530 *   go to if the Unified Extensions UI is enabled - otherwise, those items will
   7531 *   go to the default items overflow panel.
   7532 *
   7533 * Finally, OverflowableToolbar manages the showing of the default items
   7534 * overflow panel when the associated anchor is clicked or dragged over. The
   7535 * Unified Extensions panel is managed separately by the extension code.
   7536 *
   7537 * In theory, we could have multiple overflowable toolbars, but in practice,
   7538 * only the nav-bar (CustomizableUI.AREA_NAVBAR) makes use of this class.
   7539 */
   7540 class OverflowableToolbar {
   7541  /**
   7542   * The OverflowableToolbar class is constructed during browser window
   7543   * creation, but to optimize for window painting, we defer most work until
   7544   * after the window has painted. This property is set to true once
   7545   * initialization has completed.
   7546   *
   7547   * @type {boolean}
   7548   */
   7549  #initialized = false;
   7550 
   7551  /**
   7552   * A reference to the <xul:toolbar> that is overflowable.
   7553   *
   7554   * @type {Element}
   7555   */
   7556  #toolbar = null;
   7557 
   7558  /**
   7559   * A reference to the part of the <xul:toolbar> that accepts CustomizableUI
   7560   * widgets.
   7561   *
   7562   * @type {Element}
   7563   */
   7564  #target = null;
   7565 
   7566  /**
   7567   * A mapping from the ID of a toolbar item that has overflowed to the width
   7568   * that the toolbar item occupied in the toolbar at the time of overflow. Any
   7569   * item that is currently overflowed will have an entry in this map.
   7570   *
   7571   * @type {Map<string, number>}
   7572   */
   7573  #overflowedInfo = new Map();
   7574 
   7575  /**
   7576   * The set of overflowed DOM nodes that were hidden at the time of overflowing.
   7577   */
   7578  #hiddenOverflowedNodes = new WeakSet();
   7579 
   7580  /**
   7581   * True if the overflowable toolbar is actively handling overflows and
   7582   * underflows. This value is set internally by the private #enable() and
   7583   * #disable() methods.
   7584   *
   7585   * @type {boolean}
   7586   */
   7587  #enabled = true;
   7588 
   7589  /**
   7590   * A reference to the element that overflowed toolbar items will be
   7591   * appended to as children upon overflow.
   7592   *
   7593   * @type {Element}
   7594   */
   7595  #defaultList = null;
   7596 
   7597  /**
   7598   * A reference to the button that opens the overflow panel. This is also
   7599   * the element that the panel will anchor to.
   7600   *
   7601   * @type {Element}
   7602   */
   7603  #defaultListButton = null;
   7604 
   7605  /**
   7606   * A reference to the <xul:panel> overflow panel that contains the #defaultList
   7607   * element.
   7608   *
   7609   * @type {Element}
   7610   */
   7611  #defaultListPanel = null;
   7612 
   7613  /**
   7614   * A reference to the the element that overflowed extension browser action
   7615   * toolbar items will be appended to as children upon overflow if the
   7616   * Unified Extension UI is enabled. This is created lazily and might be null,
   7617   * so you should use the #webExtList memoizing getter instead to get this.
   7618   *
   7619   * @type {Element|null}
   7620   */
   7621  #webExtListRef = null;
   7622 
   7623  /**
   7624   * An empty object that is created in #checkOverflow to identify individual
   7625   * calls to #checkOverflow and avoid re-entrancy (since #checkOverflow is
   7626   * asynchronous, and in theory, could be called multiple times before any of
   7627   * those times have a chance to fully exit).
   7628   *
   7629   * @type {object}
   7630   */
   7631  #checkOverflowHandle = null;
   7632 
   7633  /**
   7634   * A timeout ID returned by setTimeout that identifies a timeout function that
   7635   * runs to hide the #defaultListPanel if the user happened to open the panel by dragging
   7636   * over the #defaultListButton and then didn't hover any part of the #defaultListPanel.
   7637   *
   7638   * @type {number}
   7639   */
   7640  #hideTimeoutId = null;
   7641 
   7642  /**
   7643   * Public methods start here.
   7644   */
   7645 
   7646  /**
   7647   * OverflowableToolbar constructor. This is run very early on in the lifecycle
   7648   * of a browser window, so it tries to defer most work to the init() method
   7649   * instead after first paint.
   7650   *
   7651   * Upon construction, a "overflowable" attribute will be set on the
   7652   * toolbar, set to the value of "true".
   7653   *
   7654   * Part of the API for OverflowableToolbar is declarative, in that it expects
   7655   * certain attributes to be set on the <xul:toolbar> that is overflowable.
   7656   * Those attributes are:
   7657   *
   7658   * default-overflowbutton:
   7659   *   The ID of the button that is used to open and anchor the overflow panel.
   7660   * default-overflowtarget:
   7661   *   The ID of the element that overflowed items will be appended to as
   7662   *   children. Note that the overflowed toolbar items are moved into and out
   7663   *   of this overflow target, so it is definitely advisable to let
   7664   *   OverflowableToolbar own managing the children of default-overflowtarget,
   7665   *   and to not modify it outside of this class.
   7666   * default-overflowpanel:
   7667   *   The ID of the <xul:panel> that contains the default-overflowtarget.
   7668   * addon-webext-overflowbutton:
   7669   *   The ID of the button that is used to open and anchor the Unified
   7670   *   Extensions panel.
   7671   * addon-webext-overflowtarget:
   7672   *   The ID of the element that overflowed extension toolbar buttons will
   7673   *   be appended to as children if the Unified Extensions UI is enabled.
   7674   *   Note that the overflowed toolbar items are moved into and out of this
   7675   *   overflow target, so it is definitely advisable to let OverflowableToolbar
   7676   *   own managing the children of addon-webext-overflowtarget, and to not
   7677   *   modify it outside of this class.
   7678   *
   7679   * @param {Element} aToolbarNode The <xul:toolbar> that will be overflowable.
   7680   * @throws {Error} Throws if the customization target of the toolbar somehow
   7681   *   isn't a direct descendent of the toolbar.
   7682   */
   7683  constructor(aToolbarNode) {
   7684    this.#toolbar = aToolbarNode;
   7685    this.#target = CustomizableUI.getCustomizationTarget(this.#toolbar);
   7686    if (this.#target.parentNode != this.#toolbar) {
   7687      throw new Error(
   7688        "Customization target must be a direct child of an overflowable toolbar."
   7689      );
   7690    }
   7691 
   7692    this.#toolbar.setAttribute("overflowable", "true");
   7693    let doc = this.#toolbar.ownerDocument;
   7694    this.#defaultList = doc.getElementById(
   7695      this.#toolbar.getAttribute("default-overflowtarget")
   7696    );
   7697    this.#defaultList._customizationTarget = this.#defaultList;
   7698 
   7699    let window = this.#toolbar.ownerGlobal;
   7700 
   7701    if (window.gBrowserInit.delayedStartupFinished) {
   7702      this.init();
   7703    } else {
   7704      Services.obs.addObserver(this, "browser-delayed-startup-finished");
   7705    }
   7706  }
   7707 
   7708  /**
   7709   * Does final initialization of the OverflowableToolbar after the window has
   7710   * first painted. This will also kick off the first check to see if overflow
   7711   * has already occurred at the time of initialization.
   7712   */
   7713  init() {
   7714    let doc = this.#toolbar.ownerDocument;
   7715    let window = doc.defaultView;
   7716    window.addEventListener("resize", this);
   7717    window.gNavToolbox.addEventListener("customizationstarting", this);
   7718    window.gNavToolbox.addEventListener("aftercustomization", this);
   7719 
   7720    let defaultListButton = this.#toolbar.getAttribute(
   7721      "default-overflowbutton"
   7722    );
   7723    this.#defaultListButton = doc.getElementById(defaultListButton);
   7724    this.#defaultListButton.addEventListener("mousedown", this);
   7725    this.#defaultListButton.addEventListener("keypress", this);
   7726    this.#defaultListButton.addEventListener("dragover", this);
   7727    this.#defaultListButton.addEventListener("dragend", this);
   7728 
   7729    let panelId = this.#toolbar.getAttribute("default-overflowpanel");
   7730    this.#defaultListPanel = doc.getElementById(panelId);
   7731    this.#defaultListPanel.addEventListener("popuphiding", this);
   7732    CustomizableUIInternal.addPanelCloseListeners(this.#defaultListPanel);
   7733 
   7734    CustomizableUI.addListener(this);
   7735 
   7736    this.#checkOverflow();
   7737 
   7738    this.#initialized = true;
   7739  }
   7740 
   7741  /**
   7742   * Almost the exact reverse of init(). This is called when the browser window
   7743   * is unloading.
   7744   */
   7745  uninit() {
   7746    this.#toolbar.removeAttribute("overflowable");
   7747 
   7748    if (!this.#initialized) {
   7749      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
   7750      Services.prefs.removeObserver(kPrefSidebarVerticalTabsEnabled, this);
   7751      Services.prefs.removeObserver(kPrefSidebarRevampEnabled, this);
   7752      return;
   7753    }
   7754 
   7755    this.#disable();
   7756 
   7757    let window = this.#toolbar.ownerGlobal;
   7758    window.removeEventListener("resize", this);
   7759    window.gNavToolbox.removeEventListener("customizationstarting", this);
   7760    window.gNavToolbox.removeEventListener("aftercustomization", this);
   7761    this.#defaultListButton.removeEventListener("mousedown", this);
   7762    this.#defaultListButton.removeEventListener("keypress", this);
   7763    this.#defaultListButton.removeEventListener("dragover", this);
   7764    this.#defaultListButton.removeEventListener("dragend", this);
   7765    this.#defaultListPanel.removeEventListener("popuphiding", this);
   7766 
   7767    CustomizableUI.removeListener(this);
   7768    CustomizableUIInternal.removePanelCloseListeners(this.#defaultListPanel);
   7769  }
   7770 
   7771  /**
   7772   * Opens the overflow #defaultListPanel if it's not already open. If the panel is in
   7773   * the midst of hiding when this is called, the panel will be re-opened.
   7774   *
   7775   * @returns {Promise<undefined>}
   7776   *  Resolves once the panel is open.
   7777   */
   7778  show(aEvent) {
   7779    if (this.#defaultListPanel.state == "open") {
   7780      return Promise.resolve();
   7781    }
   7782    return new Promise(resolve => {
   7783      let doc = this.#defaultListPanel.ownerDocument;
   7784      this.#defaultListPanel.hidden = false;
   7785      let multiview = this.#defaultListPanel.querySelector("panelmultiview");
   7786      let mainViewId = multiview.getAttribute("mainViewId");
   7787      let mainView = doc.getElementById(mainViewId);
   7788      let contextMenu = doc.getElementById(mainView.getAttribute("context"));
   7789      contextMenu.addEventListener("command", this, {
   7790        capture: true,
   7791        mozSystemGroup: true,
   7792      });
   7793      let anchor = this.#defaultListButton.icon;
   7794 
   7795      let popupshown = false;
   7796      this.#defaultListPanel.addEventListener(
   7797        "popupshown",
   7798        () => {
   7799          popupshown = true;
   7800          this.#defaultListPanel.addEventListener("dragover", this);
   7801          this.#defaultListPanel.addEventListener("dragend", this);
   7802          // Wait until the next tick to resolve so all popupshown
   7803          // handlers have a chance to run before our promise resolution
   7804          // handlers do.
   7805          Services.tm.dispatchToMainThread(resolve);
   7806        },
   7807        { once: true }
   7808      );
   7809 
   7810      let openPanel = () => {
   7811        // Ensure we update the gEditUIVisible flag when opening the popup, in
   7812        // case the edit controls are in it.
   7813        this.#defaultListPanel.addEventListener(
   7814          "popupshowing",
   7815          () => {
   7816            doc.defaultView.updateEditUIVisibility();
   7817          },
   7818          { once: true }
   7819        );
   7820 
   7821        this.#defaultListPanel.addEventListener(
   7822          "popuphidden",
   7823          () => {
   7824            if (!popupshown) {
   7825              // The panel was hidden again before it was shown. This can break
   7826              // consumers waiting for the panel to show. So we try again.
   7827              openPanel();
   7828            }
   7829          },
   7830          { once: true }
   7831        );
   7832 
   7833        lazy.PanelMultiView.openPopup(
   7834          this.#defaultListPanel,
   7835          anchor || this.#defaultListButton,
   7836          {
   7837            triggerEvent: aEvent,
   7838          }
   7839        );
   7840        this.#defaultListButton.open = true;
   7841      };
   7842 
   7843      openPanel();
   7844    });
   7845  }
   7846 
   7847  /**
   7848   * Exposes whether #checkOverflow is currently running.
   7849   *
   7850   * @returns {boolean} True if #checkOverflow is currently running.
   7851   */
   7852  isHandlingOverflow() {
   7853    return !!this.#checkOverflowHandle;
   7854  }
   7855 
   7856  /**
   7857   * Finds the most appropriate place to insert toolbar item aNode if we've been
   7858   * asked to put it into the overflowable toolbar without being told exactly
   7859   * where.
   7860   *
   7861   * @param {Element} aNode The toolbar item being inserted.
   7862   * @returns {Array} [parent, nextNode]
   7863   *   parent: {Element} The parent element that should contain aNode.
   7864   *   nextNode: {Element|null} The node that should follow aNode after
   7865   *     insertion, if any. If this is null, aNode should be placed at the end
   7866   *     of parent.
   7867   */
   7868  findOverflowedInsertionPoints(aNode) {
   7869    let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
   7870    let areaId = this.#toolbar.id;
   7871    let placements = gPlacements.get(areaId);
   7872    let nodeIndex = placements.indexOf(aNode.id);
   7873    let nodeBeforeNewNodeIsOverflown = false;
   7874 
   7875    let loopIndex = -1;
   7876    // Loop through placements to find where to insert this item.
   7877    // As soon as we find an overflown widget, we will only
   7878    // insert in the overflow panel (this is why we check placements
   7879    // before the desired location for the new node). Once we pass
   7880    // the desired location of the widget, we look for placement ids
   7881    // that actually have DOM equivalents to insert before. If all
   7882    // else fails, we insert at the end of either the overflow list
   7883    // or the toolbar target.
   7884    while (++loopIndex < placements.length) {
   7885      let nextNodeId = placements[loopIndex];
   7886      if (loopIndex > nodeIndex) {
   7887        // Note that if aNode is in a template, its `ownerDocument` is *not*
   7888        // going to be the browser.xhtml document, so we cannot rely on it.
   7889        let nextNode = this.#toolbar.ownerDocument.getElementById(nextNodeId);
   7890        // If the node we're inserting can overflow, and the next node
   7891        // in the toolbar is overflown, we should insert this node
   7892        // in the overflow panel before it.
   7893        if (
   7894          newNodeCanOverflow &&
   7895          this.#overflowedInfo.has(nextNodeId) &&
   7896          nextNode &&
   7897          nextNode.parentNode == this.#defaultList
   7898        ) {
   7899          return [this.#defaultList, nextNode];
   7900        }
   7901        // Otherwise (if either we can't overflow, or the previous node
   7902        // wasn't overflown), and the next node is in the toolbar itself,
   7903        // insert the node in the toolbar.
   7904        if (
   7905          (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) &&
   7906          nextNode &&
   7907          (nextNode.parentNode == this.#target ||
   7908            // Also check if the next node is in a customization wrapper
   7909            // (toolbarpaletteitem). We don't need to do this for the
   7910            // overflow case because overflow is disabled in customize mode.
   7911            (nextNode.parentNode.localName == "toolbarpaletteitem" &&
   7912              nextNode.parentNode.parentNode == this.#target))
   7913        ) {
   7914          return [this.#target, nextNode];
   7915        }
   7916      } else if (
   7917        loopIndex < nodeIndex &&
   7918        this.#overflowedInfo.has(nextNodeId)
   7919      ) {
   7920        nodeBeforeNewNodeIsOverflown = true;
   7921      }
   7922    }
   7923 
   7924    let overflowList = CustomizableUI.isWebExtensionWidget(aNode.id)
   7925      ? this.#webExtList
   7926      : this.#defaultList;
   7927 
   7928    let containerForAppending =
   7929      this.#overflowedInfo.size && newNodeCanOverflow
   7930        ? overflowList
   7931        : this.#target;
   7932    return [containerForAppending, null];
   7933  }
   7934 
   7935  /**
   7936   * Allows callers to query for the current parent of a toolbar item that may
   7937   * or may not be overflowed. That parent will either be #defaultList,
   7938   * #webExtList (if it's an extension button) or #target.
   7939   *
   7940   * Note: It is assumed that the caller has verified that aNode is placed
   7941   * within the toolbar customizable area according to CustomizableUI.
   7942   *
   7943   * @param {Element} aNode the node that can be overflowed by this
   7944   *   OverflowableToolbar.
   7945   * @returns {Element} The current containing node for aNode.
   7946   */
   7947  getContainerFor(aNode) {
   7948    if (aNode.getAttribute("overflowedItem") == "true") {
   7949      return CustomizableUI.isWebExtensionWidget(aNode.id)
   7950        ? this.#webExtList
   7951        : this.#defaultList;
   7952    }
   7953    return this.#target;
   7954  }
   7955 
   7956  /**
   7957   * Private methods start here.
   7958   */
   7959 
   7960  /**
   7961   * Handle overflow in the toolbar by moving items to the overflow menu.
   7962   */
   7963  async #onOverflow() {
   7964    if (!this.#enabled) {
   7965      return;
   7966    }
   7967 
   7968    let win = this.#target.ownerGlobal;
   7969    let checkOverflowHandle = this.#checkOverflowHandle;
   7970    let webExtButtonID = this.#toolbar.getAttribute(
   7971      "addon-webext-overflowbutton"
   7972    );
   7973 
   7974    let { isOverflowing, targetContentWidth } = await this.#getOverflowInfo();
   7975 
   7976    // Stop if the window has closed or if we re-enter while waiting for
   7977    // layout.
   7978    if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) {
   7979      lazy.log.debug("Window closed or another overflow handler started.");
   7980      return;
   7981    }
   7982 
   7983    let webExtList = this.#webExtList;
   7984 
   7985    let child = this.#target.lastElementChild;
   7986    while (child && isOverflowing) {
   7987      let prevChild = child.previousElementSibling;
   7988 
   7989      if (child.getAttribute("overflows") != "false") {
   7990        this.#overflowedInfo.set(child.id, targetContentWidth);
   7991        let { width: childWidth } =
   7992          win.windowUtils.getBoundsWithoutFlushing(child);
   7993        if (!childWidth) {
   7994          this.#hiddenOverflowedNodes.add(child);
   7995        }
   7996 
   7997        child.setAttribute("overflowedItem", true);
   7998        CustomizableUIInternal.ensureButtonContextMenu(
   7999          child,
   8000          this.#toolbar,
   8001          true
   8002        );
   8003        CustomizableUIInternal.notifyListeners(
   8004          "onWidgetOverflow",
   8005          child,
   8006          this.#target
   8007        );
   8008 
   8009        if (webExtList && CustomizableUI.isWebExtensionWidget(child.id)) {
   8010          child.setAttribute("cui-anchorid", webExtButtonID);
   8011          webExtList.insertBefore(child, webExtList.firstElementChild);
   8012        } else {
   8013          child.setAttribute("cui-anchorid", this.#defaultListButton.id);
   8014          this.#defaultList.insertBefore(
   8015            child,
   8016            this.#defaultList.firstElementChild
   8017          );
   8018          if (!CustomizableUI.isSpecialWidget(child.id) && childWidth) {
   8019            this.#toolbar.setAttribute("overflowing", "true");
   8020          }
   8021        }
   8022      }
   8023      child = prevChild;
   8024      ({ isOverflowing, targetContentWidth } = await this.#getOverflowInfo());
   8025      // Stop if the window has closed or if we re-enter while waiting for
   8026      // layout.
   8027      if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) {
   8028        lazy.log.debug("Window closed or another overflow handler started.");
   8029        return;
   8030      }
   8031    }
   8032 
   8033    win.UpdateUrlbarSearchSplitterState();
   8034  }
   8035 
   8036  /**
   8037   * @typedef {object} CustomizableUIOverflowInfo
   8038   * @property {boolean} isOverflowing
   8039   *   True if at least one toolbar item has overflowed into an overflow panel.
   8040   * @property {number} targetContentWidth
   8041   *   The total width of the items within the customization target area of the
   8042   *   overflowable toolbar toolbar.
   8043   * @property {number} totalAvailWidth
   8044   *   The maximum width items in the overflowable toolbar may occupy before
   8045   *   causing an overflow.
   8046   */
   8047 
   8048  /**
   8049   * Returns a Promise that resolves to a an object that describes the state
   8050   * that this OverflowableToolbar is currently in.
   8051   *
   8052   * @returns {Promise<CustomizableUIOverflowInfo>}
   8053   */
   8054  async #getOverflowInfo() {
   8055    function getInlineSize(aElement) {
   8056      return aElement.getBoundingClientRect().width;
   8057    }
   8058 
   8059    function sumChildrenInlineSize(aParent, aExceptChild = null) {
   8060      let sum = 0;
   8061      for (let child of aParent.children) {
   8062        let style = win.getComputedStyle(child);
   8063        if (
   8064          style.display == "none" ||
   8065          win.XULPopupElement.isInstance(child) ||
   8066          (style.position != "static" && style.position != "relative")
   8067        ) {
   8068          continue;
   8069        }
   8070        sum += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
   8071        if (child != aExceptChild) {
   8072          sum += getInlineSize(child);
   8073        }
   8074      }
   8075      return sum;
   8076    }
   8077 
   8078    let win = this.#target.ownerGlobal;
   8079    let totalAvailWidth;
   8080    let targetWidth;
   8081    let targetChildrenWidth;
   8082 
   8083    await win.promiseDocumentFlushed(() => {
   8084      let style = win.getComputedStyle(this.#toolbar);
   8085      let toolbarChildrenWidth = sumChildrenInlineSize(
   8086        this.#toolbar,
   8087        this.#target
   8088      );
   8089      totalAvailWidth =
   8090        getInlineSize(this.#toolbar) -
   8091        parseFloat(style.paddingLeft) -
   8092        parseFloat(style.paddingRight) -
   8093        toolbarChildrenWidth;
   8094      targetWidth = getInlineSize(this.#target);
   8095      targetChildrenWidth =
   8096        this.#target == this.#toolbar
   8097          ? toolbarChildrenWidth
   8098          : sumChildrenInlineSize(this.#target);
   8099    });
   8100 
   8101    lazy.log.debug(
   8102      `Getting overflow info: target width: ${targetWidth} (${targetChildrenWidth}); avail: ${totalAvailWidth}`
   8103    );
   8104 
   8105    // If the target has min-width: 0, their children might actually overflow
   8106    // it, so check for both cases explicitly.
   8107    // We don't care about <1px differences, so ceil the avail width and floor
   8108    // the content width to deal with it.
   8109    let targetContentWidth = Math.floor(
   8110      Math.max(targetWidth, targetChildrenWidth)
   8111    );
   8112    totalAvailWidth = Math.ceil(totalAvailWidth);
   8113    let isOverflowing = targetContentWidth > totalAvailWidth;
   8114    return { isOverflowing, targetContentWidth, totalAvailWidth };
   8115  }
   8116 
   8117  /**
   8118   * Tries to move toolbar items back to the toolbar from the overflow panel.
   8119   *
   8120   * @param {boolean} shouldMoveAllItems
   8121   *        Whether we should move everything (e.g. because we're being
   8122   *        disabled)
   8123   * @param {number} [totalAvailWidth=undefined]
   8124   *        Optional; the width of the toolbar area in which we can put things.
   8125   *        Some consumers pass this to avoid reflows.
   8126   *
   8127   *        While there are items in the list, this width won't change, and so
   8128   *        we can avoid flushing layout by providing it and/or caching it.
   8129   *        Note that if `shouldMoveAllItems` is true, we never need the width
   8130   *        anyway, and this value is ignored.
   8131   * @returns {Promise<undefined>}
   8132   *   Resolves once moving of items has completed.
   8133   */
   8134  async #moveItemsBackToTheirOrigin(shouldMoveAllItems, totalAvailWidth) {
   8135    lazy.log.debug(
   8136      `Attempting to move ${shouldMoveAllItems ? "all" : "some"} items back`
   8137    );
   8138    let placements = gPlacements.get(this.#toolbar.id);
   8139    let win = this.#target.ownerGlobal;
   8140    let doc = this.#target.ownerDocument;
   8141    let checkOverflowHandle = this.#checkOverflowHandle;
   8142 
   8143    let overflowedItemStack = Array.from(this.#overflowedInfo.entries());
   8144 
   8145    for (let i = overflowedItemStack.length - 1; i >= 0; --i) {
   8146      let [childID, minSize] = overflowedItemStack[i];
   8147 
   8148      // The item may have been placed inside of a <xul:panel> that is lazily
   8149      // loaded and still in the view cache. PanelMultiView.getViewNode will
   8150      // do the work of checking the DOM for the child, and then falling back to
   8151      // the cache if that is the case.
   8152      let child = lazy.PanelMultiView.getViewNode(doc, childID);
   8153 
   8154      if (!child) {
   8155        this.#overflowedInfo.delete(childID);
   8156        continue;
   8157      }
   8158 
   8159      lazy.log.debug(
   8160        `Considering moving ${child.id} back, minSize: ${minSize}`
   8161      );
   8162 
   8163      if (!shouldMoveAllItems && minSize) {
   8164        if (!totalAvailWidth) {
   8165          ({ totalAvailWidth } = await this.#getOverflowInfo());
   8166 
   8167          // If the window has closed or if we re-enter because we were waiting
   8168          // for layout, stop.
   8169          if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) {
   8170            lazy.log.debug("Window closed or #checkOverflow called again.");
   8171            return;
   8172          }
   8173        }
   8174        if (totalAvailWidth <= minSize) {
   8175          lazy.log.debug(
   8176            `Need ${minSize} but width is ${totalAvailWidth} so bailing`
   8177          );
   8178          break;
   8179        }
   8180      }
   8181 
   8182      lazy.log.debug(`Moving ${child.id} back`);
   8183      this.#overflowedInfo.delete(child.id);
   8184      let beforeNodeIndex = placements.indexOf(child.id) + 1;
   8185      // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
   8186      // we're inserting it at the end. This will mean first-in, first-out (more or less)
   8187      // leading to as little change in order as possible.
   8188      if (beforeNodeIndex == 0) {
   8189        beforeNodeIndex = placements.length;
   8190      }
   8191      let inserted = false;
   8192      for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
   8193        let beforeNode = this.#target.getElementsByAttribute(
   8194          "id",
   8195          placements[beforeNodeIndex]
   8196        )[0];
   8197        // Unfortunately, XUL add-ons can mess with nodes after they are inserted,
   8198        // and this breaks the following code if the button isn't where we expect
   8199        // it to be (ie not a child of the target). In this case, ignore the node.
   8200        if (beforeNode && this.#target == beforeNode.parentElement) {
   8201          this.#target.insertBefore(child, beforeNode);
   8202          inserted = true;
   8203          break;
   8204        }
   8205      }
   8206      if (!inserted) {
   8207        this.#target.appendChild(child);
   8208      }
   8209      child.removeAttribute("cui-anchorid");
   8210      child.removeAttribute("overflowedItem");
   8211      CustomizableUIInternal.ensureButtonContextMenu(child, this.#target);
   8212      CustomizableUIInternal.notifyListeners(
   8213        "onWidgetUnderflow",
   8214        child,
   8215        this.#target
   8216      );
   8217    }
   8218 
   8219    win.UpdateUrlbarSearchSplitterState();
   8220 
   8221    let defaultListItems = Array.from(this.#defaultList.children);
   8222    if (
   8223      defaultListItems.every(
   8224        item =>
   8225          CustomizableUI.isSpecialWidget(item.id) ||
   8226          this.#hiddenOverflowedNodes.has(item)
   8227      )
   8228    ) {
   8229      this.#toolbar.removeAttribute("overflowing");
   8230    }
   8231  }
   8232 
   8233  /**
   8234   * Checks to see if there are overflowable items within the customization
   8235   * target of the toolbar that should be moved into the overflow panel, and
   8236   * if there are, moves them.
   8237   *
   8238   * Note that since this is an async function that can be called in bursts
   8239   * by resize events on the window, this function is often re-called even
   8240   * when a prior call hasn't yet resolved. In that situation, the older calls
   8241   * resolve early without doing any work and leave any DOM manipulation to the
   8242   * most recent call.
   8243   *
   8244   * This function is a no-op if the OverflowableToolbar is disabled or the
   8245   * DOM fullscreen UI is currently being used.
   8246   *
   8247   * @returns {Promise<undefined>}
   8248   *   Resolves once any movement of toolbar items has completed.
   8249   */
   8250  async #checkOverflow() {
   8251    if (!this.#enabled) {
   8252      return;
   8253    }
   8254 
   8255    let win = this.#target.ownerGlobal;
   8256    if (win.document.documentElement.hasAttribute("inDOMFullscreen")) {
   8257      // Toolbars are hidden and cannot be made visible in DOM fullscreen mode
   8258      // so there's nothing to do here.
   8259      return;
   8260    }
   8261 
   8262    let checkOverflowHandle = (this.#checkOverflowHandle = {});
   8263 
   8264    lazy.log.debug("Checking overflow");
   8265    let { isOverflowing, totalAvailWidth } = await this.#getOverflowInfo();
   8266    if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) {
   8267      return;
   8268    }
   8269 
   8270    if (isOverflowing) {
   8271      await this.#onOverflow();
   8272    } else {
   8273      await this.#moveItemsBackToTheirOrigin(false, totalAvailWidth);
   8274    }
   8275 
   8276    if (checkOverflowHandle == this.#checkOverflowHandle) {
   8277      this.#checkOverflowHandle = null;
   8278    }
   8279  }
   8280 
   8281  /**
   8282   * Makes the OverflowableToolbar inert and moves all overflowable items back
   8283   * into the customization target of the toolbar.
   8284   */
   8285  #disable() {
   8286    // Abort any ongoing overflow check. #enable() will #checkOverflow()
   8287    // anyways, so this is enough.
   8288    this.#checkOverflowHandle = {};
   8289    this.#moveItemsBackToTheirOrigin(true);
   8290    this.#enabled = false;
   8291  }
   8292 
   8293  /**
   8294   * Puts the OverflowableToolbar into the enabled state and then checks to see
   8295   * if any of the items in the customization target should be overflowed into
   8296   * the overflow panel list.
   8297   */
   8298  #enable() {
   8299    this.#enabled = true;
   8300    this.#checkOverflow();
   8301  }
   8302 
   8303  /**
   8304   * Shows the overflow panel and sets a timeout to automatically re-hide the
   8305   * panel if it is not being hovered.
   8306   */
   8307  #showWithTimeout() {
   8308    const OVERFLOW_PANEL_HIDE_DELAY_MS = 500;
   8309 
   8310    this.show().then(() => {
   8311      let window = this.#toolbar.ownerGlobal;
   8312      if (this.#hideTimeoutId) {
   8313        window.clearTimeout(this.#hideTimeoutId);
   8314      }
   8315      this.#hideTimeoutId = window.setTimeout(() => {
   8316        if (!this.#defaultListPanel.firstElementChild.matches(":hover")) {
   8317          lazy.PanelMultiView.hidePopup(this.#defaultListPanel);
   8318        }
   8319      }, OVERFLOW_PANEL_HIDE_DELAY_MS);
   8320    });
   8321  }
   8322 
   8323  /**
   8324   * Gets and caches a reference to the DOM node with the ID set as the value
   8325   * of addon-webext-overflowtarget. If a cache already exists, that's returned
   8326   * instead. If addon-webext-overflowtarget has no value, null is returned.
   8327   *
   8328   * @returns {Element|null} the list that overflowed extension toolbar
   8329   *   buttons should go to if the Unified Extensions UI is enabled, or null
   8330   *   if no such list exists.
   8331   */
   8332  get #webExtList() {
   8333    if (!this.#webExtListRef) {
   8334      let targetID = this.#toolbar.getAttribute("addon-webext-overflowtarget");
   8335      if (!targetID) {
   8336        throw new Error(
   8337          "addon-webext-overflowtarget was not defined on the " +
   8338            `overflowable toolbar with id: ${this.#toolbar.id}`
   8339        );
   8340      }
   8341      let win = this.#toolbar.ownerGlobal;
   8342      let { panel } = win.gUnifiedExtensions;
   8343      this.#webExtListRef = panel.querySelector(`#${targetID}`);
   8344    }
   8345    return this.#webExtListRef;
   8346  }
   8347 
   8348  /**
   8349   * Returns true if aNode is not null and is one of either this.#webExtList or
   8350   * this.#defaultList.
   8351   *
   8352   * @param {DOMElement} aNode The node to test.
   8353   * @returns {boolean}
   8354   */
   8355  #isOverflowList(aNode) {
   8356    return aNode == this.#defaultList || aNode == this.#webExtList;
   8357  }
   8358 
   8359  /**
   8360   * Private event handlers start here.
   8361   */
   8362 
   8363  /**
   8364   * Handles clicks on the #defaultListButton element.
   8365   *
   8366   * @param {MouseEvent} aEvent the click event.
   8367   */
   8368  #onClickDefaultListButton(aEvent) {
   8369    if (this.#defaultListButton.open) {
   8370      this.#defaultListButton.open = false;
   8371      lazy.PanelMultiView.hidePopup(this.#defaultListPanel);
   8372    } else if (
   8373      this.#defaultListPanel.state != "hiding" &&
   8374      !this.#defaultListButton.disabled
   8375    ) {
   8376      this.show(aEvent);
   8377    }
   8378  }
   8379 
   8380  /**
   8381   * Handles the popuphiding event firing on the #defaultListPanel.
   8382   *
   8383   * @param {WidgetMouseEvent} aEvent the popuphiding event that fired on the
   8384   *   #defaultListPanel.
   8385   */
   8386  #onPanelHiding(aEvent) {
   8387    if (aEvent.target != this.#defaultListPanel) {
   8388      // Ignore context menus, <select> popups, etc.
   8389      return;
   8390    }
   8391    this.#defaultListButton.open = false;
   8392    this.#defaultListPanel.removeEventListener("dragover", this);
   8393    this.#defaultListPanel.removeEventListener("dragend", this);
   8394    let doc = aEvent.target.ownerDocument;
   8395    doc.defaultView.updateEditUIVisibility();
   8396    let contextMenuId = this.#defaultListPanel.getAttribute("context");
   8397    if (contextMenuId) {
   8398      let contextMenu = doc.getElementById(contextMenuId);
   8399      contextMenu.removeEventListener("command", this, {
   8400        capture: true,
   8401        mozSystemGroup: true,
   8402      });
   8403    }
   8404  }
   8405 
   8406  /**
   8407   * Handles a resize event fired on the window hosting this
   8408   * OverflowableToolbar.
   8409   *
   8410   * @param {UIEvent} aEvent the resize event.
   8411   */
   8412  #onResize(aEvent) {
   8413    // Ignore bubbled-up resize events.
   8414    if (aEvent.target != aEvent.currentTarget) {
   8415      return;
   8416    }
   8417    this.#checkOverflow();
   8418  }
   8419 
   8420  /**
   8421   * CustomizableUI listener methods start here.
   8422   */
   8423 
   8424  onWidgetBeforeDOMChange(aNode, aNextNode, aContainer) {
   8425    // This listener method is used to handle the case where a widget is
   8426    // moved or removed from an area via the CustomizableUI API while
   8427    // overflowed. It reorganizes the internal state of this OverflowableToolbar
   8428    // to handle that change.
   8429    if (!this.#enabled || !this.#isOverflowList(aContainer)) {
   8430      return;
   8431    }
   8432    // When we (re)move an item, update all the items that come after it in the list
   8433    // with the minsize *of the item before the to-be-removed node*. This way, we
   8434    // ensure that we try to move items back as soon as that's possible.
   8435    let updatedMinSize;
   8436    if (aNode.previousElementSibling) {
   8437      updatedMinSize = this.#overflowedInfo.get(
   8438        aNode.previousElementSibling.id
   8439      );
   8440    } else {
   8441      // Force (these) items to try to flow back into the bar:
   8442      updatedMinSize = 1;
   8443    }
   8444    let nextItem = aNode.nextElementSibling;
   8445    while (nextItem) {
   8446      this.#overflowedInfo.set(nextItem.id, updatedMinSize);
   8447      nextItem = nextItem.nextElementSibling;
   8448    }
   8449  }
   8450 
   8451  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
   8452    // This listener method is used to handle the case where a widget is
   8453    // moved or removed from an area via the CustomizableUI API while
   8454    // overflowed. It updates the DOM in the event that the movement or removal
   8455    // causes overflow or underflow of the toolbar.
   8456    if (
   8457      !this.#enabled ||
   8458      (aContainer != this.#target && !this.#isOverflowList(aContainer))
   8459    ) {
   8460      return;
   8461    }
   8462 
   8463    let nowOverflowed = this.#isOverflowList(aNode.parentNode);
   8464    let wasOverflowed = this.#overflowedInfo.has(aNode.id);
   8465 
   8466    // If this wasn't overflowed before...
   8467    if (!wasOverflowed) {
   8468      // ... but it is now, then we added to one of the overflow panels.
   8469      if (nowOverflowed) {
   8470        // We could be the first item in the overflow panel if we're being inserted
   8471        // before the previous first item in it. We can't assume the minimum
   8472        // size is the same (because the other item might be much wider), so if
   8473        // there is no previous item, just allow this item to be put back in the
   8474        // toolbar immediately by specifying a very low minimum size.
   8475        let sourceOfMinSize = aNode.previousElementSibling;
   8476        let minSize = sourceOfMinSize
   8477          ? this.#overflowedInfo.get(sourceOfMinSize.id)
   8478          : 1;
   8479        this.#overflowedInfo.set(aNode.id, minSize);
   8480        aNode.setAttribute("cui-anchorid", this.#defaultListButton.id);
   8481        aNode.setAttribute("overflowedItem", true);
   8482        CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer, true);
   8483        CustomizableUIInternal.notifyListeners(
   8484          "onWidgetOverflow",
   8485          aNode,
   8486          this.#target
   8487        );
   8488      }
   8489    } else if (!nowOverflowed) {
   8490      // If it used to be overflowed...
   8491      // ... and isn't anymore, let's remove our bookkeeping:
   8492      this.#overflowedInfo.delete(aNode.id);
   8493      aNode.removeAttribute("cui-anchorid");
   8494      aNode.removeAttribute("overflowedItem");
   8495      CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer);
   8496      CustomizableUIInternal.notifyListeners(
   8497        "onWidgetUnderflow",
   8498        aNode,
   8499        this.#target
   8500      );
   8501 
   8502      let collapsedWidgetIds = Array.from(this.#overflowedInfo.keys());
   8503      if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
   8504        this.#toolbar.removeAttribute("overflowing");
   8505      }
   8506    } else if (aNode.previousElementSibling) {
   8507      // but if it still is, it must have changed places. Bookkeep:
   8508      let prevId = aNode.previousElementSibling.id;
   8509      let minSize = this.#overflowedInfo.get(prevId);
   8510      this.#overflowedInfo.set(aNode.id, minSize);
   8511    }
   8512 
   8513    // We might overflow now if an item was added, or we may be able to move
   8514    // stuff back into the toolbar if an item was removed.
   8515    this.#checkOverflow();
   8516  }
   8517 
   8518  /**
   8519   * @returns {boolean} whether the given node is in the overflow list.
   8520   */
   8521  isInOverflowList(node) {
   8522    return node.parentNode == this.#defaultList;
   8523  }
   8524 
   8525  /**
   8526   * nsIObserver implementation starts here.
   8527   */
   8528 
   8529  observe(aSubject, aTopic) {
   8530    // This nsIObserver method allows us to defer initialization until after
   8531    // this window has finished painting and starting up.
   8532    if (
   8533      aTopic == "browser-delayed-startup-finished" &&
   8534      aSubject == this.#toolbar.ownerGlobal
   8535    ) {
   8536      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
   8537      this.init();
   8538    }
   8539  }
   8540 
   8541  /**
   8542   * nsIDOMEventListener implementation starts here.
   8543   */
   8544 
   8545  handleEvent(aEvent) {
   8546    switch (aEvent.type) {
   8547      case "aftercustomization": {
   8548        this.#enable();
   8549        break;
   8550      }
   8551      case "mousedown": {
   8552        if (aEvent.button != 0) {
   8553          break;
   8554        }
   8555        if (aEvent.target == this.#defaultListButton) {
   8556          this.#onClickDefaultListButton(aEvent);
   8557        } else {
   8558          lazy.PanelMultiView.hidePopup(this.#defaultListPanel);
   8559        }
   8560        break;
   8561      }
   8562      case "keypress": {
   8563        if (
   8564          aEvent.target == this.#defaultListButton &&
   8565          (aEvent.key == " " || aEvent.key == "Enter")
   8566        ) {
   8567          this.#onClickDefaultListButton(aEvent);
   8568        }
   8569        break;
   8570      }
   8571      case "customizationstarting": {
   8572        this.#disable();
   8573        break;
   8574      }
   8575      case "dragover": {
   8576        if (this.#enabled) {
   8577          this.#showWithTimeout();
   8578        }
   8579        break;
   8580      }
   8581      case "dragend": {
   8582        lazy.PanelMultiView.hidePopup(this.#defaultListPanel);
   8583        break;
   8584      }
   8585      case "popuphiding": {
   8586        this.#onPanelHiding(aEvent);
   8587        break;
   8588      }
   8589      case "resize": {
   8590        this.#onResize(aEvent);
   8591        break;
   8592      }
   8593    }
   8594  }
   8595 }
   8596 
   8597 CustomizableUIInternal.initialize();