tor-browser

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

CustomizeMode.sys.mjs (136189B)


      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 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
      6 const kPaletteId = "customization-palette";
      7 const kDragDataTypePrefix = "text/toolbarwrapper-id/";
      8 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
      9 const kDrawInTitlebarPref = "browser.tabs.inTitlebar";
     10 const kCompactModeShowPref = "browser.compactmode.show";
     11 const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility";
     12 const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
     13 
     14 const kPanelItemContextMenu = "customizationPanelItemContextMenu";
     15 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
     16 
     17 const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
     18 const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
     19 const kDownloadAutoHidePref = "browser.download.autohideButton";
     20 
     21 import { CustomizableUI } from "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs";
     22 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     23 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     24 
     25 const lazy = {};
     26 
     27 ChromeUtils.defineESModuleGetters(lazy, {
     28  BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
     29  DragPositionManager:
     30    "moz-src:///browser/components/customizableui/DragPositionManager.sys.mjs",
     31  URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
     32 });
     33 ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
     34  const kUrl =
     35    "chrome://browser/locale/customizableui/customizableWidgets.properties";
     36  return Services.strings.createBundle(kUrl);
     37 });
     38 XPCOMUtils.defineLazyServiceGetter(
     39  lazy,
     40  "gTouchBarUpdater",
     41  "@mozilla.org/widget/touchbarupdater;1",
     42  Ci.nsITouchBarUpdater
     43 );
     44 
     45 let gDebug;
     46 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     47  let { ConsoleAPI } = ChromeUtils.importESModule(
     48    "resource://gre/modules/Console.sys.mjs"
     49  );
     50  gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
     51  let consoleOptions = {
     52    maxLogLevel: gDebug ? "all" : "log",
     53    prefix: "CustomizeMode",
     54  };
     55  return new ConsoleAPI(consoleOptions);
     56 });
     57 
     58 var gDraggingInToolbars;
     59 
     60 var gTab;
     61 
     62 function closeGlobalTab() {
     63  let win = gTab.ownerGlobal;
     64  if (win.gBrowser.browsers.length == 1) {
     65    win.BrowserCommands.openTab();
     66  }
     67  win.gBrowser.removeTab(gTab, { animate: true });
     68  gTab = null;
     69 }
     70 
     71 var gTabsProgressListener = {
     72  onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) {
     73    // Tear down customize mode when the customize mode tab loads some other page.
     74    // Customize mode will be re-entered if "about:blank" is loaded again, so
     75    // don't tear down in this case.
     76    if (
     77      !gTab ||
     78      gTab.linkedBrowser != aBrowser ||
     79      aLocation.spec == "about:blank"
     80    ) {
     81      return;
     82    }
     83 
     84    unregisterGlobalTab();
     85  },
     86 };
     87 
     88 function unregisterGlobalTab() {
     89  gTab.removeEventListener("TabClose", unregisterGlobalTab);
     90  let win = gTab.ownerGlobal;
     91  win.removeEventListener("unload", unregisterGlobalTab);
     92  win.gBrowser.removeTabsProgressListener(gTabsProgressListener);
     93 
     94  gTab.removeAttribute("customizemode");
     95 
     96  gTab = null;
     97 }
     98 
     99 /**
    100 * This class manages the lifetime of the CustomizeMode UI in a single browser
    101 * window. There is one instance of CustomizeMode per browser window.
    102 */
    103 export class CustomizeMode {
    104  constructor(aWindow) {
    105    this.#window = aWindow;
    106    this.#document = aWindow.document;
    107    this.#browser = aWindow.gBrowser;
    108    this.areas = new Set();
    109 
    110    this.#translationObserver = new aWindow.MutationObserver(mutations =>
    111      this.#onTranslations(mutations)
    112    );
    113    this.#ensureCustomizationPanels();
    114 
    115    let content = this.$("customization-content-container");
    116    if (!content) {
    117      this.#window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
    118      let container = this.$("customization-container");
    119      container.replaceChild(
    120        this.#window.MozXULElement.parseXULToFragment(
    121          container.firstChild.data
    122        ),
    123        container.lastChild
    124      );
    125    }
    126 
    127    this.#attachEventListeners();
    128 
    129    // There are two palettes - there's the palette that can be overlayed with
    130    // toolbar items in browser.xhtml. This is invisible, and never seen by the
    131    // user. Then there's the visible palette, which gets populated and displayed
    132    // to the user when in customizing mode.
    133    this.visiblePalette = this.$(kPaletteId);
    134    this.pongArena = this.$("customization-pong-arena");
    135 
    136    if (this.#canDrawInTitlebar()) {
    137      this.#updateTitlebarCheckbox();
    138      Services.prefs.addObserver(kDrawInTitlebarPref, this);
    139    } else {
    140      this.$("customization-titlebar-visibility-checkbox").hidden = true;
    141    }
    142 
    143    // Observe pref changes to the bookmarks toolbar visibility,
    144    // since we won't get a toolbarvisibilitychange event if the
    145    // toolbar is changing from 'newtab' to 'always' in Customize mode
    146    // since the toolbar is shown with the 'newtab' setting.
    147    Services.prefs.addObserver(kBookmarksToolbarPref, this);
    148 
    149    this.#window.addEventListener("unload", this);
    150  }
    151 
    152  /**
    153   * True if CustomizeMode is in the process of being transitioned into or out
    154   * of.
    155   *
    156   * @type {boolean}
    157   */
    158  #transitioning = false;
    159 
    160  /**
    161   * A reference to the top-level browser window that this instance of
    162   * CustomizeMode is configured to use.
    163   *
    164   * @type {DOMWindow}
    165   */
    166  #window = null;
    167 
    168  /**
    169   * A reference to the top-level browser window document that this instance of
    170   * CustomizeMode is configured to use.
    171   *
    172   * @type {Document}
    173   */
    174  #document = null;
    175 
    176  /**
    177   * A reference to the Tabbrowser instance that belongs to the top-level
    178   * browser window that CustomizeMode is configured to use.
    179   *
    180   * @type {Tabbrowser}
    181   */
    182  #browser = null;
    183 
    184  /**
    185   * An array of customize target DOM nodes that this instance of CustomizeMode
    186   * can be used to manipulate. It is assumed that when targets are in this
    187   * Set, that they have drag / drop listeners attached and that their
    188   * customizable children have been wrapped as toolbarpaletteitems.
    189   *
    190   * @type {null|Set<DOMNode>}
    191   */
    192  areas = null;
    193 
    194  /**
    195   * When in customizing mode, we swap out the reference to the invisible
    196   * palette in gNavToolbox.palette for our visiblePalette. This way, for the
    197   * customizing browser window, when widgets are removed from customizable
    198   * areas and added to the palette, they're added to the visible palette.
    199   * #stowedPalette is a reference to the old invisible palette so we can
    200   * restore gNavToolbox.palette to its original state after exiting
    201   * customization mode.
    202   *
    203   * @type {null|DOMNode}
    204   */
    205  #stowedPalette = null;
    206 
    207  /**
    208   * If a drag and drop operation is underway for a customizable toolbar item,
    209   * this member is set to the current item being dragged over.
    210   *
    211   * @type {null|DOMNode}
    212   */
    213  #dragOverItem = null;
    214 
    215  /**
    216   * True if we're in the state of customizing this browser window.
    217   *
    218   * @type {boolean}
    219   */
    220  #customizing = false;
    221 
    222  /**
    223   * True if we're synthesizing drag and drop in customize mode for automated
    224   * tests and want to skip the checks that ensure that the source of the
    225   * drag events was this top-level browser window. This is controllable via
    226   * `browser.uiCustomization.skipSourceNodeCheck`.
    227   *
    228   * @type {boolean}
    229   */
    230  #skipSourceNodeCheck = false;
    231 
    232  /**
    233   * These are the commands we continue to leave enabled while in customize
    234   * mode. All other commands are disabled, and we remove the disabled attribute
    235   * when leaving customize mode.
    236   *
    237   * @type {Set<string>}
    238   */
    239  #enabledCommands = new Set([
    240    "cmd_newNavigator",
    241    "cmd_newNavigatorTab",
    242    "cmd_newNavigatorTabNoEvent",
    243    "cmd_close",
    244    "cmd_closeWindow",
    245    "cmd_maximizeWindow",
    246    "cmd_minimizeWindow",
    247    "cmd_restoreWindow",
    248    "cmd_quitApplication",
    249    "View:FullScreen",
    250    "Browser:NextTab",
    251    "Browser:PrevTab",
    252    "Browser:NewUserContextTab",
    253    "Tools:PrivateBrowsing",
    254    "zoomWindow",
    255    "cmd_newIdentity",
    256    "cmd_newCircuit",
    257  ]);
    258 
    259  /**
    260   * A MutationObserver used to hear about Fluent localization occurring for
    261   * customizable items.
    262   *
    263   * @type {MutationObserver|null}
    264   */
    265  #translationObserver = null;
    266 
    267  /**
    268   * A description of the size of a customizable item were it to be placed in a
    269   * particular customizable area.
    270   *
    271   * @typedef {object} ItemSizeForArea
    272   * @property {number} width
    273   *   The width of the customizable item when placed in a certain area.
    274   * @property {number} height
    275   *   The height of the customizable item when placed in a certain area.
    276   */
    277 
    278  /**
    279   * A WeakMap mapping dragged customizable items to a WeakMap of areas that
    280   * the item could be dragged into. That mapping maps to ItemSizeForArea
    281   * objects that describe the width and height of the item were it to be
    282   * dropped and placed within that area (since certain areas will encourage
    283   * items to expand or contract).
    284   *
    285   * @type {WeakMap<DOMNode, WeakMap<DOMNode, DragSizeForArea>>|null}
    286   */
    287  #dragSizeMap = null;
    288 
    289  /**
    290   * If this is set to true, this means that the user enabled the downloads
    291   * button auto-hide feature while the button was in the palette. If so, then
    292   * on exiting mode, the button is moved to its default position in the
    293   * navigation toolbar.
    294   */
    295  #moveDownloadsButtonToNavBar = false;
    296 
    297  /**
    298   * Returns the CustomizationHandler browser window global object. See
    299   * browser-customization.js.
    300   *
    301   * @type {object}
    302   */
    303  get #handler() {
    304    return this.#window.CustomizationHandler;
    305  }
    306 
    307  /**
    308   * Does cleanup of any listeners or observers when the browser window
    309   * that this CustomizeMode instance is configured to use unloads.
    310   */
    311  #uninit() {
    312    if (this.#canDrawInTitlebar()) {
    313      Services.prefs.removeObserver(kDrawInTitlebarPref, this);
    314    }
    315    Services.prefs.removeObserver(kBookmarksToolbarPref, this);
    316  }
    317 
    318  /**
    319   * This is a shortcut for this.#document.getElementById.
    320   *
    321   * @param {string} id
    322   *   The DOM ID to return a result for.
    323   * @returns {DOMNode|null}
    324   */
    325  $(id) {
    326    return this.#document.getElementById(id);
    327  }
    328 
    329  /**
    330   * Sets the fake tab element that will be associated with being in
    331   * customize mode. Customize mode looks similar to a "special kind of tab",
    332   * and when that tab is closed, we exit customize mode. When that tab is
    333   * switched to, we enter customize mode. If that tab is restored in the
    334   * foreground, we enter customize mode.
    335   *
    336   * This method assigns the special <xul:tab> that will represent customize
    337   * mode for this window, and sets up the relevant listeners to it. The
    338   * tab will have a "customizemode" attribute set to "true" on it, as well as
    339   * a special favicon.
    340   *
    341   * @param {MozTabbrowserTab} aTab
    342   *   The tab to act as the tab representation of customize mode.
    343   */
    344  setTab(aTab) {
    345    if (gTab == aTab) {
    346      return;
    347    }
    348 
    349    if (gTab) {
    350      closeGlobalTab();
    351    }
    352 
    353    gTab = aTab;
    354 
    355    gTab.setAttribute("customizemode", "true");
    356 
    357    if (gTab.linkedPanel) {
    358      gTab.linkedBrowser.stop();
    359    }
    360 
    361    let win = gTab.ownerGlobal;
    362 
    363    win.gBrowser.setTabTitle(gTab);
    364    win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg");
    365 
    366    gTab.addEventListener("TabClose", unregisterGlobalTab);
    367 
    368    win.gBrowser.addTabsProgressListener(gTabsProgressListener);
    369 
    370    win.addEventListener("unload", unregisterGlobalTab);
    371 
    372    if (gTab.selected) {
    373      win.gCustomizeMode.enter();
    374    }
    375  }
    376 
    377  /**
    378   * Kicks off the process of entering customize mode for the window that this
    379   * CustomizeMode instance was constructed with. If this window happens to be
    380   * a popup window or web app window, the opener window will enter customize mode.
    381   *
    382   * Entering customize mode is a multistep asynchronous operation, but this
    383   * method returns immediately while this operation is underway. A
    384   * `customizationready` custom event is dispatched on the gNavToolbox when
    385   * this asynchronous process has completed.
    386   *
    387   * This method will return early if customize mode is already active in this
    388   * window.
    389   */
    390  enter() {
    391    if (
    392      !this.#window.toolbar.visible ||
    393      this.#window.document.documentElement.hasAttribute("taskbartab")
    394    ) {
    395      let w = lazy.URILoadingHelper.getTargetWindow(this.#window, {
    396        skipPopups: true,
    397        skipTaskbarTabs: true,
    398      });
    399      if (w) {
    400        w.gCustomizeMode.enter();
    401        return;
    402      }
    403      let obs = () => {
    404        Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
    405        w = lazy.URILoadingHelper.getTargetWindow(this.#window, {
    406          skipPopups: true,
    407          skipTaskbarTabs: true,
    408        });
    409        w.gCustomizeMode.enter();
    410      };
    411      Services.obs.addObserver(obs, "browser-delayed-startup-finished");
    412      this.#window.openTrustedLinkIn("about:newtab", "window");
    413      return;
    414    }
    415    this._wantToBeInCustomizeMode = true;
    416 
    417    if (this.#customizing || this.#handler.isEnteringCustomizeMode) {
    418      return;
    419    }
    420 
    421    // Exiting; want to re-enter once we've done that.
    422    if (this.#handler.isExitingCustomizeMode) {
    423      lazy.log.debug(
    424        "Attempted to enter while we're in the middle of exiting. " +
    425          "We'll exit after we've entered"
    426      );
    427      return;
    428    }
    429 
    430    if (!gTab) {
    431      this.setTab(
    432        this.#browser.addTab("about:blank", {
    433          inBackground: false,
    434          forceNotRemote: true,
    435          skipAnimation: true,
    436          triggeringPrincipal:
    437            Services.scriptSecurityManager.getSystemPrincipal(),
    438        })
    439      );
    440      return;
    441    }
    442    if (!gTab.selected) {
    443      // This will force another .enter() to be called via the
    444      // onlocationchange handler of the tabbrowser, so we return early.
    445      gTab.ownerGlobal.gBrowser.selectedTab = gTab;
    446      return;
    447    }
    448    gTab.ownerGlobal.focus();
    449    if (gTab.ownerDocument != this.#document) {
    450      return;
    451    }
    452 
    453    let window = this.#window;
    454    let document = this.#document;
    455 
    456    this.#handler.isEnteringCustomizeMode = true;
    457 
    458    // Always disable the reset button at the start of customize mode, it'll be re-enabled
    459    // if necessary when we finish entering:
    460    let resetButton = this.$("customization-reset-button");
    461    resetButton.setAttribute("disabled", "true");
    462 
    463    (async () => {
    464      // We shouldn't start customize mode until after browser-delayed-startup has finished:
    465      if (!this.#window.gBrowserInit.delayedStartupFinished) {
    466        await new Promise(resolve => {
    467          let delayedStartupObserver = aSubject => {
    468            if (aSubject == this.#window) {
    469              Services.obs.removeObserver(
    470                delayedStartupObserver,
    471                "browser-delayed-startup-finished"
    472              );
    473              resolve();
    474            }
    475          };
    476 
    477          Services.obs.addObserver(
    478            delayedStartupObserver,
    479            "browser-delayed-startup-finished"
    480          );
    481        });
    482      }
    483 
    484      CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
    485      CustomizableUI.notifyStartCustomizing(this.#window);
    486 
    487      // Add a keypress listener to the document so that we can quickly exit
    488      // customization mode when pressing ESC.
    489      document.addEventListener("keypress", this);
    490 
    491      // Same goes for the menu button - if we're customizing, a click on the
    492      // menu button means a quick exit from customization mode.
    493      window.PanelUI.hide();
    494 
    495      let panelHolder = document.getElementById("customization-panelHolder");
    496      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
    497      this._previousPanelContextMenuParent = panelContextMenu.parentNode;
    498      document.getElementById("mainPopupSet").appendChild(panelContextMenu);
    499      panelHolder.appendChild(window.PanelUI.overflowFixedList);
    500 
    501      window.PanelUI.overflowFixedList.toggleAttribute("customizing", true);
    502      window.PanelUI.menuButton.disabled = true;
    503      document.getElementById("nav-bar-overflow-button").disabled = true;
    504 
    505      this.#transitioning = true;
    506 
    507      let customizer = document.getElementById("customization-container");
    508      let browser = document.getElementById("browser");
    509      browser.hidden = true;
    510      customizer.hidden = false;
    511 
    512      this.#wrapAreaItemsSync(CustomizableUI.AREA_TABSTRIP);
    513 
    514      this.#document.documentElement.toggleAttribute("customizing", true);
    515 
    516      let customizableToolbars = document.querySelectorAll(
    517        "toolbar[customizable=true]:not([autohide], [collapsed])"
    518      );
    519      for (let toolbar of customizableToolbars) {
    520        toolbar.toggleAttribute("customizing", true);
    521      }
    522 
    523      this.#updateOverflowPanelArrowOffset();
    524 
    525      // Let everybody in this window know that we're about to customize.
    526      CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
    527 
    528      await this.#wrapAllAreaItems();
    529      this.#populatePalette();
    530 
    531      this.#setupPaletteDragging();
    532 
    533      window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
    534 
    535      this.#updateResetButton();
    536      this.#updateUndoResetButton();
    537      this.#updateTouchBarButton();
    538      this.#updateDensityMenu();
    539 
    540      this.#skipSourceNodeCheck =
    541        Services.prefs.getPrefType(kSkipSourceNodePref) ==
    542          Ci.nsIPrefBranch.PREF_BOOL &&
    543        Services.prefs.getBoolPref(kSkipSourceNodePref);
    544 
    545      CustomizableUI.addListener(this);
    546      this.#customizing = true;
    547      this.#transitioning = false;
    548 
    549      // Show the palette now that the transition has finished.
    550      this.visiblePalette.hidden = false;
    551      window.setTimeout(() => {
    552        // Force layout reflow to ensure the animation runs,
    553        // and make it async so it doesn't affect the timing.
    554        this.visiblePalette.clientTop;
    555        this.visiblePalette.setAttribute("showing", "true");
    556      }, 0);
    557      this.#updateEmptyPaletteNotice();
    558 
    559      this.#setupDownloadAutoHideToggle();
    560 
    561      this.#handler.isEnteringCustomizeMode = false;
    562 
    563      CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
    564 
    565      if (!this._wantToBeInCustomizeMode) {
    566        this.exit();
    567      }
    568    })().catch(e => {
    569      lazy.log.error("Error entering customize mode", e);
    570      this.#handler.isEnteringCustomizeMode = false;
    571      // Exit customize mode to ensure proper clean-up when entering failed.
    572      this.exit();
    573    });
    574  }
    575 
    576  /**
    577   * Exits customize mode, if we happen to be in it. This is a no-op if we
    578   * are not in customize mode.
    579   *
    580   * This is a multi-step asynchronous operation, but this method returns
    581   * synchronously after the operation begins. An `aftercustomization`
    582   * custom event is dispatched on the gNavToolbox once the operation has
    583   * completed.
    584   */
    585  exit() {
    586    this._wantToBeInCustomizeMode = false;
    587 
    588    if (!this.#customizing || this.#handler.isExitingCustomizeMode) {
    589      return;
    590    }
    591 
    592    // Entering; want to exit once we've done that.
    593    if (this.#handler.isEnteringCustomizeMode) {
    594      lazy.log.debug(
    595        "Attempted to exit while we're in the middle of entering. " +
    596          "We'll exit after we've entered"
    597      );
    598      return;
    599    }
    600 
    601    if (this.resetting) {
    602      lazy.log.debug(
    603        "Attempted to exit while we're resetting. " +
    604          "We'll exit after resetting has finished."
    605      );
    606      return;
    607    }
    608 
    609    this.#handler.isExitingCustomizeMode = true;
    610 
    611    this.#translationObserver.disconnect();
    612 
    613    this.#teardownDownloadAutoHideToggle();
    614 
    615    CustomizableUI.removeListener(this);
    616 
    617    let window = this.#window;
    618    let document = this.#document;
    619 
    620    document.removeEventListener("keypress", this);
    621 
    622    this.#togglePong(false);
    623 
    624    // Disable the reset and undo reset buttons while transitioning:
    625    let resetButton = this.$("customization-reset-button");
    626    let undoResetButton = this.$("customization-undo-reset-button");
    627    undoResetButton.hidden = resetButton.disabled = true;
    628 
    629    this.#transitioning = true;
    630 
    631    this.#depopulatePalette();
    632 
    633    // We need to set this.#customizing to false and remove the `customizing`
    634    // attribute before removing the tab or else
    635    // XULBrowserWindow.onLocationChange might think that we're still in
    636    // customization mode and need to exit it for a second time.
    637    this.#customizing = false;
    638    document.documentElement.removeAttribute("customizing");
    639 
    640    if (this.#browser.selectedTab == gTab) {
    641      closeGlobalTab();
    642    }
    643 
    644    let customizer = document.getElementById("customization-container");
    645    let browser = document.getElementById("browser");
    646    customizer.hidden = true;
    647    browser.hidden = false;
    648 
    649    window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
    650 
    651    this.#teardownPaletteDragging();
    652 
    653    (async () => {
    654      await this.#unwrapAllAreaItems();
    655 
    656      // And drop all area references.
    657      this.areas.clear();
    658 
    659      // Let everybody in this window know that we're starting to
    660      // exit customization mode.
    661      CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
    662 
    663      window.PanelUI.menuButton.disabled = false;
    664      let overflowContainer = document.getElementById(
    665        "widget-overflow-mainView"
    666      ).firstElementChild;
    667      overflowContainer.appendChild(window.PanelUI.overflowFixedList);
    668      document.getElementById("nav-bar-overflow-button").disabled = false;
    669      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
    670      this._previousPanelContextMenuParent.appendChild(panelContextMenu);
    671 
    672      let customizableToolbars = document.querySelectorAll(
    673        "toolbar[customizable=true]:not([autohide])"
    674      );
    675      for (let toolbar of customizableToolbars) {
    676        toolbar.removeAttribute("customizing");
    677      }
    678 
    679      this.#maybeMoveDownloadsButtonToNavBar();
    680 
    681      delete this._lastLightweightTheme;
    682      this.#transitioning = false;
    683      this.#handler.isExitingCustomizeMode = false;
    684      CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
    685      CustomizableUI.notifyEndCustomizing(window);
    686 
    687      if (this._wantToBeInCustomizeMode) {
    688        this.enter();
    689      }
    690    })().catch(e => {
    691      lazy.log.error("Error exiting customize mode", e);
    692      this.#handler.isExitingCustomizeMode = false;
    693    });
    694  }
    695 
    696  /**
    697   * The overflow panel in customize mode should have its arrow pointing
    698   * at the overflow button. In order to do this correctly, we pass the
    699   * distance between the inside of window and the middle of the button
    700   * to the customize mode markup in which the arrow and panel are placed.
    701   *
    702   * The returned Promise resolves once the offset has been set on the panel
    703   * wrapper.
    704   *
    705   * @returns {Promise<undefined>}
    706   */
    707  async #updateOverflowPanelArrowOffset() {
    708    let currentDensity =
    709      this.#document.documentElement.getAttribute("uidensity");
    710    let offset = await this.#window.promiseDocumentFlushed(() => {
    711      let overflowButton = this.$("nav-bar-overflow-button");
    712      let buttonRect = overflowButton.getBoundingClientRect();
    713      let endDistance;
    714      if (this.#window.RTL_UI) {
    715        endDistance = buttonRect.left;
    716      } else {
    717        endDistance = this.#window.innerWidth - buttonRect.right;
    718      }
    719      return endDistance + buttonRect.width / 2;
    720    });
    721    if (
    722      !this.#document ||
    723      currentDensity != this.#document.documentElement.getAttribute("uidensity")
    724    ) {
    725      return;
    726    }
    727    this.$("customization-panelWrapper").style.setProperty(
    728      "--panel-arrow-offset",
    729      offset + "px"
    730    );
    731  }
    732 
    733  /**
    734   * Given some DOM node, attempts to resolve to the relevant child or overflow
    735   * target node that can have customizable items placed within it. It may
    736   * resolve to  aNode itself, if aNode can have customizable items placed
    737   * directly within it. If the node does not appear to have such a child, this
    738   * method will return `null`.
    739   *
    740   * @param {DOMNode} aNode
    741   * @returns {null|DOMNode}
    742   */
    743  #getCustomizableChildForNode(aNode) {
    744    // NB: adjusted from #getCustomizableParent to keep that method fast
    745    // (it's used during drags), and avoid multiple DOM loops
    746    let areas = CustomizableUI.areas;
    747    // Caching this length is important because otherwise we'll also iterate
    748    // over items we add to the end from within the loop.
    749    let numberOfAreas = areas.length;
    750    for (let i = 0; i < numberOfAreas; i++) {
    751      let area = areas[i];
    752      let areaNode = aNode.ownerDocument.getElementById(area);
    753      let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
    754      if (customizationTarget && customizationTarget != areaNode) {
    755        areas.push(customizationTarget.id);
    756      }
    757      let overflowTarget =
    758        areaNode && areaNode.getAttribute("default-overflowtarget");
    759      if (overflowTarget) {
    760        areas.push(overflowTarget);
    761      }
    762    }
    763    areas.push(kPaletteId);
    764 
    765    while (aNode && aNode.parentNode) {
    766      let parent = aNode.parentNode;
    767      if (areas.includes(parent.id)) {
    768        return aNode;
    769      }
    770      aNode = parent;
    771    }
    772    return null;
    773  }
    774 
    775  /**
    776   * Kicks off an animation for aNode that causes it to scale down and become
    777   * transparent before being removed from a toolbar. This can be seen when
    778   * using the context menu to remove an item from a toolbar.
    779   *
    780   * For nodes that are within the overflow panel, aren't
    781   * toolbaritem / toolbarbuttons, or is the hidden downloads button, this
    782   * returns `null` immediately and no animation is performed. If reduced
    783   * motion is enabled, this returns `null` immediately and no animation is
    784   * performed.
    785   *
    786   * @param {DOMNode} aNode
    787   *   The node to be removed from the toolbar.
    788   * @returns {null|Promise<DOMNode>}
    789   *   Returns `null` if no animation is going to occur, or the DOMNode that
    790   *   the animation was performed on after the animation has completed.
    791   */
    792  #promiseWidgetAnimationOut(aNode) {
    793    if (
    794      this.#window.gReduceMotion ||
    795      aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
    796      (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
    797      (aNode.id == "downloads-button" && aNode.hidden)
    798    ) {
    799      return null;
    800    }
    801 
    802    let animationNode;
    803    if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
    804      animationNode = aNode.parentNode;
    805    } else {
    806      animationNode = aNode;
    807    }
    808    return new Promise(resolve => {
    809      function cleanupCustomizationExit() {
    810        resolveAnimationPromise();
    811      }
    812 
    813      function cleanupWidgetAnimationEnd(e) {
    814        if (
    815          e.animationName == "widget-animate-out" &&
    816          e.target.id == animationNode.id
    817        ) {
    818          resolveAnimationPromise();
    819        }
    820      }
    821 
    822      function resolveAnimationPromise() {
    823        animationNode.removeEventListener(
    824          "animationend",
    825          cleanupWidgetAnimationEnd
    826        );
    827        animationNode.removeEventListener(
    828          "customizationending",
    829          cleanupCustomizationExit
    830        );
    831        resolve(animationNode);
    832      }
    833 
    834      // Wait until the next frame before setting the class to ensure
    835      // we do start the animation.
    836      this.#window.requestAnimationFrame(() => {
    837        this.#window.requestAnimationFrame(() => {
    838          animationNode.classList.add("animate-out");
    839          animationNode.ownerGlobal.gNavToolbox.addEventListener(
    840            "customizationending",
    841            cleanupCustomizationExit
    842          );
    843          animationNode.addEventListener(
    844            "animationend",
    845            cleanupWidgetAnimationEnd
    846          );
    847        });
    848      });
    849    });
    850  }
    851 
    852  /**
    853   * Moves a customizable item to the end of the navbar. This is used primarily
    854   * by the toolbar item context menus regardless of whether or not we're in
    855   * customize mode. If this item is within a toolbar, this may kick off an
    856   * animation that shrinks the item icon and causes it to become transparent
    857   * before the move occurs. The returned Promise resolves once the animation
    858   * and the move has completed.
    859   *
    860   * @param {DOMNode} aNode
    861   *   The node to be moved to the end of the navbar area.
    862   * @returns {Promise<undefined>}
    863   *   Resolves once the move has completed.
    864   */
    865  async addToToolbar(aNode) {
    866    aNode = this.#getCustomizableChildForNode(aNode);
    867    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
    868      aNode = aNode.firstElementChild;
    869    }
    870    let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode);
    871    let animationNode;
    872    if (widgetAnimationPromise) {
    873      animationNode = await widgetAnimationPromise;
    874    }
    875 
    876    let widgetToAdd = aNode.id;
    877    if (
    878      CustomizableUI.isSpecialWidget(widgetToAdd) &&
    879      aNode.closest("#customization-palette")
    880    ) {
    881      widgetToAdd = widgetToAdd.match(
    882        /^customizableui-special-(spring|spacer|separator)/
    883      )[1];
    884    }
    885 
    886    CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
    887    lazy.BrowserUsageTelemetry.recordWidgetChange(
    888      widgetToAdd,
    889      CustomizableUI.AREA_NAVBAR
    890    );
    891    if (!this.#customizing) {
    892      CustomizableUI.dispatchToolboxEvent("customizationchange");
    893    }
    894 
    895    // If the user explicitly moves this item, turn off autohide.
    896    if (aNode.id == "downloads-button") {
    897      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
    898      if (this.#customizing) {
    899        this.#showDownloadsAutoHidePanel();
    900      }
    901    }
    902 
    903    if (animationNode) {
    904      animationNode.classList.remove("animate-out");
    905    }
    906  }
    907 
    908  /**
    909   * Pins a customizable widget to the overflow panel. This is used by the
    910   * toolbar item context menus regardless of whether or not we're in customize
    911   * mode. It is also on the widget context menu within the overflow panel when
    912   * the widget is placed there temporarily during a toolbar overflow. If this
    913   * item is within a toolbar, this may kick off an* animation that shrinks the
    914   * item icon and causes it to become transparent before the move occurs. The
    915   * returned Promise resolves once the animation and the move has completed.
    916   *
    917   * @param {DOMNode} aNode
    918   *   The node to be moved to the overflow panel.
    919   * @param {string} aReason
    920   *   A string to describe why the item is being moved to the overflow panel.
    921   *   This is passed along to BrowserUsageTelemetry.recordWidgetChange, and
    922   *   is dash-delimited.
    923   *
    924   *   Examples:
    925   *
    926   *   "toolbar-context-menu": the reason is that the user chose to do this via
    927   *     the toolbar context menu.
    928   *
    929   *   "panelitem-context": the reason is that the user chose to do this via
    930   *     the overflow panel item context menu when the item was moved there
    931   *     temporarily during a toolbar overflow.
    932   * @returns {Promise<undefined>}
    933   *   Resolves once the move has completed.
    934   */
    935  async addToPanel(aNode, aReason) {
    936    aNode = this.#getCustomizableChildForNode(aNode);
    937    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
    938      aNode = aNode.firstElementChild;
    939    }
    940    let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode);
    941    let animationNode;
    942    if (widgetAnimationPromise) {
    943      animationNode = await widgetAnimationPromise;
    944    }
    945 
    946    let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
    947    CustomizableUI.addWidgetToArea(aNode.id, panel);
    948    lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason);
    949    if (!this.#customizing) {
    950      CustomizableUI.dispatchToolboxEvent("customizationchange");
    951    }
    952 
    953    // If the user explicitly moves this item, turn off autohide.
    954    if (aNode.id == "downloads-button") {
    955      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
    956      if (this.#customizing) {
    957        this.#showDownloadsAutoHidePanel();
    958      }
    959    }
    960 
    961    if (animationNode) {
    962      animationNode.classList.remove("animate-out");
    963    }
    964    if (!this.#window.gReduceMotion) {
    965      let overflowButton = this.$("nav-bar-overflow-button");
    966      overflowButton.setAttribute("animate", "true");
    967      overflowButton.addEventListener(
    968        "animationend",
    969        function onAnimationEnd(event) {
    970          if (event.animationName.startsWith("overflow-animation")) {
    971            this.removeEventListener("animationend", onAnimationEnd);
    972            this.removeAttribute("animate");
    973          }
    974        }
    975      );
    976    }
    977  }
    978 
    979  /**
    980   * Removes a customizable item from its area and puts it in the palette. This
    981   * is used by the toolbar item context menus regardless of whether or not
    982   * we're in customize mode. It is also on the widget context menu within the
    983   * overflow panel when the widget is placed there temporarily during a toolbar
    984   * overflow. If this item is within a toolbar, this may kick off an animation
    985   * that shrinks the item icon and causes it to become transparent before the
    986   * removal occurs. The returned Promise resolves once the animation and the
    987   * removal has completed.
    988   *
    989   * @param {DOMNode} aNode
    990   *   The node to be removed and placed into the palette.
    991   * @param {string} aReason
    992   *   A string to describe why the item is being removed.
    993   *   This is passed along to BrowserUsageTelemetry.recordWidgetChange, and
    994   *   is dash-delimited.
    995   *
    996   *   Examples:
    997   *
    998   *   "toolbar-context-menu": the reason is that the user chose to do this via
    999   *     the toolbar context menu.
   1000   *
   1001   *   "panelitem-context": the reason is that the user chose to do this via
   1002   *     the overflow panel item context menu when the item was moved there
   1003   *     temporarily during a toolbar overflow.
   1004   * @returns {Promise<undefined>}
   1005   *   Resolves once the removal has completed.
   1006   */
   1007  async removeFromArea(aNode, aReason) {
   1008    aNode = this.#getCustomizableChildForNode(aNode);
   1009    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
   1010      aNode = aNode.firstElementChild;
   1011    }
   1012    let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode);
   1013    let animationNode;
   1014    if (widgetAnimationPromise) {
   1015      animationNode = await widgetAnimationPromise;
   1016    }
   1017 
   1018    CustomizableUI.removeWidgetFromArea(aNode.id);
   1019    lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason);
   1020    if (!this.#customizing) {
   1021      CustomizableUI.dispatchToolboxEvent("customizationchange");
   1022    }
   1023 
   1024    // If the user explicitly removes this item, turn off autohide.
   1025    if (aNode.id == "downloads-button") {
   1026      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
   1027      if (this.#customizing) {
   1028        this.#showDownloadsAutoHidePanel();
   1029      }
   1030    }
   1031    if (animationNode) {
   1032      animationNode.classList.remove("animate-out");
   1033    }
   1034  }
   1035 
   1036  /**
   1037   * Populates the visible palette seen in the content area when entering
   1038   * customize mode. This moves items from the normal "hidden" palette that
   1039   * belongs to the toolbox, and then temporarily overrides the toolbox
   1040   * with the visible palette until we exit customize mode.
   1041   */
   1042  #populatePalette() {
   1043    let fragment = this.#document.createDocumentFragment();
   1044    let toolboxPalette = this.#window.gNavToolbox.palette;
   1045 
   1046    try {
   1047      let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
   1048      for (let widget of unusedWidgets) {
   1049        let paletteItem = this.#makePaletteItem(widget);
   1050        if (!paletteItem) {
   1051          continue;
   1052        }
   1053        fragment.appendChild(paletteItem);
   1054      }
   1055 
   1056      let flexSpace = CustomizableUI.createSpecialWidget(
   1057        "spring",
   1058        this.#document
   1059      );
   1060      fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));
   1061 
   1062      this.visiblePalette.appendChild(fragment);
   1063      this.#stowedPalette = this.#window.gNavToolbox.palette;
   1064      this.#window.gNavToolbox.palette = this.visiblePalette;
   1065 
   1066      // Now that the palette items are all here, disable all commands.
   1067      // We do this here rather than directly in `enter` because we
   1068      // need to do/undo this when we're called from reset(), too.
   1069      this.#updateCommandsDisabledState(true);
   1070    } catch (ex) {
   1071      lazy.log.error(ex);
   1072    }
   1073  }
   1074 
   1075  /**
   1076   * For a given widget, finds the associated node within this window and then
   1077   * creates / updates a toolbarpaletteitem that will wrap that node to make
   1078   * drag and drop operations on that node work while in customize mode.
   1079   *
   1080   * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget
   1081   * @returns {DOMNode}
   1082   */
   1083  #makePaletteItem(aWidget) {
   1084    let widgetNode = aWidget.forWindow(this.#window).node;
   1085    if (!widgetNode) {
   1086      lazy.log.error(
   1087        "Widget with id " + aWidget.id + " does not return a valid node"
   1088      );
   1089      return null;
   1090    }
   1091    // Do not build a palette item for hidden widgets; there's not much to show.
   1092    if (widgetNode.hidden) {
   1093      return null;
   1094    }
   1095 
   1096    let wrapper = this.createOrUpdateWrapper(widgetNode, "palette");
   1097    wrapper.appendChild(widgetNode);
   1098    return wrapper;
   1099  }
   1100 
   1101  /**
   1102   * Unwraps and moves items from the visible palette back to the hidden palette
   1103   * when exiting customize mode. This also reassigns the toolbox's palette
   1104   * property to point back at the default hidden palette, as this was
   1105   * overridden to be the visible palette in #populatePalette.
   1106   */
   1107  #depopulatePalette() {
   1108    // Quick, undo the command disabling before we depopulate completely:
   1109    this.#updateCommandsDisabledState(false);
   1110 
   1111    this.visiblePalette.hidden = true;
   1112    let paletteChild = this.visiblePalette.firstElementChild;
   1113    let nextChild;
   1114    while (paletteChild) {
   1115      nextChild = paletteChild.nextElementSibling;
   1116      let itemId = paletteChild.firstElementChild.id;
   1117      if (CustomizableUI.isSpecialWidget(itemId)) {
   1118        this.visiblePalette.removeChild(paletteChild);
   1119      } else {
   1120        // XXXunf Currently this doesn't destroy the (now unused) node in the
   1121        //       API provider case. It would be good to do so, but we need to
   1122        //       keep strong refs to it in CustomizableUI (can't iterate of
   1123        //       WeakMaps), and there's the question of what behavior
   1124        //       wrappers should have if consumers keep hold of them.
   1125        let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild);
   1126        this.#stowedPalette.appendChild(unwrappedPaletteItem);
   1127      }
   1128 
   1129      paletteChild = nextChild;
   1130    }
   1131    this.visiblePalette.hidden = false;
   1132    this.#window.gNavToolbox.palette = this.#stowedPalette;
   1133  }
   1134 
   1135  /**
   1136   * For all <command> elements in the window document that have no ID, or
   1137   * are not in the #enabledCommands set, puts them in the "disabled" state if
   1138   * `shouldBeDisabled` is true. For any command that was already disabled, adds
   1139   * a "wasdisabled" attribute to the command.
   1140   *
   1141   * If `shouldBeDisabled` is false, removes the "wasdisabled" attribute from
   1142   * any command nodes that have them, and for those that don't, removes the
   1143   * "disabled" attribute.
   1144   *
   1145   * @param {boolean} shouldBeDisabled
   1146   *   True if all <command> elements not in #enabledCommands should be
   1147   *   disabled. False otherwise.
   1148   */
   1149  #updateCommandsDisabledState(shouldBeDisabled) {
   1150    for (let command of this.#document.querySelectorAll("command")) {
   1151      if (!command.id || !this.#enabledCommands.has(command.id)) {
   1152        if (shouldBeDisabled) {
   1153          if (!command.hasAttribute("disabled")) {
   1154            command.setAttribute("disabled", true);
   1155          } else {
   1156            command.setAttribute("wasdisabled", true);
   1157          }
   1158        } else if (command.getAttribute("wasdisabled") != "true") {
   1159          command.removeAttribute("disabled");
   1160        } else {
   1161          command.removeAttribute("wasdisabled");
   1162        }
   1163      }
   1164    }
   1165  }
   1166 
   1167  /**
   1168   * Checks if the passed in DOM node is one that can represent a
   1169   * customizable widget.
   1170   *
   1171   * @param {DOMNode} aNode
   1172   *   The node to check to see if it's a customizable widget node.
   1173   * @returns {boolean}
   1174   *   `true` if the passed in DOM node is a type that can be used for
   1175   *   customizable widgets.
   1176   */
   1177  #isCustomizableItem(aNode) {
   1178    return (
   1179      aNode.localName == "toolbarbutton" ||
   1180      aNode.localName == "toolbaritem" ||
   1181      aNode.localName == "toolbarseparator" ||
   1182      aNode.localName == "toolbarspring" ||
   1183      aNode.localName == "toolbarspacer"
   1184    );
   1185  }
   1186 
   1187  /**
   1188   * Checks if the passed in DOM node is a toolbarpaletteitem wrapper.
   1189   *
   1190   * @param {DOMNode} aNode
   1191   *   The node to check for being wrapped.
   1192   * @returns {boolean}
   1193   *   `true` if the passed in DOM node is a toolbarpaletteitem, meaning
   1194   *   that it was wrapped via createOrUpdateWrapper.
   1195   */
   1196  isWrappedToolbarItem(aNode) {
   1197    return aNode.localName == "toolbarpaletteitem";
   1198  }
   1199 
   1200  /**
   1201   * Queues a function for the main thread to execute soon that will wrap
   1202   * aNode in a toolbarpaletteitem (or update the wrapper if it already exists).
   1203   *
   1204   * @param {DOMNode} aNode
   1205   *   The node to wrap in a toolbarpaletteitem.
   1206   * @param {string} aPlace
   1207   *   The string to set as the "place" attribute on the node when it is
   1208   *   wrapped. This is expected to be one of the strings returned by
   1209   *   CustomizableUI.getPlaceForItem.
   1210   * @returns {Promise<DOMNode>}
   1211   *   Resolves with the wrapper node, or the node itself if the node is not
   1212   *   a customizable item.
   1213   */
   1214  #deferredWrapToolbarItem(aNode, aPlace) {
   1215    return new Promise(resolve => {
   1216      Services.tm.dispatchToMainThread(() => {
   1217        let wrapper = this.wrapToolbarItem(aNode, aPlace);
   1218        resolve(wrapper);
   1219      });
   1220    });
   1221  }
   1222 
   1223  /**
   1224   * Creates or updates a wrapping toolbarpaletteitem around aNode, presuming
   1225   * the node is a customizable item.
   1226   *
   1227   * @param {DOMNode} aNode
   1228   *   The node to wrap, or update the wrapper for.
   1229   * @param {string} aPlace
   1230   *   The string to set as the "place" attribute on the node when it is
   1231   *   wrapped. This is expected to be one of the strings returned by
   1232   *   CustomizableUI.getPlaceForItem.
   1233   * @returns {DOMNode}
   1234   *   The toolbarbpaletteitem wrapper, in the event that aNode is a
   1235   *   customizable item. Otherwise, returns aNode.
   1236   */
   1237  wrapToolbarItem(aNode, aPlace) {
   1238    if (!this.#isCustomizableItem(aNode)) {
   1239      return aNode;
   1240    }
   1241    let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
   1242 
   1243    // It's possible that this toolbar node is "mid-flight" and doesn't have
   1244    // a parent, in which case we skip replacing it. This can happen if a
   1245    // toolbar item has been dragged into the palette. In that case, we tell
   1246    // CustomizableUI to remove the widget from its area before putting the
   1247    // widget in the palette - so the node will have no parent.
   1248    if (aNode.parentNode) {
   1249      aNode = aNode.parentNode.replaceChild(wrapper, aNode);
   1250    }
   1251    wrapper.appendChild(aNode);
   1252    return wrapper;
   1253  }
   1254 
   1255  /**
   1256   * Helper to set the title and tooltiptext on a toolbarpaletteitem wrapper
   1257   * based on the wrapped node - either by reading the label/title attributes
   1258   * of aNode, or (in the event of delayed Fluent localization) setting up a
   1259   * mutation observer on aNode such that when the label and/or title do
   1260   * get set, we re-enter #updateWrapperLabel to update the toolbarpaletteitem.
   1261   *
   1262   * @param {DOMNode} aNode
   1263   *   The wrapped customizable item to update the wrapper for.
   1264   * @param {boolean} aIsUpdate
   1265   *   True if the node already has a pre-existing wrapper that is being
   1266   *   updated rather than created.
   1267   * @param {DOMNode} [aWrapper=aNode.parentElement]
   1268   *   The toolbarpaletteitem wrapper, in the event that it's not the
   1269   *   immediate ancestor of aNode for some reason.
   1270   */
   1271  #updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) {
   1272    if (aNode.hasAttribute("label")) {
   1273      aWrapper.setAttribute("title", aNode.getAttribute("label"));
   1274      aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
   1275    } else if (aNode.hasAttribute("title")) {
   1276      aWrapper.setAttribute("title", aNode.getAttribute("title"));
   1277      aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
   1278    } else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) {
   1279      this.#translationObserver.observe(aNode, {
   1280        attributes: true,
   1281        attributeFilter: ["label", "title"],
   1282      });
   1283    }
   1284  }
   1285 
   1286  /**
   1287   * Called when a node without a label or title has those attributes updated.
   1288   *
   1289   * @param {MutationRecord[]} aMutations
   1290   *   The list of mutations for the label/title attributes of the nodes that
   1291   *   had neither of those attributes set when wrapping them.
   1292   */
   1293  #onTranslations(aMutations) {
   1294    for (let mut of aMutations) {
   1295      let { target } = mut;
   1296      if (
   1297        target.parentElement?.localName == "toolbarpaletteitem" &&
   1298        (target.hasAttribute("label") || mut.target.hasAttribute("title"))
   1299      ) {
   1300        this.#updateWrapperLabel(target, true);
   1301      }
   1302    }
   1303  }
   1304 
   1305  /**
   1306   * Creates or updates a toolbarpaletteitem to wrap a customizable item. This
   1307   * wrapper makes it possible to click and drag these customizable items around
   1308   * in the DOM without the underlying item having its event handlers invoked.
   1309   *
   1310   * @param {DOMNode} aNode
   1311   *   The node to create or update the toolbarpaletteitem wrapper for.
   1312   * @param {string} aPlace
   1313   *   The string to set as the "place" attribute on the node when it is
   1314   *   wrapped. This is expected to be one of the strings returned by
   1315   *   CustomizableUI.getPlaceForItem.
   1316   * @param {boolean} aIsUpdate
   1317   *   True if it is expected that aNode is already wrapped and that we're
   1318   *   updating the wrapper rather than creating it.
   1319   * @returns {DOMNode}
   1320   *   Returns the created or updated toolbarpaletteitem wrapper.
   1321   */
   1322  createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
   1323    let wrapper;
   1324    if (
   1325      aIsUpdate &&
   1326      aNode.parentNode &&
   1327      aNode.parentNode.localName == "toolbarpaletteitem"
   1328    ) {
   1329      wrapper = aNode.parentNode;
   1330      aPlace = wrapper.getAttribute("place");
   1331    } else {
   1332      wrapper = this.#document.createXULElement("toolbarpaletteitem");
   1333      // "place" is used to show the label when it's sitting in the palette.
   1334      wrapper.setAttribute("place", aPlace);
   1335    }
   1336 
   1337    // Ensure the wrapped item doesn't look like it's in any special state, and
   1338    // can't be interactved with when in the customization palette.
   1339    // Note that some buttons opt out of this with the
   1340    // keepbroadcastattributeswhencustomizing attribute.
   1341    if (
   1342      aNode.hasAttribute("command") &&
   1343      aNode.getAttribute(kKeepBroadcastAttributes) != "true"
   1344    ) {
   1345      wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
   1346      aNode.removeAttribute("command");
   1347    }
   1348 
   1349    if (
   1350      aNode.hasAttribute("observes") &&
   1351      aNode.getAttribute(kKeepBroadcastAttributes) != "true"
   1352    ) {
   1353      wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
   1354      aNode.removeAttribute("observes");
   1355    }
   1356 
   1357    if (aNode.hasAttribute("checked")) {
   1358      wrapper.setAttribute("itemchecked", "true");
   1359      aNode.removeAttribute("checked");
   1360    }
   1361 
   1362    if (aNode.hasAttribute("id")) {
   1363      wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
   1364    }
   1365 
   1366    this.#updateWrapperLabel(aNode, aIsUpdate, wrapper);
   1367 
   1368    if (aNode.hasAttribute("flex")) {
   1369      wrapper.setAttribute("flex", aNode.getAttribute("flex"));
   1370    }
   1371 
   1372    let removable =
   1373      aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
   1374    wrapper.setAttribute("removable", removable);
   1375 
   1376    // Allow touch events to initiate dragging in customize mode.
   1377    // This is only supported on Windows for now.
   1378    wrapper.setAttribute("touchdownstartsdrag", "true");
   1379 
   1380    let contextMenuAttrName = "";
   1381    if (aNode.getAttribute("context")) {
   1382      contextMenuAttrName = "context";
   1383    } else if (aNode.getAttribute("contextmenu")) {
   1384      contextMenuAttrName = "contextmenu";
   1385    }
   1386    let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
   1387    let contextMenuForPlace =
   1388      aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu;
   1389    if (aPlace != "toolbar") {
   1390      wrapper.setAttribute("context", contextMenuForPlace);
   1391    }
   1392    // Only keep track of the menu if it is non-default.
   1393    if (currentContextMenu && currentContextMenu != contextMenuForPlace) {
   1394      aNode.setAttribute("wrapped-context", currentContextMenu);
   1395      aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName);
   1396      aNode.removeAttribute(contextMenuAttrName);
   1397    } else if (currentContextMenu == contextMenuForPlace) {
   1398      aNode.removeAttribute(contextMenuAttrName);
   1399    }
   1400 
   1401    // Only add listeners for newly created wrappers:
   1402    if (!aIsUpdate) {
   1403      wrapper.addEventListener("mousedown", this);
   1404      wrapper.addEventListener("mouseup", this);
   1405    }
   1406 
   1407    if (CustomizableUI.isSpecialWidget(aNode.id)) {
   1408      wrapper.setAttribute(
   1409        "title",
   1410        lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
   1411      );
   1412    }
   1413 
   1414    return wrapper;
   1415  }
   1416 
   1417  /**
   1418   * Queues a function for the main thread to execute soon that will unwrap
   1419   * the node wrapped with aWrapper, which should be a toolbarpaletteitem.
   1420   *
   1421   * @param {DOMNode} aWrapper
   1422   *   The toolbarpaletteitem wrapper around a node to unwrap.
   1423   * @returns {Promise<DOMNode|null>}
   1424   *   Resolves with the unwrapped node, or null in the event that some
   1425   *   problem occurred while unwrapping (which will be logged).
   1426   */
   1427  #deferredUnwrapToolbarItem(aWrapper) {
   1428    return new Promise(resolve => {
   1429      Services.tm.dispatchToMainThread(() => {
   1430        let item = null;
   1431        try {
   1432          item = this.unwrapToolbarItem(aWrapper);
   1433        } catch (ex) {
   1434          console.error(ex);
   1435        }
   1436        resolve(item);
   1437      });
   1438    });
   1439  }
   1440 
   1441  /**
   1442   * Unwraps a customizable item wrapped with a toolbarpaletteitem. If the
   1443   * passed in aWrapper is not a toolbarpaletteitem, this just returns
   1444   * aWrapper.
   1445   *
   1446   * @param {DOMNode} aWrapper
   1447   *   The toolbarpaletteitem wrapper around a node to be unwrapped.
   1448   * @returns {DOMNode|null}
   1449   *   Returns the unwrapped customizable item, or null if something went wrong
   1450   *   while unwrapping.
   1451   */
   1452  unwrapToolbarItem(aWrapper) {
   1453    if (aWrapper.nodeName != "toolbarpaletteitem") {
   1454      return aWrapper;
   1455    }
   1456    aWrapper.removeEventListener("mousedown", this);
   1457    aWrapper.removeEventListener("mouseup", this);
   1458 
   1459    let place = aWrapper.getAttribute("place");
   1460 
   1461    let toolbarItem = aWrapper.firstElementChild;
   1462    if (!toolbarItem) {
   1463      lazy.log.error(
   1464        "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id
   1465      );
   1466      aWrapper.remove();
   1467      return null;
   1468    }
   1469 
   1470    if (aWrapper.hasAttribute("itemobserves")) {
   1471      toolbarItem.setAttribute(
   1472        "observes",
   1473        aWrapper.getAttribute("itemobserves")
   1474      );
   1475    }
   1476 
   1477    if (aWrapper.hasAttribute("itemchecked")) {
   1478      toolbarItem.checked = true;
   1479    }
   1480 
   1481    if (aWrapper.hasAttribute("itemcommand")) {
   1482      let commandID = aWrapper.getAttribute("itemcommand");
   1483      toolbarItem.setAttribute("command", commandID);
   1484 
   1485      // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
   1486      let command = this.$(commandID);
   1487      if (command?.hasAttribute("disabled")) {
   1488        toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
   1489      }
   1490    }
   1491 
   1492    let wrappedContext = toolbarItem.getAttribute("wrapped-context");
   1493    if (wrappedContext) {
   1494      let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
   1495      toolbarItem.setAttribute(contextAttrName, wrappedContext);
   1496      toolbarItem.removeAttribute("wrapped-contextAttrName");
   1497      toolbarItem.removeAttribute("wrapped-context");
   1498    } else if (place == "panel") {
   1499      toolbarItem.setAttribute("context", kPanelItemContextMenu);
   1500    }
   1501 
   1502    if (aWrapper.parentNode) {
   1503      aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
   1504    }
   1505    return toolbarItem;
   1506  }
   1507 
   1508  /**
   1509   * For a given area, iterates the children within its customize target,
   1510   * and asynchronously wraps each customizable child in a
   1511   * toolbarpaletteitem. This also adds drag and drop handlers to the
   1512   * customize target for the area.
   1513   *
   1514   * @param {string} aArea
   1515   *   The ID of the area to wrap the children for in the window that this
   1516   *   CustomizeMode was instantiated for.
   1517   * @returns {Promise<DOMNode|null>}
   1518   *   Resolves with the customize target DOMNode for aArea for the window that
   1519   *   this CustomizeMode was instantiated for - or null if the customize target
   1520   *   cannot be found or is unknown to CustomizeMode.
   1521   */
   1522  async #wrapAreaItems(aArea) {
   1523    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.#window);
   1524    if (!target || this.areas.has(target)) {
   1525      return null;
   1526    }
   1527 
   1528    this.#addCustomizeTargetDragAndDropHandlers(target);
   1529    for (let child of target.children) {
   1530      if (
   1531        this.#isCustomizableItem(child) &&
   1532        !this.isWrappedToolbarItem(child)
   1533      ) {
   1534        await this.#deferredWrapToolbarItem(
   1535          child,
   1536          CustomizableUI.getPlaceForItem(child)
   1537        ).catch(lazy.log.error);
   1538      }
   1539    }
   1540    this.areas.add(target);
   1541    return target;
   1542  }
   1543 
   1544  /**
   1545   * A synchronous version of #wrapAreaItems that will wrap all of the
   1546   * customizable children of aArea's customize target in toolbarpaletteitems.
   1547   *
   1548   * @param {string} aArea
   1549   *   The ID of the area to wrap the children for in the window that this
   1550   *   CustomizeMode was instantiated for.
   1551   * @returns {DOMNode|null}
   1552   *   Returns the customize target DOMNode for aArea for the window that
   1553   *   this CustomizeMode was instantiated for - or null if the customize target
   1554   *   cannot be found or is unknown to CustomizeMode.
   1555   */
   1556  #wrapAreaItemsSync(aArea) {
   1557    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.#window);
   1558    if (!target || this.areas.has(target)) {
   1559      return null;
   1560    }
   1561 
   1562    this.#addCustomizeTargetDragAndDropHandlers(target);
   1563    try {
   1564      for (let child of target.children) {
   1565        if (
   1566          this.#isCustomizableItem(child) &&
   1567          !this.isWrappedToolbarItem(child)
   1568        ) {
   1569          this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
   1570        }
   1571      }
   1572    } catch (ex) {
   1573      lazy.log.error(ex, ex.stack);
   1574    }
   1575 
   1576    this.areas.add(target);
   1577    return target;
   1578  }
   1579 
   1580  /**
   1581   * Iterates all areas and asynchronously wraps their customize target children
   1582   * with toolbarpaletteitems in the window that this CustomizeMode was
   1583   * constructed for.
   1584   *
   1585   * @returns {Promise<undefined>}
   1586   *   Resolves when wrapping has completed.
   1587   */
   1588  async #wrapAllAreaItems() {
   1589    for (let area of CustomizableUI.areas) {
   1590      await this.#wrapAreaItems(area);
   1591    }
   1592  }
   1593 
   1594  /**
   1595   * Adds capturing drag and drop handlers for some customize target for some
   1596   * customizable area. These handlers delegate event handling to the
   1597   * handleEvent method.
   1598   *
   1599   * @param {DOMNode} aTarget
   1600   *   The customize target node to add drag and drop handlers for.
   1601   */
   1602  #addCustomizeTargetDragAndDropHandlers(aTarget) {
   1603    // Allow dropping on the padding of the arrow panel.
   1604    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
   1605      aTarget = this.$("customization-panelHolder");
   1606    }
   1607    aTarget.addEventListener("dragstart", this, true);
   1608    aTarget.addEventListener("dragover", this, true);
   1609    aTarget.addEventListener("dragleave", this, true);
   1610    aTarget.addEventListener("drop", this, true);
   1611    aTarget.addEventListener("dragend", this, true);
   1612  }
   1613 
   1614  /**
   1615   * Iterates all of the customizable item children of a customize target within
   1616   * a particular area, and attempts to wrap them in toolbarpaletteitems.
   1617   *
   1618   * @param {DOMNode} target
   1619   *   The customize target for some customizable area.
   1620   */
   1621  #wrapItemsInArea(target) {
   1622    for (let child of target.children) {
   1623      if (this.#isCustomizableItem(child)) {
   1624        this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
   1625      }
   1626    }
   1627  }
   1628 
   1629  /**
   1630   * Removes capturing drag and drop handlers for some customize target for some
   1631   * customizable area added via #addCustomizeTargetDragAndDropHandlers.
   1632   *
   1633   * @param {DOMNode} aTarget
   1634   *   The customize target node to remove drag and drop handlers for.
   1635   */
   1636  #removeCustomizeTargetDragAndDropHandlers(aTarget) {
   1637    // Remove handler from different target if it was added to
   1638    // allow dropping on the padding of the arrow panel.
   1639    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
   1640      aTarget = this.$("customization-panelHolder");
   1641    }
   1642    aTarget.removeEventListener("dragstart", this, true);
   1643    aTarget.removeEventListener("dragover", this, true);
   1644    aTarget.removeEventListener("dragleave", this, true);
   1645    aTarget.removeEventListener("drop", this, true);
   1646    aTarget.removeEventListener("dragend", this, true);
   1647  }
   1648 
   1649  /**
   1650   * Iterates all of the customizable item children of a customize target within
   1651   * a particular area that have been wrapped in toolbarpaletteitems, and
   1652   * attemps to unwrap them.
   1653   *
   1654   * @param {DOMNode} target
   1655   *   The customize target for some customizable area.
   1656   */
   1657  #unwrapItemsInArea(target) {
   1658    for (let toolbarItem of target.children) {
   1659      if (this.isWrappedToolbarItem(toolbarItem)) {
   1660        this.unwrapToolbarItem(toolbarItem);
   1661      }
   1662    }
   1663  }
   1664 
   1665  /**
   1666   * Iterates all areas and asynchronously unwraps their customize target
   1667   * children that had been previously wrapped in toolbarpaletteitems in the
   1668   * window that this CustomizeMode was constructed for. This also removes
   1669   * the drag and drop handlers for each area.
   1670   *
   1671   * @returns {Promise<undefined>}
   1672   *   Resolves when unwrapping has completed.
   1673   */
   1674  #unwrapAllAreaItems() {
   1675    return (async () => {
   1676      for (let target of this.areas) {
   1677        for (let toolbarItem of target.children) {
   1678          if (this.isWrappedToolbarItem(toolbarItem)) {
   1679            await this.#deferredUnwrapToolbarItem(toolbarItem);
   1680          }
   1681        }
   1682        this.#removeCustomizeTargetDragAndDropHandlers(target);
   1683      }
   1684      this.areas.clear();
   1685    })().catch(lazy.log.error);
   1686  }
   1687 
   1688  /**
   1689   * Resets the customization state of the browser across all windows to the
   1690   * default settings.
   1691   *
   1692   * @returns {Promise<undefined>}
   1693   *   Resolves once resetting the customization state has completed.
   1694   */
   1695  reset() {
   1696    this.resetting = true;
   1697    // Disable the reset button temporarily while resetting:
   1698    let btn = this.$("customization-reset-button");
   1699    btn.disabled = true;
   1700    return (async () => {
   1701      this.#depopulatePalette();
   1702      await this.#unwrapAllAreaItems();
   1703 
   1704      CustomizableUI.reset();
   1705 
   1706      await this.#wrapAllAreaItems();
   1707      this.#populatePalette();
   1708 
   1709      this.#updateResetButton();
   1710      this.#updateUndoResetButton();
   1711      this.#updateEmptyPaletteNotice();
   1712      this.#moveDownloadsButtonToNavBar = false;
   1713      this.resetting = false;
   1714      if (!this._wantToBeInCustomizeMode) {
   1715        this.exit();
   1716      }
   1717    })().catch(lazy.log.error);
   1718  }
   1719 
   1720  /**
   1721   * Reverts a reset operation back to the prior customization state.
   1722   *
   1723   * @see CustomizeMode.reset()
   1724   * @returns {Promise<undefined>}
   1725   */
   1726  undoReset() {
   1727    this.resetting = true;
   1728 
   1729    return (async () => {
   1730      this.#depopulatePalette();
   1731      await this.#unwrapAllAreaItems();
   1732 
   1733      CustomizableUI.undoReset();
   1734 
   1735      await this.#wrapAllAreaItems();
   1736      this.#populatePalette();
   1737 
   1738      this.#updateResetButton();
   1739      this.#updateUndoResetButton();
   1740      this.#updateEmptyPaletteNotice();
   1741      this.#moveDownloadsButtonToNavBar = false;
   1742      this.resetting = false;
   1743    })().catch(lazy.log.error);
   1744  }
   1745 
   1746  /**
   1747   * Handler for toolbarvisibilitychange events that fire within the window
   1748   * that this CustomizeMode was constructed for.
   1749   *
   1750   * @param {CustomEvent} aEvent
   1751   *   The toolbarvisibilitychange event that was fired.
   1752   */
   1753  #onToolbarVisibilityChange(aEvent) {
   1754    let toolbar = aEvent.target;
   1755    toolbar.toggleAttribute(
   1756      "customizing",
   1757      aEvent.detail.visible && toolbar.getAttribute("customizable") == "true"
   1758    );
   1759    this.#onUIChange();
   1760  }
   1761 
   1762  /**
   1763   * The callback called by CustomizableUI when a widget moves.
   1764   */
   1765  onWidgetMoved() {
   1766    this.#onUIChange();
   1767  }
   1768 
   1769  /**
   1770   * The callback called by CustomizableUI when a widget is added to an area.
   1771   */
   1772  onWidgetAdded() {
   1773    this.#onUIChange();
   1774  }
   1775 
   1776  /**
   1777   * The callback called by CustomizableUI when a widget is removed from an
   1778   * area.
   1779   */
   1780  onWidgetRemoved() {
   1781    this.#onUIChange();
   1782  }
   1783 
   1784  /**
   1785   * The callback called by CustomizableUI *before* a widget's DOM node is acted
   1786   * upon by CustomizableUI (to add, move or remove it).
   1787   *
   1788   * @param {Element} aNodeToChange
   1789   *   The DOM node being acted upon.
   1790   * @param {Element|null} aSecondaryNode
   1791   *   The DOM node (if any) before which a widget will be inserted.
   1792   * @param {Element} aContainer
   1793   *   The *actual* DOM container for the widget (could be an overflow panel in
   1794   *   case of an overflowable toolbar).
   1795   */
   1796  onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
   1797    if (aContainer.ownerGlobal != this.#window || this.resetting) {
   1798      return;
   1799    }
   1800    // If we get called for widgets that aren't in the window yet, they might not have
   1801    // a parentNode at all.
   1802    if (aNodeToChange.parentNode) {
   1803      this.unwrapToolbarItem(aNodeToChange.parentNode);
   1804    }
   1805    if (aSecondaryNode) {
   1806      this.unwrapToolbarItem(aSecondaryNode.parentNode);
   1807    }
   1808  }
   1809 
   1810  /**
   1811   * The callback called by CustomizableUI *after* a widget's DOM node is acted
   1812   * upon by CustomizableUI (to add, move or remove it).
   1813   *
   1814   * @param {Element} aNodeToChange
   1815   *   The DOM node that was acted upon.
   1816   * @param {Element|null} aSecondaryNode
   1817   *   The DOM node (if any) that the widget was inserted before.
   1818   * @param {Element} aContainer
   1819   *   The *actual* DOM container for the widget (could be an overflow panel in
   1820   *   case of an overflowable toolbar).
   1821   */
   1822  onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
   1823    if (aContainer.ownerGlobal != this.#window || this.resetting) {
   1824      return;
   1825    }
   1826    // If the node is still attached to the container, wrap it again:
   1827    if (aNodeToChange.parentNode) {
   1828      let place = CustomizableUI.getPlaceForItem(aNodeToChange);
   1829      this.wrapToolbarItem(aNodeToChange, place);
   1830      if (aSecondaryNode) {
   1831        this.wrapToolbarItem(aSecondaryNode, place);
   1832      }
   1833    } else {
   1834      // If not, it got removed.
   1835 
   1836      // If an API-based widget is removed while customizing, append it to the palette.
   1837      // The #applyDrop code itself will take care of positioning it correctly, if
   1838      // applicable. We need the code to be here so removing widgets using CustomizableUI's
   1839      // API also does the right thing (and adds it to the palette)
   1840      let widgetId = aNodeToChange.id;
   1841      let widget = CustomizableUI.getWidget(widgetId);
   1842      if (widget.provider == CustomizableUI.PROVIDER_API) {
   1843        let paletteItem = this.#makePaletteItem(widget);
   1844        this.visiblePalette.appendChild(paletteItem);
   1845      }
   1846    }
   1847  }
   1848 
   1849  /**
   1850   * The callback called by CustomizableUI when a widget is destroyed. Only
   1851   * fired for API-based widgets.
   1852   *
   1853   * @param {string} aWidgetId
   1854   *   The ID of the widget that was destroyed.
   1855   */
   1856  onWidgetDestroyed(aWidgetId) {
   1857    let wrapper = this.$("wrapper-" + aWidgetId);
   1858    if (wrapper) {
   1859      wrapper.remove();
   1860    }
   1861  }
   1862 
   1863  /**
   1864   * The callback called by CustomizableUI after a widget with id aWidgetId has
   1865   * been created, and has been added to either its default area or the area in
   1866   * which it was placed previously. If the widget has no default area and/or it
   1867   * has never been placed anywhere, aArea may be null. Only fired for API-based
   1868   * widgets.
   1869   *
   1870   * @param {string} aWidgetId
   1871   *   The ID of the widget that was just created.
   1872   * @param {string|null} aArea
   1873   *   The ID of the area that the widget was placed in, or null if it is
   1874   *   now in the customization palette.
   1875   */
   1876  onWidgetAfterCreation(aWidgetId, aArea) {
   1877    // If the node was added to an area, we would have gotten an onWidgetAdded notification,
   1878    // plus associated DOM change notifications, so only do stuff for the palette:
   1879    if (!aArea) {
   1880      let widgetNode = this.$(aWidgetId);
   1881      if (widgetNode) {
   1882        this.wrapToolbarItem(widgetNode, "palette");
   1883      } else {
   1884        let widget = CustomizableUI.getWidget(aWidgetId);
   1885        this.visiblePalette.appendChild(this.#makePaletteItem(widget));
   1886      }
   1887    }
   1888  }
   1889 
   1890  /**
   1891   * Called by CustomizableUI after an area node is first built when it is
   1892   * registered.
   1893   *
   1894   * @param {string} aArea
   1895   *   The ID for the area that was just registered.
   1896   * @param {DOMNode} aContainer
   1897   *   The DOM node for the customizable area.
   1898   */
   1899  onAreaNodeRegistered(aArea, aContainer) {
   1900    if (aContainer.ownerDocument == this.#document) {
   1901      this.#wrapItemsInArea(aContainer);
   1902      this.#addCustomizeTargetDragAndDropHandlers(aContainer);
   1903      this.areas.add(aContainer);
   1904    }
   1905  }
   1906 
   1907  /**
   1908   * Called by CustomizableUI after an area node is unregistered and no longer
   1909   * available in this window.
   1910   *
   1911   * @param {string} aArea
   1912   *   The ID for the area that was just registered.
   1913   * @param {DOMNode} aContainer
   1914   *   The DOM node for the customizable area.
   1915   * @param {string} aReason
   1916   *   One of the CustomizableUI.REASON_* constants to describe the reason
   1917   *   that the area was unregistered for.
   1918   */
   1919  onAreaNodeUnregistered(aArea, aContainer, aReason) {
   1920    if (
   1921      aContainer.ownerDocument == this.#document &&
   1922      aReason == CustomizableUI.REASON_AREA_UNREGISTERED
   1923    ) {
   1924      this.#unwrapItemsInArea(aContainer);
   1925      this.#removeCustomizeTargetDragAndDropHandlers(aContainer);
   1926      this.areas.delete(aContainer);
   1927    }
   1928  }
   1929 
   1930  /**
   1931   * Opens about:addons in a new tab, showing the themes list.
   1932   */
   1933  #openAddonsManagerThemes() {
   1934    this.#window.BrowserAddonUI.openAddonsMgr("addons://list/theme");
   1935  }
   1936 
   1937  /**
   1938   * Temporarily updates the density of the browser UI to suit the passed in
   1939   * mode. This is used to preview the density of the browser while the user
   1940   * hovers the various density options, and is reset when the user stops
   1941   * hovering the options.
   1942   *
   1943   * @param {number|null} mode
   1944   *   One of the density mode constants from gUIDensity - for example,
   1945   *   gUIDensity.MODE_TOUCH.
   1946   */
   1947  #previewUIDensity(mode) {
   1948    this.#window.gUIDensity.update(mode);
   1949    this.#updateOverflowPanelArrowOffset();
   1950  }
   1951 
   1952  /**
   1953   * Resets the current UI density to the currently configured density. This
   1954   * is used after temporarily previewing a density.
   1955   */
   1956  #resetUIDensity() {
   1957    this.#window.gUIDensity.update();
   1958    this.#updateOverflowPanelArrowOffset();
   1959  }
   1960 
   1961  /**
   1962   * Sets a UI density mode as the configured density.
   1963   *
   1964   * @param {number|null} mode
   1965   *   One of the density mode constants from gUIDensity - for example,
   1966   *   gUIDensity.MODE_TOUCH.
   1967   */
   1968  setUIDensity(mode) {
   1969    let win = this.#window;
   1970    let gUIDensity = win.gUIDensity;
   1971    let currentDensity = gUIDensity.getCurrentDensity();
   1972    let panel = win.document.getElementById("customization-uidensity-menu");
   1973 
   1974    Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);
   1975 
   1976    // If the user is choosing a different UI density mode while
   1977    // the mode is overriden to Touch, remove the override.
   1978    if (currentDensity.overridden) {
   1979      Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
   1980    }
   1981 
   1982    this.#onUIChange();
   1983    panel.hidePopup();
   1984    this.#updateOverflowPanelArrowOffset();
   1985  }
   1986 
   1987  /**
   1988   * Updates the state of the UI density menupopup to correctly reflect the
   1989   * current configured density and to list the available alternative densities.
   1990   */
   1991  #onUIDensityMenuShowing() {
   1992    let win = this.#window;
   1993    let doc = win.document;
   1994    let gUIDensity = win.gUIDensity;
   1995    let currentDensity = gUIDensity.getCurrentDensity();
   1996 
   1997    let normalItem = doc.getElementById(
   1998      "customization-uidensity-menuitem-normal"
   1999    );
   2000    normalItem.mode = gUIDensity.MODE_NORMAL;
   2001 
   2002    let items = [normalItem];
   2003 
   2004    let compactItem = doc.getElementById(
   2005      "customization-uidensity-menuitem-compact"
   2006    );
   2007    compactItem.mode = gUIDensity.MODE_COMPACT;
   2008 
   2009    if (Services.prefs.getBoolPref(kCompactModeShowPref)) {
   2010      compactItem.hidden = false;
   2011      items.push(compactItem);
   2012    } else {
   2013      compactItem.hidden = true;
   2014    }
   2015 
   2016    let touchItem = doc.getElementById(
   2017      "customization-uidensity-menuitem-touch"
   2018    );
   2019    // Touch mode can not be enabled in OSX right now.
   2020    if (touchItem) {
   2021      touchItem.mode = gUIDensity.MODE_TOUCH;
   2022      items.push(touchItem);
   2023    }
   2024 
   2025    // Mark the active mode menuitem.
   2026    for (let item of items) {
   2027      if (item.mode == currentDensity.mode) {
   2028        item.setAttribute("aria-checked", "true");
   2029        item.setAttribute("active", "true");
   2030      } else {
   2031        item.removeAttribute("aria-checked");
   2032        item.removeAttribute("active");
   2033      }
   2034    }
   2035 
   2036    // Add menu items for automatically switching to Touch mode in Windows Tablet Mode.
   2037    if (AppConstants.platform == "win") {
   2038      let spacer = doc.getElementById("customization-uidensity-touch-spacer");
   2039      let checkbox = doc.getElementById(
   2040        "customization-uidensity-autotouchmode-checkbox"
   2041      );
   2042      spacer.removeAttribute("hidden");
   2043      checkbox.removeAttribute("hidden");
   2044 
   2045      // Show a hint that the UI density was overridden automatically.
   2046      if (currentDensity.overridden) {
   2047        let sb = Services.strings.createBundle(
   2048          "chrome://browser/locale/uiDensity.properties"
   2049        );
   2050        touchItem.setAttribute(
   2051          "acceltext",
   2052          sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
   2053        );
   2054      } else {
   2055        touchItem.removeAttribute("acceltext");
   2056      }
   2057 
   2058      let autoTouchMode = Services.prefs.getBoolPref(
   2059        win.gUIDensity.autoTouchModePref
   2060      );
   2061      if (autoTouchMode) {
   2062        checkbox.setAttribute("checked", "true");
   2063      } else {
   2064        checkbox.removeAttribute("checked");
   2065      }
   2066    }
   2067  }
   2068 
   2069  /**
   2070   * Sets "automatic" touch mode to enabled or disabled. Automatic touch mode
   2071   * means that touch density is used automatically if the device has switched
   2072   * into a tablet mode.
   2073   *
   2074   * @param {boolean} checked
   2075   *   True if automatic touch mode should be enabled.
   2076   */
   2077  #updateAutoTouchMode(checked) {
   2078    Services.prefs.setBoolPref("browser.touchmode.auto", checked);
   2079    // Re-render the menu items since the active mode might have
   2080    // change because of this.
   2081    this.#onUIDensityMenuShowing();
   2082    this.#onUIChange();
   2083  }
   2084 
   2085  /**
   2086   * Called anytime the UI configuration has changed in such a way that we need
   2087   * to update the state and appearance of customize mode.
   2088   */
   2089  #onUIChange() {
   2090    if (!this.resetting) {
   2091      this.#updateResetButton();
   2092      this.#updateUndoResetButton();
   2093      this.#updateEmptyPaletteNotice();
   2094    }
   2095    CustomizableUI.dispatchToolboxEvent("customizationchange");
   2096  }
   2097 
   2098  /**
   2099   * Handles updating the state of customize mode if the palette has been
   2100   * emptied such that only the special "spring" element remains (as this
   2101   * cannot be removed from the palette).
   2102   */
   2103  #updateEmptyPaletteNotice() {
   2104    let paletteItems =
   2105      this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
   2106    let whimsyButton = this.$("whimsy-button");
   2107 
   2108    if (
   2109      paletteItems.length == 1 &&
   2110      paletteItems[0].id.includes("wrapper-customizableui-special-spring")
   2111    ) {
   2112      whimsyButton.hidden = false;
   2113    } else {
   2114      this.#togglePong(false);
   2115      whimsyButton.hidden = true;
   2116    }
   2117  }
   2118 
   2119  /**
   2120   * Updates the enabled / disabled state of the Restore Defaults button based
   2121   * on whether or not we're already in the default state.
   2122   */
   2123  #updateResetButton() {
   2124    let btn = this.$("customization-reset-button");
   2125    btn.disabled = CustomizableUI.inDefaultState;
   2126  }
   2127 
   2128  /**
   2129   * Updates the hidden / visible state of the "undo reset" button based on
   2130   * whether or not we've just performed a reset that can be undone.
   2131   */
   2132  #updateUndoResetButton() {
   2133    let undoResetButton = this.$("customization-undo-reset-button");
   2134    undoResetButton.hidden = !CustomizableUI.canUndoReset;
   2135  }
   2136 
   2137  /**
   2138   * On macOS, if a touch bar is available on the device, updates the
   2139   * hidden / visible state of the Customize Touch Bar button and spacer.
   2140   */
   2141  #updateTouchBarButton() {
   2142    if (AppConstants.platform != "macosx") {
   2143      return;
   2144    }
   2145    let touchBarButton = this.$("customization-touchbar-button");
   2146    let touchBarSpacer = this.$("customization-touchbar-spacer");
   2147 
   2148    let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized();
   2149    touchBarButton.hidden = !isTouchBarInitialized;
   2150    touchBarSpacer.hidden = !isTouchBarInitialized;
   2151  }
   2152 
   2153  /**
   2154   * Updates the hidden / visible state of the UI density button.
   2155   */
   2156  #updateDensityMenu() {
   2157    // If we're entering Customize Mode, and we're using compact mode,
   2158    // then show the button after that.
   2159    let gUIDensity = this.#window.gUIDensity;
   2160    if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) {
   2161      Services.prefs.setBoolPref(kCompactModeShowPref, true);
   2162    }
   2163 
   2164    let button = this.#document.getElementById(
   2165      "customization-uidensity-button"
   2166    );
   2167    button.hidden =
   2168      !Services.prefs.getBoolPref(kCompactModeShowPref) &&
   2169      !button.querySelector("#customization-uidensity-menuitem-touch");
   2170  }
   2171 
   2172  /**
   2173   * Generic event handler used throughout most of the Customize Mode UI. This
   2174   * is mainly used to dispatch events to more specific handlers based on the
   2175   * event type.
   2176   *
   2177   * @param {Event} aEvent
   2178   *   The event being handled.
   2179   */
   2180  handleEvent(aEvent) {
   2181    switch (aEvent.type) {
   2182      case "toolbarvisibilitychange":
   2183        this.#onToolbarVisibilityChange(aEvent);
   2184        break;
   2185      case "dragstart":
   2186        this.#onDragStart(aEvent);
   2187        break;
   2188      case "dragover":
   2189        this.#onDragOver(aEvent);
   2190        break;
   2191      case "drop":
   2192        this.#onDragDrop(aEvent);
   2193        break;
   2194      case "dragleave":
   2195        this.#onDragLeave(aEvent);
   2196        break;
   2197      case "dragend":
   2198        this.#onDragEnd(aEvent);
   2199        break;
   2200      case "mousedown":
   2201        this.#onMouseDown(aEvent);
   2202        break;
   2203      case "mouseup":
   2204        this.#onMouseUp(aEvent);
   2205        break;
   2206      case "keypress":
   2207        if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
   2208          this.exit();
   2209        }
   2210        break;
   2211      case "unload":
   2212        this.#uninit();
   2213        break;
   2214    }
   2215  }
   2216 
   2217  /**
   2218   * Sets up the dragover/drop handlers on the visible palette. We handle
   2219   * dragover/drop on the outer palette separately to avoid overlap with other
   2220   * drag/drop handlers.
   2221   */
   2222  #setupPaletteDragging() {
   2223    this.#addCustomizeTargetDragAndDropHandlers(this.visiblePalette);
   2224 
   2225    this.paletteDragHandler = aEvent => {
   2226      let originalTarget = aEvent.originalTarget;
   2227      if (
   2228        this.#isUnwantedDragDrop(aEvent) ||
   2229        this.visiblePalette.contains(originalTarget) ||
   2230        this.$("customization-panelHolder").contains(originalTarget)
   2231      ) {
   2232        return;
   2233      }
   2234      // We have a dragover/drop on the palette.
   2235      if (aEvent.type == "dragover") {
   2236        this.#onDragOver(aEvent, this.visiblePalette);
   2237      } else {
   2238        this.#onDragDrop(aEvent, this.visiblePalette);
   2239      }
   2240    };
   2241    let contentContainer = this.$("customization-content-container");
   2242    contentContainer.addEventListener(
   2243      "dragover",
   2244      this.paletteDragHandler,
   2245      true
   2246    );
   2247    contentContainer.addEventListener("drop", this.paletteDragHandler, true);
   2248  }
   2249 
   2250  /**
   2251   * Tears down the dragover/drop handlers on the visible palette added by
   2252   * #setupPaletteDragging.
   2253   */
   2254  #teardownPaletteDragging() {
   2255    lazy.DragPositionManager.stop();
   2256    this.#removeCustomizeTargetDragAndDropHandlers(this.visiblePalette);
   2257 
   2258    let contentContainer = this.$("customization-content-container");
   2259    contentContainer.removeEventListener(
   2260      "dragover",
   2261      this.paletteDragHandler,
   2262      true
   2263    );
   2264    contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
   2265    delete this.paletteDragHandler;
   2266  }
   2267 
   2268  /**
   2269   * Implements nsIObserver. This is mainly to observe for preference changes.
   2270   *
   2271   * @param {nsISupports} aSubject
   2272   *   The nsISupports subject for the notification topic that is being
   2273   *   observed.
   2274   * @param {string} aTopic
   2275   *   The notification topic that is being observed.
   2276   */
   2277  observe(aSubject, aTopic) {
   2278    switch (aTopic) {
   2279      case "nsPref:changed":
   2280        this.#updateResetButton();
   2281        this.#updateUndoResetButton();
   2282        if (this.#canDrawInTitlebar()) {
   2283          this.#updateTitlebarCheckbox();
   2284        }
   2285        break;
   2286    }
   2287  }
   2288 
   2289  /**
   2290   * Returns true if the current platform and configuration allows us to draw in
   2291   * the window titlebar.
   2292   *
   2293   * @returns {boolean}
   2294   */
   2295  #canDrawInTitlebar() {
   2296    return this.#window.CustomTitlebar.systemSupported;
   2297  }
   2298 
   2299  /**
   2300   * De-lazifies the customization panel and the menupopup / panel template
   2301   * holding various DOM nodes for customize mode. These things are lazily
   2302   * added to the DOM to avoid polluting the browser window DOM with things
   2303   * that only Customize Mode cares about.
   2304   */
   2305  #ensureCustomizationPanels() {
   2306    let template = this.$("customizationPanel");
   2307    template.replaceWith(template.content);
   2308 
   2309    let wrapper = this.$("customModeWrapper");
   2310    wrapper.replaceWith(wrapper.content);
   2311  }
   2312 
   2313  /**
   2314   * Adds event listeners for all of the interactive elements in the window that
   2315   * this Customize Mode instance was constructed with.
   2316   */
   2317  #attachEventListeners() {
   2318    let container = this.$("customization-container");
   2319 
   2320    container.addEventListener("command", event => {
   2321      switch (event.target.id) {
   2322        case "customization-titlebar-visibility-checkbox":
   2323          // NB: because command fires after click, by the time we've fired, the checkbox binding
   2324          //     will already have switched the button's state, so this is correct:
   2325          this.#toggleTitlebar(event.target.checked);
   2326          break;
   2327        case "customization-uidensity-menuitem-compact":
   2328        case "customization-uidensity-menuitem-normal":
   2329        case "customization-uidensity-menuitem-touch":
   2330          this.setUIDensity(event.target.mode);
   2331          break;
   2332        case "customization-uidensity-autotouchmode-checkbox":
   2333          this.#updateAutoTouchMode(event.target.checked);
   2334          break;
   2335        case "whimsy-button":
   2336          this.#togglePong(event.target.checked);
   2337          break;
   2338        case "customization-touchbar-button":
   2339          this.#customizeTouchBar();
   2340          break;
   2341        case "customization-undo-reset-button":
   2342          this.undoReset();
   2343          break;
   2344        case "customization-reset-button":
   2345          this.reset();
   2346          break;
   2347        case "customization-done-button":
   2348          this.exit();
   2349          break;
   2350      }
   2351    });
   2352 
   2353    container.addEventListener("popupshowing", event => {
   2354      switch (event.target.id) {
   2355        case "customization-toolbar-menu":
   2356          this.#window.ToolbarContextMenu.onViewToolbarsPopupShowing(event);
   2357          break;
   2358        case "customization-uidensity-menu":
   2359          this.#onUIDensityMenuShowing();
   2360          break;
   2361      }
   2362    });
   2363 
   2364    let updateDensity = event => {
   2365      switch (event.target.id) {
   2366        case "customization-uidensity-menuitem-compact":
   2367        case "customization-uidensity-menuitem-normal":
   2368        case "customization-uidensity-menuitem-touch":
   2369          this.#previewUIDensity(event.target.mode);
   2370      }
   2371    };
   2372    let densityMenu = this.#document.getElementById(
   2373      "customization-uidensity-menu"
   2374    );
   2375    densityMenu.addEventListener("focus", updateDensity);
   2376    densityMenu.addEventListener("mouseover", updateDensity);
   2377 
   2378    let resetDensity = event => {
   2379      switch (event.target.id) {
   2380        case "customization-uidensity-menuitem-compact":
   2381        case "customization-uidensity-menuitem-normal":
   2382        case "customization-uidensity-menuitem-touch":
   2383          this.#resetUIDensity();
   2384      }
   2385    };
   2386    densityMenu.addEventListener("blur", resetDensity);
   2387    densityMenu.addEventListener("mouseout", resetDensity);
   2388 
   2389    this.$("customization-lwtheme-link").addEventListener("click", () => {
   2390      this.#openAddonsManagerThemes();
   2391    });
   2392 
   2393    this.$(kPaletteItemContextMenu).addEventListener("popupshowing", event => {
   2394      this.#onPaletteContextMenuShowing(event);
   2395    });
   2396 
   2397    this.$(kPaletteItemContextMenu).addEventListener("command", event => {
   2398      switch (event.target.id) {
   2399        case "customizationPaletteItemContextMenuAddToToolbar":
   2400          this.addToToolbar(
   2401            event.target.parentNode.triggerNode,
   2402            "palette-context"
   2403          );
   2404          break;
   2405        case "customizationPaletteItemContextMenuAddToPanel":
   2406          this.addToPanel(
   2407            event.target.parentNode.triggerNode,
   2408            "palette-context"
   2409          );
   2410          break;
   2411      }
   2412    });
   2413 
   2414    let autohidePanel = this.$(kDownloadAutohidePanelId);
   2415    autohidePanel.addEventListener("popupshown", event => {
   2416      this._downloadPanelAutoHideTimeout = this.#window.setTimeout(
   2417        () => event.target.hidePopup(),
   2418        4000
   2419      );
   2420    });
   2421    autohidePanel.addEventListener("mouseover", () => {
   2422      this.#window.clearTimeout(this._downloadPanelAutoHideTimeout);
   2423    });
   2424    autohidePanel.addEventListener("mouseout", event => {
   2425      this._downloadPanelAutoHideTimeout = this.#window.setTimeout(
   2426        () => event.target.hidePopup(),
   2427        2000
   2428      );
   2429    });
   2430    autohidePanel.addEventListener("popuphidden", () => {
   2431      this.#window.clearTimeout(this._downloadPanelAutoHideTimeout);
   2432    });
   2433 
   2434    this.$(kDownloadAutohideCheckboxId).addEventListener("command", event => {
   2435      this.#onDownloadsAutoHideChange(event);
   2436    });
   2437  }
   2438 
   2439  /**
   2440   * Updates the checked / unchecked state of the Titlebar checkbox, to
   2441   * reflect whether or not we're currently configured to show the native
   2442   * titlebar or not.
   2443   */
   2444  #updateTitlebarCheckbox() {
   2445    let drawInTitlebar = Services.appinfo.drawInTitlebar;
   2446    let checkbox = this.$("customization-titlebar-visibility-checkbox");
   2447    // Drawing in the titlebar means 'hiding' the titlebar.
   2448    // We use the attribute rather than a property because if we're not in
   2449    // customize mode the button is hidden and properties don't work.
   2450    if (drawInTitlebar) {
   2451      checkbox.removeAttribute("checked");
   2452    } else {
   2453      checkbox.setAttribute("checked", "true");
   2454    }
   2455  }
   2456 
   2457  /**
   2458   * Configures whether or not we should show the native titlebar.
   2459   *
   2460   * @param {boolean} aShouldShowTitlebar
   2461   *   True if we should show the native titlebar. False to draw the browser
   2462   *   UI into the titlebar instead.
   2463   */
   2464  #toggleTitlebar(aShouldShowTitlebar) {
   2465    // Drawing in the titlebar means not showing the titlebar, hence the negation:
   2466    Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
   2467  }
   2468 
   2469  /**
   2470   * A convenient shortcut to calling getBoundsWithoutFlushing on this windows'
   2471   * nsIDOMWindowUtils.
   2472   *
   2473   * @param {DOMNode} element
   2474   *   An element for which to try to get the bounding client rect, but without
   2475   *   flushing styles or layout.
   2476   * @returns {DOMRect}
   2477   */
   2478  #getBoundsWithoutFlushing(element) {
   2479    return this.#window.windowUtils.getBoundsWithoutFlushing(element);
   2480  }
   2481 
   2482  /**
   2483   * Handles the dragstart event on any customizable item in one of the
   2484   * customizable areas.
   2485   *
   2486   * @param {DragEvent} aEvent
   2487   *   The dragstart event being handled.
   2488   */
   2489  #onDragStart(aEvent) {
   2490    __dumpDragData(aEvent);
   2491    let item = aEvent.target;
   2492    while (item && item.localName != "toolbarpaletteitem") {
   2493      if (
   2494        item.localName == "toolbar" ||
   2495        item.id == kPaletteId ||
   2496        item.id == "customization-panelHolder"
   2497      ) {
   2498        return;
   2499      }
   2500      item = item.parentNode;
   2501    }
   2502 
   2503    let draggedItem = item.firstElementChild;
   2504    let placeForItem = CustomizableUI.getPlaceForItem(item);
   2505 
   2506    let dt = aEvent.dataTransfer;
   2507    let documentId = aEvent.target.ownerDocument.documentElement.id;
   2508 
   2509    dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
   2510    dt.effectAllowed = "move";
   2511 
   2512    let itemRect = this.#getBoundsWithoutFlushing(draggedItem);
   2513    let itemCenter = {
   2514      x: itemRect.left + itemRect.width / 2,
   2515      y: itemRect.top + itemRect.height / 2,
   2516    };
   2517    this._dragOffset = {
   2518      x: aEvent.clientX - itemCenter.x,
   2519      y: aEvent.clientY - itemCenter.y,
   2520    };
   2521 
   2522    let toolbarParent = draggedItem.closest("toolbar");
   2523    if (toolbarParent) {
   2524      let toolbarRect = this.#getBoundsWithoutFlushing(toolbarParent);
   2525      toolbarParent.style.minHeight = toolbarRect.height + "px";
   2526    }
   2527 
   2528    gDraggingInToolbars = new Set();
   2529 
   2530    // Hack needed so that the dragimage will still show the
   2531    // item as it appeared before it was hidden.
   2532    this._initializeDragAfterMove = () => {
   2533      // For automated tests, we sometimes start exiting customization mode
   2534      // before this fires, which leaves us with placeholders inserted after
   2535      // we've exited. So we need to check that we are indeed customizing.
   2536      if (this.#customizing && !this.#transitioning) {
   2537        item.hidden = true;
   2538        lazy.DragPositionManager.start(this.#window);
   2539        let canUsePrevSibling =
   2540          placeForItem == "toolbar" || placeForItem == "panel";
   2541        if (item.nextElementSibling) {
   2542          this.#setDragActive(
   2543            item.nextElementSibling,
   2544            "before",
   2545            draggedItem.id,
   2546            placeForItem
   2547          );
   2548          this.#dragOverItem = item.nextElementSibling;
   2549        } else if (canUsePrevSibling && item.previousElementSibling) {
   2550          this.#setDragActive(
   2551            item.previousElementSibling,
   2552            "after",
   2553            draggedItem.id,
   2554            placeForItem
   2555          );
   2556          this.#dragOverItem = item.previousElementSibling;
   2557        }
   2558        let currentArea = this.#getCustomizableParent(item);
   2559        currentArea.setAttribute("draggingover", "true");
   2560      }
   2561      this._initializeDragAfterMove = null;
   2562      this.#window.clearTimeout(this._dragInitializeTimeout);
   2563    };
   2564    this._dragInitializeTimeout = this.#window.setTimeout(
   2565      this._initializeDragAfterMove,
   2566      0
   2567    );
   2568  }
   2569 
   2570  /**
   2571   * Handles the dragover event for any customizable area.
   2572   *
   2573   * @param {DragEvent} aEvent
   2574   *   The dragover event being handled.
   2575   * @param {DOMNode} [aOverrideTarget=undefined]
   2576   *   Optional argument that allows callers to override the dragover target to
   2577   *   be something other than the dragover event current target.
   2578   */
   2579  #onDragOver(aEvent, aOverrideTarget) {
   2580    if (this.#isUnwantedDragDrop(aEvent)) {
   2581      return;
   2582    }
   2583    if (this._initializeDragAfterMove) {
   2584      this._initializeDragAfterMove();
   2585    }
   2586 
   2587    __dumpDragData(aEvent);
   2588 
   2589    let document = aEvent.target.ownerDocument;
   2590    let documentId = document.documentElement.id;
   2591    if (!aEvent.dataTransfer.mozTypesAt(0).length) {
   2592      return;
   2593    }
   2594 
   2595    let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
   2596      kDragDataTypePrefix + documentId,
   2597      0
   2598    );
   2599    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
   2600    let targetArea = this.#getCustomizableParent(
   2601      aOverrideTarget || aEvent.currentTarget
   2602    );
   2603    let originArea = this.#getCustomizableParent(draggedWrapper);
   2604 
   2605    // Do nothing if the target or origin are not customizable.
   2606    if (!targetArea || !originArea) {
   2607      return;
   2608    }
   2609 
   2610    // Do nothing if the widget is not allowed to be removed.
   2611    if (
   2612      targetArea.id == kPaletteId &&
   2613      !CustomizableUI.isWidgetRemovable(draggedItemId)
   2614    ) {
   2615      return;
   2616    }
   2617 
   2618    // Do nothing if the widget is not allowed to move to the target area.
   2619    if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
   2620      return;
   2621    }
   2622 
   2623    let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
   2624    let targetNode = this.#getDragOverNode(
   2625      aEvent,
   2626      targetArea,
   2627      targetAreaType,
   2628      draggedItemId
   2629    );
   2630 
   2631    // We need to determine the place that the widget is being dropped in
   2632    // the target.
   2633    let dragOverItem, dragValue;
   2634    if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) {
   2635      // We'll assume if the user is dragging directly over the target, that
   2636      // they're attempting to append a child to that target.
   2637      dragOverItem =
   2638        (targetAreaType == "toolbar"
   2639          ? this.#findVisiblePreviousSiblingNode(targetNode.lastElementChild)
   2640          : targetNode.lastElementChild) || targetNode;
   2641      dragValue = "after";
   2642    } else {
   2643      let targetParent = targetNode.parentNode;
   2644      let position = Array.prototype.indexOf.call(
   2645        targetParent.children,
   2646        targetNode
   2647      );
   2648      if (position == -1) {
   2649        dragOverItem =
   2650          targetAreaType == "toolbar"
   2651            ? this.#findVisiblePreviousSiblingNode(targetNode.lastElementChild)
   2652            : targetNode.lastElementChild;
   2653        dragValue = "after";
   2654      } else {
   2655        dragOverItem = targetParent.children[position];
   2656        if (targetAreaType == "toolbar") {
   2657          // Check if the aDraggedItem is hovered past the first half of dragOverItem
   2658          let itemRect = this.#getBoundsWithoutFlushing(dragOverItem);
   2659          let dropTargetCenter = itemRect.left + itemRect.width / 2;
   2660          let existingDir = dragOverItem.getAttribute("dragover");
   2661          let dirFactor = this.#window.RTL_UI ? -1 : 1;
   2662          if (existingDir == "before") {
   2663            dropTargetCenter +=
   2664              ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) *
   2665              dirFactor;
   2666          } else {
   2667            dropTargetCenter -=
   2668              ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) *
   2669              dirFactor;
   2670          }
   2671          let before = this.#window.RTL_UI
   2672            ? aEvent.clientX > dropTargetCenter
   2673            : aEvent.clientX < dropTargetCenter;
   2674          dragValue = before ? "before" : "after";
   2675        } else if (targetAreaType == "panel") {
   2676          let itemRect = this.#getBoundsWithoutFlushing(dragOverItem);
   2677          let dropTargetCenter = itemRect.top + itemRect.height / 2;
   2678          let existingDir = dragOverItem.getAttribute("dragover");
   2679          if (existingDir == "before") {
   2680            dropTargetCenter +=
   2681              (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
   2682          } else {
   2683            dropTargetCenter -=
   2684              (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
   2685          }
   2686          dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
   2687        } else {
   2688          dragValue = "before";
   2689        }
   2690      }
   2691    }
   2692 
   2693    if (this.#dragOverItem && dragOverItem != this.#dragOverItem) {
   2694      this.#cancelDragActive(this.#dragOverItem, dragOverItem);
   2695    }
   2696 
   2697    if (
   2698      dragOverItem != this.#dragOverItem ||
   2699      dragValue != dragOverItem.getAttribute("dragover")
   2700    ) {
   2701      if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
   2702        this.#setDragActive(
   2703          dragOverItem,
   2704          dragValue,
   2705          draggedItemId,
   2706          targetAreaType
   2707        );
   2708      }
   2709      this.#dragOverItem = dragOverItem;
   2710      targetArea.setAttribute("draggingover", "true");
   2711    }
   2712 
   2713    aEvent.preventDefault();
   2714    aEvent.stopPropagation();
   2715  }
   2716 
   2717  /**
   2718   * Handles the drop event on any customizable area.
   2719   *
   2720   * @param {DragEvent} aEvent
   2721   *   The drop event being handled.
   2722   * @param {DOMNode} [aOverrideTarget=undefined]
   2723   *   Optional argument that allows callers to override the drop target to
   2724   *   be something other than the drop event current target.
   2725   */
   2726  #onDragDrop(aEvent, aOverrideTarget) {
   2727    if (this.#isUnwantedDragDrop(aEvent)) {
   2728      return;
   2729    }
   2730 
   2731    __dumpDragData(aEvent);
   2732    this._initializeDragAfterMove = null;
   2733    this.#window.clearTimeout(this._dragInitializeTimeout);
   2734 
   2735    let targetArea = this.#getCustomizableParent(
   2736      aOverrideTarget || aEvent.currentTarget
   2737    );
   2738    let document = aEvent.target.ownerDocument;
   2739    let documentId = document.documentElement.id;
   2740    let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
   2741      kDragDataTypePrefix + documentId,
   2742      0
   2743    );
   2744    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
   2745    let originArea = this.#getCustomizableParent(draggedWrapper);
   2746    if (this.#dragSizeMap) {
   2747      this.#dragSizeMap = new WeakMap();
   2748    }
   2749    // Do nothing if the target area or origin area are not customizable.
   2750    if (!targetArea || !originArea) {
   2751      return;
   2752    }
   2753    let targetNode = this.#dragOverItem;
   2754    let dropDir = targetNode.getAttribute("dragover");
   2755    // Need to insert *after* this node if we promised the user that:
   2756    if (targetNode != targetArea && dropDir == "after") {
   2757      if (targetNode.nextElementSibling) {
   2758        targetNode = targetNode.nextElementSibling;
   2759      } else {
   2760        targetNode = targetArea;
   2761      }
   2762    }
   2763    if (targetNode.tagName == "toolbarpaletteitem") {
   2764      targetNode = targetNode.firstElementChild;
   2765    }
   2766 
   2767    this.#cancelDragActive(this.#dragOverItem, null, true);
   2768 
   2769    try {
   2770      this.#applyDrop(
   2771        aEvent,
   2772        targetArea,
   2773        originArea,
   2774        draggedItemId,
   2775        targetNode
   2776      );
   2777    } catch (ex) {
   2778      lazy.log.error(ex, ex.stack);
   2779    }
   2780 
   2781    // If the user explicitly moves this item, turn off autohide.
   2782    if (draggedItemId == "downloads-button") {
   2783      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
   2784      this.#showDownloadsAutoHidePanel();
   2785    }
   2786  }
   2787 
   2788  /**
   2789   * A helper method for #onDragDrop that applies the changes to the browser
   2790   * UI from the drop event on either a customizable item, or an area.
   2791   *
   2792   * @param {DragEvent} aEvent
   2793   *   The drop event being handled.
   2794   * @param {DOMNode} aTargetArea
   2795   *   The target area node being dropped on.
   2796   * @param {DOMNode} aOriginArea
   2797   *   The origin area node that the dropped item originally came from.
   2798   * @param {string} aDroppedItemId
   2799   *   The ID value of the customizable item being dropped.
   2800   * @param {DOMNode} aTargetNode
   2801   *   The customizable item (or area) node being dropped on.
   2802   */
   2803  #applyDrop(aEvent, aTargetArea, aOriginArea, aDroppedItemId, aTargetNode) {
   2804    let document = aEvent.target.ownerDocument;
   2805    let draggedItem = document.getElementById(aDroppedItemId);
   2806    draggedItem.hidden = false;
   2807    draggedItem.removeAttribute("mousedown");
   2808 
   2809    let toolbarParent = draggedItem.closest("toolbar");
   2810    if (toolbarParent) {
   2811      toolbarParent.style.removeProperty("min-height");
   2812    }
   2813 
   2814    // Do nothing if the target was dropped onto itself (ie, no change in area
   2815    // or position).
   2816    if (draggedItem == aTargetNode) {
   2817      return;
   2818    }
   2819 
   2820    if (!CustomizableUI.canWidgetMoveToArea(aDroppedItemId, aTargetArea.id)) {
   2821      return;
   2822    }
   2823 
   2824    // Is the target area the customization palette?
   2825    if (aTargetArea.id == kPaletteId) {
   2826      // Did we drag from outside the palette?
   2827      if (aOriginArea.id !== kPaletteId) {
   2828        if (!CustomizableUI.isWidgetRemovable(aDroppedItemId)) {
   2829          return;
   2830        }
   2831 
   2832        CustomizableUI.removeWidgetFromArea(aDroppedItemId, "drag");
   2833        lazy.BrowserUsageTelemetry.recordWidgetChange(
   2834          aDroppedItemId,
   2835          null,
   2836          "drag"
   2837        );
   2838        // Special widgets are removed outright, we can return here:
   2839        if (CustomizableUI.isSpecialWidget(aDroppedItemId)) {
   2840          return;
   2841        }
   2842      }
   2843      draggedItem = draggedItem.parentNode;
   2844 
   2845      // If the target node is the palette itself, just append
   2846      if (aTargetNode == this.visiblePalette) {
   2847        this.visiblePalette.appendChild(draggedItem);
   2848      } else {
   2849        // The items in the palette are wrapped, so we need the target node's parent here:
   2850        this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
   2851      }
   2852      this.#onDragEnd(aEvent);
   2853      return;
   2854    }
   2855 
   2856    // Skipintoolbarset items won't really be moved:
   2857    let areaCustomizationTarget =
   2858      CustomizableUI.getCustomizationTarget(aTargetArea);
   2859    if (draggedItem.getAttribute("skipintoolbarset") == "true") {
   2860      // These items should never leave their area:
   2861      if (aTargetArea != aOriginArea) {
   2862        return;
   2863      }
   2864      let place = draggedItem.parentNode.getAttribute("place");
   2865      this.unwrapToolbarItem(draggedItem.parentNode);
   2866      if (aTargetNode == areaCustomizationTarget) {
   2867        areaCustomizationTarget.appendChild(draggedItem);
   2868      } else {
   2869        this.unwrapToolbarItem(aTargetNode.parentNode);
   2870        areaCustomizationTarget.insertBefore(draggedItem, aTargetNode);
   2871        this.wrapToolbarItem(aTargetNode, place);
   2872      }
   2873      this.wrapToolbarItem(draggedItem, place);
   2874      return;
   2875    }
   2876 
   2877    // Force creating a new spacer/spring/separator if dragging from the palette
   2878    if (
   2879      CustomizableUI.isSpecialWidget(aDroppedItemId) &&
   2880      aOriginArea.id == kPaletteId
   2881    ) {
   2882      aDroppedItemId = aDroppedItemId.match(
   2883        /^customizableui-special-(spring|spacer|separator)/
   2884      )[1];
   2885    }
   2886 
   2887    // Is the target the customization area itself? If so, we just add the
   2888    // widget to the end of the area.
   2889    if (aTargetNode == areaCustomizationTarget) {
   2890      CustomizableUI.addWidgetToArea(aDroppedItemId, aTargetArea.id);
   2891      lazy.BrowserUsageTelemetry.recordWidgetChange(
   2892        aDroppedItemId,
   2893        aTargetArea.id,
   2894        "drag"
   2895      );
   2896      this.#onDragEnd(aEvent);
   2897      return;
   2898    }
   2899 
   2900    // We need to determine the place that the widget is being dropped in
   2901    // the target.
   2902    let placement;
   2903    let itemForPlacement = aTargetNode;
   2904    // Skip the skipintoolbarset items when determining the place of the item:
   2905    while (
   2906      itemForPlacement &&
   2907      itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
   2908      itemForPlacement.parentNode &&
   2909      itemForPlacement.parentNode.nodeName == "toolbarpaletteitem"
   2910    ) {
   2911      itemForPlacement = itemForPlacement.parentNode.nextElementSibling;
   2912      if (
   2913        itemForPlacement &&
   2914        itemForPlacement.nodeName == "toolbarpaletteitem"
   2915      ) {
   2916        itemForPlacement = itemForPlacement.firstElementChild;
   2917      }
   2918    }
   2919    if (itemForPlacement) {
   2920      let targetNodeId =
   2921        itemForPlacement.nodeName == "toolbarpaletteitem"
   2922          ? itemForPlacement.firstElementChild &&
   2923            itemForPlacement.firstElementChild.id
   2924          : itemForPlacement.id;
   2925      placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
   2926    }
   2927    if (!placement) {
   2928      lazy.log.debug(
   2929        "Could not get a position for " +
   2930          aTargetNode.nodeName +
   2931          "#" +
   2932          aTargetNode.id +
   2933          "." +
   2934          aTargetNode.className
   2935      );
   2936    }
   2937    let position = placement ? placement.position : null;
   2938 
   2939    // Is the target area the same as the origin? Since we've already handled
   2940    // the possibility that the target is the customization palette, we know
   2941    // that the widget is moving within a customizable area.
   2942    if (aTargetArea == aOriginArea) {
   2943      CustomizableUI.moveWidgetWithinArea(aDroppedItemId, position);
   2944      lazy.BrowserUsageTelemetry.recordWidgetChange(
   2945        aDroppedItemId,
   2946        aTargetArea.id,
   2947        "drag"
   2948      );
   2949    } else {
   2950      CustomizableUI.addWidgetToArea(aDroppedItemId, aTargetArea.id, position);
   2951      lazy.BrowserUsageTelemetry.recordWidgetChange(
   2952        aDroppedItemId,
   2953        aTargetArea.id,
   2954        "drag"
   2955      );
   2956    }
   2957 
   2958    this.#onDragEnd(aEvent);
   2959 
   2960    // If we dropped onto a skipintoolbarset item, manually correct the drop location:
   2961    if (aTargetNode != itemForPlacement) {
   2962      let draggedWrapper = draggedItem.parentNode;
   2963      let container = draggedWrapper.parentNode;
   2964      container.insertBefore(draggedWrapper, aTargetNode.parentNode);
   2965    }
   2966  }
   2967 
   2968  /**
   2969   * Handles the dragleave event on any customizable item or area.
   2970   *
   2971   * @param {DragEvent} aEvent
   2972   *   The dragleave event being handled.
   2973   */
   2974  #onDragLeave(aEvent) {
   2975    if (this.#isUnwantedDragDrop(aEvent)) {
   2976      return;
   2977    }
   2978 
   2979    __dumpDragData(aEvent);
   2980 
   2981    // When leaving customization areas, cancel the drag on the last dragover item
   2982    // We've attached the listener to areas, so aEvent.currentTarget will be the area.
   2983    // We don't care about dragleave events fired on descendants of the area,
   2984    // so we check that the event's target is the same as the area to which the listener
   2985    // was attached.
   2986    if (this.#dragOverItem && aEvent.target == aEvent.currentTarget) {
   2987      this.#cancelDragActive(this.#dragOverItem);
   2988      this.#dragOverItem = null;
   2989    }
   2990  }
   2991 
   2992  /**
   2993   * Handles the dragleave event on any customizable item being dragged.
   2994   *
   2995   * @param {DragEvent} aEvent
   2996   *   The dragleave event being handled.
   2997   */
   2998  #onDragEnd(aEvent) {
   2999    // To workaround bug 460801 we manually forward the drop event here when
   3000    // dragend wouldn't be fired.
   3001    //
   3002    // Note that that means that this function may be called multiple times by a
   3003    // single drag operation.
   3004    if (this.#isUnwantedDragDrop(aEvent)) {
   3005      return;
   3006    }
   3007    this._initializeDragAfterMove = null;
   3008    this.#window.clearTimeout(this._dragInitializeTimeout);
   3009    __dumpDragData(aEvent, "#onDragEnd");
   3010 
   3011    let document = aEvent.target.ownerDocument;
   3012    document.documentElement.removeAttribute("customizing-movingItem");
   3013 
   3014    let documentId = document.documentElement.id;
   3015    if (!aEvent.dataTransfer.mozTypesAt(0)) {
   3016      return;
   3017    }
   3018 
   3019    let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
   3020      kDragDataTypePrefix + documentId,
   3021      0
   3022    );
   3023 
   3024    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
   3025 
   3026    // DraggedWrapper might no longer available if a widget node is
   3027    // destroyed after starting (but before stopping) a drag.
   3028    if (draggedWrapper) {
   3029      draggedWrapper.hidden = false;
   3030      draggedWrapper.removeAttribute("mousedown");
   3031 
   3032      let toolbarParent = draggedWrapper.closest("toolbar");
   3033      if (toolbarParent) {
   3034        toolbarParent.style.removeProperty("min-height");
   3035      }
   3036    }
   3037 
   3038    if (this.#dragOverItem) {
   3039      this.#cancelDragActive(this.#dragOverItem);
   3040      this.#dragOverItem = null;
   3041    }
   3042    lazy.DragPositionManager.stop();
   3043  }
   3044 
   3045  /**
   3046   * True if the drag/drop event comes from a source other than one of our
   3047   * browser windows. This check can be overridden for testing by setting
   3048   * `browser.uiCustomization.skipSourceNodeCheck` to `true`.
   3049   *
   3050   * @param {DragEvent} aEvent
   3051   *   A drag/drop event.
   3052   * @returns {boolean}
   3053   *   True if the event should be ignored.
   3054   */
   3055  #isUnwantedDragDrop(aEvent) {
   3056    // The synthesized events for tests generated by synthesizePlainDragAndDrop
   3057    // and synthesizeDrop in mochitests are used only for testing whether the
   3058    // right data is being put into the dataTransfer. Neither cause a real drop
   3059    // to occur, so they don't set the source node. There isn't a means of
   3060    // testing real drag and drops, so this pref skips the check but it should
   3061    // only be set by test code.
   3062    if (this.#skipSourceNodeCheck) {
   3063      return false;
   3064    }
   3065 
   3066    /* Discard drag events that originated from a separate window to
   3067       prevent content->chrome privilege escalations. */
   3068    let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
   3069    // mozSourceNode is null in the dragStart event handler or if
   3070    // the drag event originated in an external application.
   3071    return !mozSourceNode || mozSourceNode.ownerGlobal != this.#window;
   3072  }
   3073 
   3074  /**
   3075   * Handles applying drag preview effects to a customizable item or area while
   3076   * a drag and drop operation is underway.
   3077   *
   3078   * @param {DOMNode} aDraggedOverItem
   3079   *   A customizable item being dragged over within a customizable area, or
   3080   *   a customizable area node.
   3081   * @param {string} aValue
   3082   *   A string to set as the "dragover" attribute on the dragged item to
   3083   *   indicate which direction (before or after) the placeholder for the
   3084   *   drag operation should go relative to aItem. This is either the
   3085   *   string "before" or the string "after".
   3086   * @param {string} aDraggedItemId
   3087   *   The ID of the customizable item being dragged.
   3088   * @param {string} aPlace
   3089   *   The place string associated with the customizable area being dragged
   3090   *   over. This is expected to be one of the strings returned by
   3091   *   CustomizableUI.getPlaceForItem.
   3092   */
   3093  #setDragActive(aDraggedOverItem, aValue, aDraggedItemId, aPlace) {
   3094    if (!aDraggedOverItem) {
   3095      return;
   3096    }
   3097 
   3098    if (aDraggedOverItem.getAttribute("dragover") != aValue) {
   3099      aDraggedOverItem.setAttribute("dragover", aValue);
   3100 
   3101      let window = aDraggedOverItem.ownerGlobal;
   3102      let draggedItem = window.document.getElementById(aDraggedItemId);
   3103      if (aPlace == "palette") {
   3104        // We mostly delegate the complexity of grid placeholder effects to
   3105        // DragPositionManager by way of #setGridDragActive.
   3106        this.#setGridDragActive(aDraggedOverItem, draggedItem, aValue);
   3107      } else {
   3108        let targetArea = this.#getCustomizableParent(aDraggedOverItem);
   3109        let makeSpaceImmediately = false;
   3110        if (!gDraggingInToolbars.has(targetArea.id)) {
   3111          gDraggingInToolbars.add(targetArea.id);
   3112          let draggedWrapper = this.$("wrapper-" + aDraggedItemId);
   3113          let originArea = this.#getCustomizableParent(draggedWrapper);
   3114          makeSpaceImmediately = originArea == targetArea;
   3115        }
   3116        let propertyToMeasure = aPlace == "toolbar" ? "width" : "height";
   3117        // Calculate width/height of the item when it'd be dropped in this position.
   3118        let borderWidth = this.#getDragItemSize(aDraggedOverItem, draggedItem)[
   3119          propertyToMeasure
   3120        ];
   3121        let layoutSide = aPlace == "toolbar" ? "Inline" : "Block";
   3122        let prop, otherProp;
   3123        if (aValue == "before") {
   3124          prop = "border" + layoutSide + "StartWidth";
   3125          otherProp = "border-" + layoutSide.toLowerCase() + "-end-width";
   3126        } else {
   3127          prop = "border" + layoutSide + "EndWidth";
   3128          otherProp = "border-" + layoutSide.toLowerCase() + "-start-width";
   3129        }
   3130        if (makeSpaceImmediately) {
   3131          aDraggedOverItem.setAttribute("notransition", "true");
   3132        }
   3133        aDraggedOverItem.style[prop] = borderWidth + "px";
   3134        aDraggedOverItem.style.removeProperty(otherProp);
   3135        if (makeSpaceImmediately) {
   3136          // Force a layout flush:
   3137          aDraggedOverItem.getBoundingClientRect();
   3138          aDraggedOverItem.removeAttribute("notransition");
   3139        }
   3140      }
   3141    }
   3142  }
   3143 
   3144  /**
   3145   * Reverts drag preview effects applied via #setDragActive from a customizable
   3146   * item or area when a drag and drop operation ends.
   3147   *
   3148   * @param {DOMNode} aDraggedOverItem
   3149   *   The customizable item or area that was being dragged over.
   3150   * @param {DOMNode} aNextDraggedOverItem
   3151   *   If non-null, this is the customizable item or area that is being
   3152   *   dragged over now instead of aDraggedOverItem.
   3153   * @param {boolean} aNoTransition
   3154   *   True if the reversion of the drag preview effect should occur without
   3155   *   a transition (for example, on a drop).
   3156   */
   3157  #cancelDragActive(aDraggedOverItem, aNextDraggedOverItem, aNoTransition) {
   3158    let currentArea = this.#getCustomizableParent(aDraggedOverItem);
   3159    if (!currentArea) {
   3160      return;
   3161    }
   3162    let nextArea = aNextDraggedOverItem
   3163      ? this.#getCustomizableParent(aNextDraggedOverItem)
   3164      : null;
   3165    if (currentArea != nextArea) {
   3166      currentArea.removeAttribute("draggingover");
   3167    }
   3168    let areaType = CustomizableUI.getAreaType(currentArea.id);
   3169    if (areaType) {
   3170      if (aNoTransition) {
   3171        aDraggedOverItem.setAttribute("notransition", "true");
   3172      }
   3173      aDraggedOverItem.removeAttribute("dragover");
   3174      // Remove all property values in the case that the end padding
   3175      // had been set.
   3176      aDraggedOverItem.style.removeProperty("border-inline-start-width");
   3177      aDraggedOverItem.style.removeProperty("border-inline-end-width");
   3178      aDraggedOverItem.style.removeProperty("border-block-start-width");
   3179      aDraggedOverItem.style.removeProperty("border-block-end-width");
   3180      if (aNoTransition) {
   3181        // Force a layout flush:
   3182        aDraggedOverItem.getBoundingClientRect();
   3183        aDraggedOverItem.removeAttribute("notransition");
   3184      }
   3185    } else {
   3186      aDraggedOverItem.removeAttribute("dragover");
   3187      if (aNextDraggedOverItem) {
   3188        if (nextArea == currentArea) {
   3189          // No need to do anything if we're still dragging in this area:
   3190          return;
   3191        }
   3192      }
   3193      // Otherwise, clear everything out:
   3194      let positionManager =
   3195        lazy.DragPositionManager.getManagerForArea(currentArea);
   3196      positionManager.clearPlaceholders(currentArea, aNoTransition);
   3197    }
   3198  }
   3199 
   3200  /**
   3201   * Handles applying drag preview effects to the customization palette grid.
   3202   *
   3203   * @param {DOMNode} aDragOverNode
   3204   *   A customizable item being dragged over within the palette, or
   3205   *   the palette node itself.
   3206   * @param {DOMNode} aDraggedItem
   3207   *   The customizable item being dragged.
   3208   */
   3209  #setGridDragActive(aDragOverNode, aDraggedItem) {
   3210    let targetArea = this.#getCustomizableParent(aDragOverNode);
   3211    let draggedWrapper = this.$("wrapper-" + aDraggedItem.id);
   3212    let originArea = this.#getCustomizableParent(draggedWrapper);
   3213    let positionManager =
   3214      lazy.DragPositionManager.getManagerForArea(targetArea);
   3215    let draggedSize = this.#getDragItemSize(aDragOverNode, aDraggedItem);
   3216    positionManager.insertPlaceholder(
   3217      targetArea,
   3218      aDragOverNode,
   3219      draggedSize,
   3220      originArea == targetArea
   3221    );
   3222  }
   3223 
   3224  /**
   3225   * Given a customizable item being dragged, and a DOMNode being dragged over,
   3226   * returns the size of the dragged item were it to be placed within the area
   3227   * associated with aDragOverNode.
   3228   *
   3229   * @param {DOMNode} aDragOverNode
   3230   *   The node currently being dragged over.
   3231   * @param {DOMNode} aDraggedItem
   3232   *   The customizable item node currently being dragged.
   3233   * @returns {ItemSizeForArea}
   3234   */
   3235  #getDragItemSize(aDragOverNode, aDraggedItem) {
   3236    // Cache it good, cache it real good.
   3237    if (!this.#dragSizeMap) {
   3238      this.#dragSizeMap = new WeakMap();
   3239    }
   3240    if (!this.#dragSizeMap.has(aDraggedItem)) {
   3241      this.#dragSizeMap.set(aDraggedItem, new WeakMap());
   3242    }
   3243    let itemMap = this.#dragSizeMap.get(aDraggedItem);
   3244    let targetArea = this.#getCustomizableParent(aDragOverNode);
   3245    let currentArea = this.#getCustomizableParent(aDraggedItem);
   3246    // Return the size for this target from cache, if it exists.
   3247    let size = itemMap.get(targetArea);
   3248    if (size) {
   3249      return size;
   3250    }
   3251 
   3252    // Calculate size of the item when it'd be dropped in this position.
   3253    let currentParent = aDraggedItem.parentNode;
   3254    let currentSibling = aDraggedItem.nextElementSibling;
   3255    const kAreaType = "cui-areatype";
   3256    let areaType, currentType;
   3257 
   3258    if (targetArea != currentArea) {
   3259      // Move the widget temporarily next to the placeholder.
   3260      aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
   3261      // Update the node's areaType.
   3262      areaType = CustomizableUI.getAreaType(targetArea.id);
   3263      currentType =
   3264        aDraggedItem.hasAttribute(kAreaType) &&
   3265        aDraggedItem.getAttribute(kAreaType);
   3266      if (areaType) {
   3267        aDraggedItem.setAttribute(kAreaType, areaType);
   3268      }
   3269      this.wrapToolbarItem(aDraggedItem, areaType || "palette");
   3270      CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
   3271    } else {
   3272      aDraggedItem.parentNode.hidden = false;
   3273    }
   3274 
   3275    // Fetch the new size.
   3276    let rect = aDraggedItem.parentNode.getBoundingClientRect();
   3277    size = { width: rect.width, height: rect.height };
   3278    // Cache the found value of size for this target.
   3279    itemMap.set(targetArea, size);
   3280 
   3281    if (targetArea != currentArea) {
   3282      this.unwrapToolbarItem(aDraggedItem.parentNode);
   3283      // Put the item back into its previous position.
   3284      currentParent.insertBefore(aDraggedItem, currentSibling);
   3285      // restore the areaType
   3286      if (areaType) {
   3287        if (currentType === false) {
   3288          aDraggedItem.removeAttribute(kAreaType);
   3289        } else {
   3290          aDraggedItem.setAttribute(kAreaType, currentType);
   3291        }
   3292      }
   3293      this.createOrUpdateWrapper(aDraggedItem, null, true);
   3294      CustomizableUI.onWidgetDrag(aDraggedItem.id);
   3295    } else {
   3296      aDraggedItem.parentNode.hidden = true;
   3297    }
   3298    return size;
   3299  }
   3300 
   3301  /**
   3302   * Walks the ancestry of a DOMNode element and finds the first customizable
   3303   * area node in that ancestry, or null if no such customizable area node
   3304   * can be found.
   3305   *
   3306   * @param {DOMNode} aElement
   3307   *   The DOMNode for which to find the customizable area parent.
   3308   * @returns {DOMNode}
   3309   *   The customizable area parent of aElement.
   3310   */
   3311  #getCustomizableParent(aElement) {
   3312    if (aElement) {
   3313      // Deal with drag/drop on the padding of the panel.
   3314      let containingPanelHolder = aElement.closest(
   3315        "#customization-panelHolder"
   3316      );
   3317      if (containingPanelHolder) {
   3318        return containingPanelHolder.querySelector(
   3319          "#widget-overflow-fixed-list"
   3320        );
   3321      }
   3322    }
   3323 
   3324    let areas = CustomizableUI.areas;
   3325    areas.push(kPaletteId);
   3326    return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
   3327  }
   3328 
   3329  /**
   3330   * During a drag operation of a customizable item over a customizable area,
   3331   * returns the node within that customizable area that the item is being
   3332   * dragged over.
   3333   *
   3334   * @param {DragEvent} aEvent
   3335   *   The dragover event being handled.
   3336   * @param {DOMNode} aAreaElement
   3337   *   The customizable area element that we should consider the dragover
   3338   *   operation to be occurring on. This might actually be different from the
   3339   *   target of the dragover event if we've retargeted the drag (see bug
   3340   *   1396423 for an example of where we retarget a dragover area to the
   3341   *   palette rather than the overflow panel).
   3342   * @param {string} aPlace
   3343   *   The place string associated with the customizable area being dragged
   3344   *   over. This is expected to be one of the strings returned by
   3345   *   CustomizableUI.getPlaceForItem.
   3346   * @returns {DOMNode}
   3347   *   The node within aAreaElement that we should assume the dragged item is
   3348   *   being dragged over. If we cannot resolve this to a target, this falls
   3349   *   back to just being the aEvent.target.
   3350   */
   3351  #getDragOverNode(aEvent, aAreaElement, aPlace) {
   3352    let expectedParent =
   3353      CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
   3354    if (!expectedParent.contains(aEvent.target)) {
   3355      return expectedParent;
   3356    }
   3357    // Offset the drag event's position with the offset to the center of
   3358    // the thing we're dragging
   3359    let dragX = aEvent.clientX - this._dragOffset.x;
   3360    let dragY = aEvent.clientY - this._dragOffset.y;
   3361 
   3362    // Ensure this is within the container
   3363    let boundsContainer = expectedParent;
   3364    let bounds = this.#getBoundsWithoutFlushing(boundsContainer);
   3365    dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
   3366    dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
   3367 
   3368    let targetNode;
   3369    if (aPlace == "toolbar" || aPlace == "panel") {
   3370      targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
   3371      while (targetNode && targetNode.parentNode != expectedParent) {
   3372        targetNode = targetNode.parentNode;
   3373      }
   3374    } else {
   3375      let positionManager =
   3376        lazy.DragPositionManager.getManagerForArea(aAreaElement);
   3377      // Make it relative to the container:
   3378      dragX -= bounds.left;
   3379      dragY -= bounds.top;
   3380      // Find the closest node:
   3381      targetNode = positionManager.find(aAreaElement, dragX, dragY);
   3382    }
   3383    return targetNode || aEvent.target;
   3384  }
   3385 
   3386  /**
   3387   * Handler for mousedown events in the customize mode UI for the primary
   3388   * button. If the mousedown event is being fired on a customizable item, it
   3389   * will have a "mousedown" attribute set to "true" on it. A
   3390   * "customizing-movingItem" attribute is also set to "true" on the
   3391   * document element.
   3392   *
   3393   * @param {MouseEvent} aEvent
   3394   *   The mousedown event being handled.
   3395   */
   3396  #onMouseDown(aEvent) {
   3397    lazy.log.debug("#onMouseDown");
   3398    if (aEvent.button != 0) {
   3399      return;
   3400    }
   3401    let doc = aEvent.target.ownerDocument;
   3402    doc.documentElement.setAttribute("customizing-movingItem", true);
   3403    let item = this.#getWrapper(aEvent.target);
   3404    if (item) {
   3405      item.toggleAttribute("mousedown", true);
   3406    }
   3407  }
   3408 
   3409  /**
   3410   * Handler for mouseup events in the customize mode UI for the primary
   3411   * button. If the mouseup event is being fired on a customizable item, it
   3412   * will have the "mousedown" attribute added in #onMouseDown removed. This
   3413   * will also remove the "customizing-movingItem" attribute on the document
   3414   * element.
   3415   *
   3416   * @param {MouseEvent} aEvent
   3417   *   The mouseup event being handled.
   3418   */
   3419  #onMouseUp(aEvent) {
   3420    lazy.log.debug("#onMouseUp");
   3421    if (aEvent.button != 0) {
   3422      return;
   3423    }
   3424    let doc = aEvent.target.ownerDocument;
   3425    doc.documentElement.removeAttribute("customizing-movingItem");
   3426    let item = this.#getWrapper(aEvent.target);
   3427    if (item) {
   3428      item.removeAttribute("mousedown");
   3429    }
   3430  }
   3431 
   3432  /**
   3433   * Given a customizable item (or one of its descendants), returns the
   3434   * toolbarpaletteitem wrapper node ancestor. If no such wrapper can be found,
   3435   * this returns null.
   3436   *
   3437   * @param {DOMNode} aElement
   3438   *   The customizable item node (or one of its descendants) to get the
   3439   *   toolbarpaletteitem wrapper node ancestor for.
   3440   * @returns {DOMNode|null}
   3441   *   The toolbarpaletteitem wrapper node, or null if one cannot be found.
   3442   */
   3443  #getWrapper(aElement) {
   3444    while (aElement && aElement.localName != "toolbarpaletteitem") {
   3445      if (aElement.localName == "toolbar") {
   3446        return null;
   3447      }
   3448      aElement = aElement.parentNode;
   3449    }
   3450    return aElement;
   3451  }
   3452 
   3453  /**
   3454   * Given some toolbarpaletteitem wrapper, walks the prior sibling elements
   3455   * until it finds one that either isn't a toolbarpaletteitem, or doesn't have
   3456   * it's first element hidden. Returns null if no such prior sibling element
   3457   * can be found.
   3458   *
   3459   * @param {DOMNode} aReferenceNode
   3460   *   The toolbarpaletteitem node to check the prior siblings for visibilty.
   3461   *   If aReferenceNode is not a toolbarpaletteitem, this just returns the
   3462   *   aReferenceNode immediately.
   3463   * @returns {DOMNode|null}
   3464   *   The first prior sibling with a visible first element child, or the
   3465   *   first non-toolbarpaletteitem prior sibling, or null if no such item can
   3466   *   be found.
   3467   */
   3468  #findVisiblePreviousSiblingNode(aReferenceNode) {
   3469    while (
   3470      aReferenceNode &&
   3471      aReferenceNode.localName == "toolbarpaletteitem" &&
   3472      aReferenceNode.firstElementChild.hidden
   3473    ) {
   3474      aReferenceNode = aReferenceNode.previousElementSibling;
   3475    }
   3476    return aReferenceNode;
   3477  }
   3478 
   3479  /**
   3480   * The popupshowing event handler for the context menu on the customization
   3481   * palette.
   3482   *
   3483   * @param {WidgetMouseEvent} event
   3484   *   The popupshowing event being fired for the context menu.
   3485   */
   3486  #onPaletteContextMenuShowing(event) {
   3487    let isFlexibleSpace = event.target.triggerNode.id.includes(
   3488      "wrapper-customizableui-special-spring"
   3489    );
   3490    event.target.querySelector(".customize-context-addToPanel").disabled =
   3491      isFlexibleSpace;
   3492  }
   3493 
   3494  /**
   3495   * The popupshowing event handler for the context menu for items in the
   3496   * overflow panel while in customize mode. This is currently public due to
   3497   * bug 1378427 (also see bug 1747945).
   3498   *
   3499   * @param {WidgetMouseEvent} event
   3500   *   The popupshowing event being fired for the context menu.
   3501   */
   3502  onPanelContextMenuShowing(event) {
   3503    let inPermanentArea = !!event.target.triggerNode.closest(
   3504      "#widget-overflow-fixed-list"
   3505    );
   3506    let doc = event.target.ownerDocument;
   3507    doc.getElementById("customizationPanelItemContextMenuUnpin").hidden =
   3508      !inPermanentArea;
   3509    doc.getElementById("customizationPanelItemContextMenuPin").hidden =
   3510      inPermanentArea;
   3511 
   3512    doc.ownerGlobal.MozXULElement.insertFTLIfNeeded(
   3513      "browser/toolbarContextMenu.ftl"
   3514    );
   3515    event.target.querySelectorAll("[data-lazy-l10n-id]").forEach(el => {
   3516      el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
   3517      el.removeAttribute("data-lazy-l10n-id");
   3518    });
   3519  }
   3520 
   3521  /**
   3522   * A window click event handler that checks to see if the item being clicked
   3523   * in the window is the downloads button wrapper, with the primary button.
   3524   * This is used to show the downloads button autohide panel.
   3525   *
   3526   * @param {MouseEvent} event
   3527   *   The click event on the window.
   3528   */
   3529  #checkForDownloadsClick(event) {
   3530    if (
   3531      event.target.closest("#wrapper-downloads-button") &&
   3532      event.button == 0
   3533    ) {
   3534      event.view.gCustomizeMode.#showDownloadsAutoHidePanel();
   3535    }
   3536  }
   3537 
   3538  /**
   3539   * Adds a click event listener to the top-level window to check to see if the
   3540   * downloads button is ever clicked while in customize mode. Callers should
   3541   * ensure that #teardownDownloadAutoHideToggle is called when exiting
   3542   * customize mode.
   3543   */
   3544  #setupDownloadAutoHideToggle() {
   3545    this.#window.addEventListener("click", this.#checkForDownloadsClick, true);
   3546  }
   3547 
   3548  /**
   3549   * Removes the click event listener on the top-level window that was added by
   3550   * #setupDownloadAutoHideToggle.
   3551   */
   3552  #teardownDownloadAutoHideToggle() {
   3553    this.#window.removeEventListener(
   3554      "click",
   3555      this.#checkForDownloadsClick,
   3556      true
   3557    );
   3558    this.$(kDownloadAutohidePanelId).hidePopup();
   3559  }
   3560 
   3561  /**
   3562   * Attempts to move the downloads button to the navigation toolbar in the
   3563   * event that they've turned on the autohide feature for the button while
   3564   * the button is in the palette.
   3565   */
   3566  #maybeMoveDownloadsButtonToNavBar() {
   3567    // If the user toggled the autohide checkbox while the item was in the
   3568    // palette, and hasn't moved it since, move the item to the default
   3569    // location in the navbar for them.
   3570    if (
   3571      !CustomizableUI.getPlacementOfWidget("downloads-button") &&
   3572      this.#moveDownloadsButtonToNavBar &&
   3573      this.#window.DownloadsButton.autoHideDownloadsButton
   3574    ) {
   3575      let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar");
   3576      let insertionPoint = navbarPlacements.indexOf("urlbar-container");
   3577      while (++insertionPoint < navbarPlacements.length) {
   3578        let widget = navbarPlacements[insertionPoint];
   3579        // If we find a non-searchbar, non-spacer node, break out of the loop:
   3580        if (
   3581          widget != "search-container" &&
   3582          !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))
   3583        ) {
   3584          break;
   3585        }
   3586      }
   3587      CustomizableUI.addWidgetToArea(
   3588        "downloads-button",
   3589        "nav-bar",
   3590        insertionPoint
   3591      );
   3592      lazy.BrowserUsageTelemetry.recordWidgetChange(
   3593        "downloads-button",
   3594        "nav-bar",
   3595        "move-downloads"
   3596      );
   3597    }
   3598  }
   3599 
   3600  /**
   3601   * Opens the panel that shows the toggle for auto-hiding the downloads button
   3602   * when there are no downloads underway. If that panel is already open, it
   3603   * is first closed. This panel is not shown if the downloads button is in
   3604   * the overflow panel (since when the button is there, it does not autohide).
   3605   *
   3606   * @returns {Promise<undefined>}
   3607   */
   3608  async #showDownloadsAutoHidePanel() {
   3609    let doc = this.#document;
   3610    let panel = doc.getElementById(kDownloadAutohidePanelId);
   3611    panel.hidePopup();
   3612    let button = doc.getElementById("downloads-button");
   3613    // We don't show the tooltip if the button is in the panel.
   3614    if (button.closest("#widget-overflow-fixed-list")) {
   3615      return;
   3616    }
   3617 
   3618    let offsetX = 0,
   3619      offsetY = 0;
   3620    let panelOnTheLeft = false;
   3621    let toolbarContainer = button.closest("toolbar");
   3622    if (toolbarContainer && toolbarContainer.id == "nav-bar") {
   3623      let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
   3624      if (
   3625        navbarWidgets.indexOf("urlbar-container") <=
   3626        navbarWidgets.indexOf("downloads-button")
   3627      ) {
   3628        panelOnTheLeft = true;
   3629      }
   3630    } else {
   3631      await this.#window.promiseDocumentFlushed(() => {});
   3632 
   3633      if (!this.#customizing || !this._wantToBeInCustomizeMode) {
   3634        return;
   3635      }
   3636      let buttonBounds = this.#getBoundsWithoutFlushing(button);
   3637      let windowBounds = this.#getBoundsWithoutFlushing(doc.documentElement);
   3638      panelOnTheLeft =
   3639        buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2;
   3640    }
   3641    let position;
   3642    if (panelOnTheLeft) {
   3643      // Tested in RTL, these get inverted automatically, so this does the
   3644      // right thing without taking RTL into account explicitly.
   3645      position = "topleft topright";
   3646      if (toolbarContainer) {
   3647        offsetX = 8;
   3648      }
   3649    } else {
   3650      position = "topright topleft";
   3651      if (toolbarContainer) {
   3652        offsetX = -8;
   3653      }
   3654    }
   3655 
   3656    let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
   3657    if (this.#window.DownloadsButton.autoHideDownloadsButton) {
   3658      checkbox.setAttribute("checked", "true");
   3659    } else {
   3660      checkbox.removeAttribute("checked");
   3661    }
   3662 
   3663    // We don't use the icon to anchor because it might be resizing because of
   3664    // the animations for drag/drop. Hence the use of offsets.
   3665    panel.openPopup(button, position, offsetX, offsetY);
   3666  }
   3667 
   3668  /**
   3669   * Called when the downloads button auto-hide toggle changes value.
   3670   *
   3671   * @param {CommandEvent} event
   3672   *   The event that caused the toggle change.
   3673   */
   3674  #onDownloadsAutoHideChange(event) {
   3675    let checkbox = event.target.ownerDocument.getElementById(
   3676      kDownloadAutohideCheckboxId
   3677    );
   3678    Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
   3679    // Ensure we move the button (back) after the user leaves customize mode.
   3680    event.view.gCustomizeMode.#moveDownloadsButtonToNavBar = checkbox.checked;
   3681  }
   3682 
   3683  /**
   3684   * Called when the button to customize the macOS touchbar is clicked.
   3685   */
   3686  #customizeTouchBar() {
   3687    let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService(
   3688      Ci.nsITouchBarUpdater
   3689    );
   3690    updater.enterCustomizeMode();
   3691  }
   3692 
   3693  /**
   3694   * This is a method to toggle pong on or off in customize mode. You heard me.
   3695   *
   3696   * @param {boolean} enabled
   3697   *   True if pong should be launched, or false if it should be torn down.
   3698   */
   3699  #togglePong(enabled) {
   3700    // It's possible we're toggling for a reason other than hitting
   3701    // the button (we might be exiting, for example), so make sure that
   3702    // the state and checkbox are in sync.
   3703    let whimsyButton = this.$("whimsy-button");
   3704    whimsyButton.checked = enabled;
   3705 
   3706    if (enabled) {
   3707      this.visiblePalette.setAttribute("whimsypong", "true");
   3708      this.pongArena.hidden = false;
   3709      if (!this.uninitWhimsy) {
   3710        this.uninitWhimsy = this.#whimsypong();
   3711      }
   3712    } else {
   3713      this.visiblePalette.removeAttribute("whimsypong");
   3714      if (this.uninitWhimsy) {
   3715        this.uninitWhimsy();
   3716        this.uninitWhimsy = null;
   3717      }
   3718      this.pongArena.hidden = true;
   3719    }
   3720  }
   3721 
   3722  /**
   3723   * This method contains a very simple implementation of a pong-like game.
   3724   * Calling this method presumes that the pongArea element is visible.
   3725   *
   3726   * @returns {Function}
   3727   *   Returns a clean-up function which tears down the launched pong game.
   3728   */
   3729  #whimsypong() {
   3730    function update() {
   3731      updateBall();
   3732      updatePlayers();
   3733    }
   3734 
   3735    function updateBall() {
   3736      if (ball[1] <= 0 || ball[1] >= gameSide) {
   3737        if (
   3738          (ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
   3739          (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))
   3740        ) {
   3741          updateScore(ball[1] <= 0 ? 0 : 1);
   3742        } else {
   3743          if (
   3744            (ball[1] <= 0 &&
   3745              (ball[0] - p1 < paddleEdge ||
   3746                p1 + paddleWidth - ball[0] < paddleEdge)) ||
   3747            (ball[1] >= gameSide &&
   3748              (ball[0] - p2 < paddleEdge ||
   3749                p2 + paddleWidth - ball[0] < paddleEdge))
   3750          ) {
   3751            ballDxDy[0] *= Math.random() + 1.3;
   3752            ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6);
   3753            if (Math.abs(ballDxDy[0]) == 6) {
   3754              ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random();
   3755            }
   3756          } else {
   3757            ballDxDy[0] /= 1.1;
   3758          }
   3759          ballDxDy[1] *= -1;
   3760          ball[1] = ball[1] <= 0 ? 0 : gameSide;
   3761        }
   3762      }
   3763      ball = [
   3764        Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
   3765        Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0),
   3766      ];
   3767      if (ball[0] <= 0 || ball[0] >= gameSide) {
   3768        ballDxDy[0] *= -1;
   3769      }
   3770    }
   3771 
   3772    function updatePlayers() {
   3773      if (keydown) {
   3774        let p1Adj = 1;
   3775        if (
   3776          (keydown == 37 && !window.RTL_UI) ||
   3777          (keydown == 39 && window.RTL_UI)
   3778        ) {
   3779          p1Adj = -1;
   3780        }
   3781        p1 += p1Adj * 10 * keydownAdj;
   3782      }
   3783 
   3784      let sign = Math.sign(ballDxDy[0]);
   3785      if (
   3786        (sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
   3787        (sign < 0 && ball[0] < p2 + paddleWidth / 2)
   3788      ) {
   3789        p2 += sign * 3;
   3790      } else if (
   3791        (sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
   3792        (sign < 0 && ball[0] < p2 + paddleWidth / 1.1)
   3793      ) {
   3794        p2 += sign * 9;
   3795      }
   3796 
   3797      if (score >= winScore) {
   3798        p1 = ball[0];
   3799        p2 = ball[0];
   3800      }
   3801      p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
   3802      p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
   3803    }
   3804 
   3805    function updateScore(adj) {
   3806      if (adj) {
   3807        score += adj;
   3808      } else if (--lives == 0) {
   3809        quit = true;
   3810      }
   3811      ball = ballDef.slice();
   3812      ballDxDy = ballDxDyDef.slice();
   3813      ballDxDy[1] *= score / winScore + 1;
   3814    }
   3815 
   3816    function draw() {
   3817      let xAdj = window.RTL_UI ? -1 : 1;
   3818      elements["wp-player1"].style.transform =
   3819        "translate(" + xAdj * p1 + "px, -37px)";
   3820      elements["wp-player2"].style.transform =
   3821        "translate(" + xAdj * p2 + "px, " + gameSide + "px)";
   3822      elements["wp-ball"].style.transform =
   3823        "translate(" + xAdj * ball[0] + "px, " + ball[1] + "px)";
   3824      elements["wp-score"].textContent = score;
   3825      elements["wp-lives"].setAttribute("lives", lives);
   3826      if (score >= winScore) {
   3827        let arena = elements.arena;
   3828        let image = "url(chrome://browser/skin/customizableui/whimsy.png)";
   3829        let position = `${
   3830          (window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10
   3831        }px ${ball[1] - 10}px`;
   3832        let repeat = "no-repeat";
   3833        let size = "20px";
   3834        if (arena.style.backgroundImage) {
   3835          if (arena.style.backgroundImage.split(",").length >= 160) {
   3836            quit = true;
   3837          }
   3838 
   3839          image += ", " + arena.style.backgroundImage;
   3840          position += ", " + arena.style.backgroundPosition;
   3841          repeat += ", " + arena.style.backgroundRepeat;
   3842          size += ", " + arena.style.backgroundSize;
   3843        }
   3844        arena.style.backgroundImage = image;
   3845        arena.style.backgroundPosition = position;
   3846        arena.style.backgroundRepeat = repeat;
   3847        arena.style.backgroundSize = size;
   3848      }
   3849    }
   3850 
   3851    function onkeydown(event) {
   3852      keys.push(event.which);
   3853      if (keys.length > 10) {
   3854        keys.shift();
   3855        let codeEntered = true;
   3856        for (let i = 0; i < keys.length; i++) {
   3857          if (keys[i] != keysCode[i]) {
   3858            codeEntered = false;
   3859            break;
   3860          }
   3861        }
   3862        if (codeEntered) {
   3863          elements.arena.setAttribute("kcode", "true");
   3864          let spacer = document.querySelector(
   3865            "#customization-palette > toolbarpaletteitem"
   3866          );
   3867          spacer.setAttribute("kcode", "true");
   3868        }
   3869      }
   3870      if (event.which == 37 /* left */ || event.which == 39 /* right */) {
   3871        keydown = event.which;
   3872        keydownAdj *= 1.05;
   3873      }
   3874    }
   3875 
   3876    function onkeyup(event) {
   3877      if (event.which == 37 || event.which == 39) {
   3878        keydownAdj = 1;
   3879        keydown = 0;
   3880      }
   3881    }
   3882 
   3883    function uninit() {
   3884      document.removeEventListener("keydown", onkeydown);
   3885      document.removeEventListener("keyup", onkeyup);
   3886      if (rAFHandle) {
   3887        window.cancelAnimationFrame(rAFHandle);
   3888      }
   3889      let arena = elements.arena;
   3890      while (arena.firstChild) {
   3891        arena.firstChild.remove();
   3892      }
   3893      arena.removeAttribute("score");
   3894      arena.removeAttribute("lives");
   3895      arena.removeAttribute("kcode");
   3896      arena.style.removeProperty("background-image");
   3897      arena.style.removeProperty("background-position");
   3898      arena.style.removeProperty("background-repeat");
   3899      arena.style.removeProperty("background-size");
   3900      let spacer = document.querySelector(
   3901        "#customization-palette > toolbarpaletteitem"
   3902      );
   3903      spacer.removeAttribute("kcode");
   3904      elements = null;
   3905      document = null;
   3906      quit = true;
   3907    }
   3908 
   3909    if (this.uninitWhimsy) {
   3910      return this.uninitWhimsy;
   3911    }
   3912 
   3913    let ballDef = [10, 10];
   3914    let ball = [10, 10];
   3915    let ballDxDyDef = [2, 2];
   3916    let ballDxDy = [2, 2];
   3917    let score = 0;
   3918    let p1 = 0;
   3919    let p2 = 10;
   3920    let gameSide = 300;
   3921    let paddleEdge = 30;
   3922    let paddleWidth = 84;
   3923    let keydownAdj = 1;
   3924    let keydown = 0;
   3925    let keys = [];
   3926    let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
   3927    let lives = 5;
   3928    let winScore = 11;
   3929    let quit = false;
   3930    let document = this.#document;
   3931    let rAFHandle = 0;
   3932    let elements = {
   3933      arena: document.getElementById("customization-pong-arena"),
   3934    };
   3935 
   3936    document.addEventListener("keydown", onkeydown);
   3937    document.addEventListener("keyup", onkeyup);
   3938 
   3939    for (let id of ["player1", "player2", "ball", "score", "lives"]) {
   3940      let el = document.createXULElement("box");
   3941      el.id = "wp-" + id;
   3942      elements[el.id] = elements.arena.appendChild(el);
   3943    }
   3944 
   3945    let spacer = this.visiblePalette.querySelector("toolbarpaletteitem");
   3946    for (let player of ["#wp-player1", "#wp-player2"]) {
   3947      let val = "-moz-element(#" + spacer.id + ") no-repeat";
   3948      elements.arena.querySelector(player).style.background = val;
   3949    }
   3950 
   3951    let window = this.#window;
   3952    rAFHandle = window.requestAnimationFrame(function animate() {
   3953      update();
   3954      draw();
   3955      if (quit) {
   3956        elements["wp-score"].textContent = score;
   3957        elements["wp-lives"] &&
   3958          elements["wp-lives"].setAttribute("lives", lives);
   3959        elements.arena.setAttribute("score", score);
   3960        elements.arena.setAttribute("lives", lives);
   3961      } else {
   3962        rAFHandle = window.requestAnimationFrame(animate);
   3963      }
   3964    });
   3965 
   3966    return uninit;
   3967  }
   3968 }
   3969 
   3970 /**
   3971 * A utility function that, when in debug mode, can emit drag data through the
   3972 * debug logging mechanism for various drag and drop events.
   3973 *
   3974 * @param {DragEvent} aEvent
   3975 *   The DragEvent to dump debug information to the log for.
   3976 * @param {string|null} caller
   3977 *   An optional string to indicate the caller of the log message.
   3978 */
   3979 function __dumpDragData(aEvent, caller) {
   3980  if (!gDebug) {
   3981    return;
   3982  }
   3983  let str =
   3984    "Dumping drag data (" +
   3985    (caller ? caller + " in " : "") +
   3986    "CustomizeMode.sys.mjs) {\n";
   3987  str += "  type: " + aEvent.type + "\n";
   3988  for (let el of ["target", "currentTarget", "relatedTarget"]) {
   3989    if (aEvent[el]) {
   3990      str +=
   3991        "  " +
   3992        el +
   3993        ": " +
   3994        aEvent[el] +
   3995        "(localName=" +
   3996        aEvent[el].localName +
   3997        "; id=" +
   3998        aEvent[el].id +
   3999        ")\n";
   4000    }
   4001  }
   4002  for (let prop in aEvent.dataTransfer) {
   4003    if (typeof aEvent.dataTransfer[prop] != "function") {
   4004      str +=
   4005        "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
   4006    }
   4007  }
   4008  str += "}";
   4009  lazy.log.debug(str);
   4010 }