tor-browser

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

PanelMultiView.sys.mjs (70007B)


      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 lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  CustomizableUI:
      8    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
      9 });
     10 
     11 ChromeUtils.defineLazyGetter(lazy, "gBundle", function () {
     12  return Services.strings.createBundle(
     13    "chrome://browser/locale/browser.properties"
     14  );
     15 });
     16 
     17 /**
     18 * Safety timeout after which asynchronous events will be canceled if any of the
     19 * registered blockers does not return.
     20 */
     21 const BLOCKERS_TIMEOUT_MS = 10000;
     22 
     23 const TRANSITION_PHASES = Object.freeze({
     24  START: 1,
     25  PREPARE: 2,
     26  TRANSITION: 3,
     27 });
     28 
     29 let gNodeToObjectMap = new WeakMap();
     30 let gWindowsWithUnloadHandler = new WeakSet();
     31 
     32 /**
     33 * Allows associating an object to a node lazily using a weak map.
     34 *
     35 * Classes deriving from this one may be easily converted to Custom Elements,
     36 * although they would lose the ability of being associated lazily.
     37 */
     38 var AssociatedToNode = class {
     39  constructor(node) {
     40    /**
     41     * Node associated to this object.
     42     */
     43    this.node = node;
     44 
     45    /**
     46     * This promise is resolved when the current set of blockers set by event
     47     * handlers have all been processed.
     48     */
     49    this._blockersPromise = Promise.resolve();
     50  }
     51 
     52  /**
     53   * Retrieves the instance associated with the given node, constructing a new
     54   * one if necessary. When the last reference to the node is released, the
     55   * object instance will be garbage collected as well.
     56   *
     57   * @param {DOMNode} node
     58   *   The node to retrieve or construct the AssociatedToNode instance for.
     59   * @returns {AssociatedToNode}
     60   */
     61  static forNode(node) {
     62    let associatedToNode = gNodeToObjectMap.get(node);
     63    if (!associatedToNode) {
     64      associatedToNode = new this(node);
     65      gNodeToObjectMap.set(node, associatedToNode);
     66    }
     67    return associatedToNode;
     68  }
     69 
     70  /**
     71   * A shortcut to the document that the node belongs to.
     72   *
     73   * @returns {Document}
     74   */
     75  get document() {
     76    return this.node.ownerDocument;
     77  }
     78 
     79  /**
     80   * A shortcut to the window global that the node belongs to.
     81   *
     82   * @returns {DOMWindow}
     83   */
     84  get window() {
     85    return this.node.ownerGlobal;
     86  }
     87 
     88  /**
     89   * A shortcut to windowUtils.getBoundsWithoutFlushing for the window global
     90   * associated with the node.
     91   *
     92   * This is a pseudo-private method using an `_` because we want it to be
     93   * used by subclasses. Please don't use outside of this module.
     94   *
     95   * @param {DOMNode} element
     96   *   The element to retrieve the bounds for without flushing layout.
     97   * @returns {DOMRect}
     98   *   The bounding rect of the element.
     99   */
    100  _getBoundsWithoutFlushing(element) {
    101    return this.window.windowUtils.getBoundsWithoutFlushing(element);
    102  }
    103 
    104  /**
    105   * Dispatches a custom event on this element.
    106   *
    107   * @param {string} eventName
    108   *   Name of the event to dispatch.
    109   * @param {object|undefined} [detail]
    110   *   Event detail object. Optional.
    111   * @param {boolean} cancelable
    112   *   True if the event can be canceled.
    113   * @returns {boolean}
    114   *   True if the event was canceled by an event handler, false otherwise.
    115   */
    116  dispatchCustomEvent(eventName, detail, cancelable = false) {
    117    let event = new this.window.CustomEvent(eventName, {
    118      detail,
    119      bubbles: true,
    120      cancelable,
    121    });
    122    this.node.dispatchEvent(event);
    123    return event.defaultPrevented;
    124  }
    125 
    126  /**
    127   * Dispatches a custom event on this element and waits for any blocking
    128   * promises registered using the "addBlocker" function on the details object.
    129   * If this function is called again, the event is only dispatched after all
    130   * the previously registered blockers have returned.
    131   *
    132   * The event can be canceled either by resolving any blocking promise to the
    133   * boolean value "false" or by calling preventDefault on the event. Rejections
    134   * and exceptions will be reported and will cancel the event.
    135   *
    136   * Blocking should be used sporadically because it slows down the interface.
    137   * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
    138   * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
    139   * This helps to prevent deadlocks if any of the event handlers does not
    140   * resolve a blocker promise.
    141   *
    142   * Note:
    143   *   Since there is no use case for dispatching different asynchronous
    144   *   events in parallel for the same element, this function will also wait
    145   *   for previous blockers when the event name is different.
    146   *
    147   * @param {string} eventName
    148   *   Name of the custom event to dispatch.
    149   * @returns {Promise<boolean>}
    150   *   Resolves to true if the event was canceled by a handler, false otherwise.
    151   */
    152  async dispatchAsyncEvent(eventName) {
    153    // Wait for all the previous blockers before dispatching the event.
    154    let blockersPromise = this._blockersPromise.catch(() => {});
    155    return (this._blockersPromise = blockersPromise.then(async () => {
    156      let blockers = new Set();
    157      let cancel = this.dispatchCustomEvent(
    158        eventName,
    159        {
    160          addBlocker(promise) {
    161            // Any exception in the blocker will cancel the operation.
    162            blockers.add(
    163              promise.catch(ex => {
    164                console.error(ex);
    165                return true;
    166              })
    167            );
    168          },
    169        },
    170        true
    171      );
    172      if (blockers.size) {
    173        let timeoutPromise = new Promise((resolve, reject) => {
    174          this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
    175        });
    176        try {
    177          let results = await Promise.race([
    178            Promise.all(blockers),
    179            timeoutPromise,
    180          ]);
    181          cancel = cancel || results.some(result => result === false);
    182        } catch (ex) {
    183          console.error(
    184            new Error(`One of the blockers for ${eventName} timed out.`)
    185          );
    186          return true;
    187        }
    188      }
    189      return cancel;
    190    }));
    191  }
    192 };
    193 
    194 /**
    195 * This is associated to <panelmultiview> elements.
    196 */
    197 export var PanelMultiView = class extends AssociatedToNode {
    198  /**
    199   * Tries to open the specified <panel> and displays the main view specified
    200   * with the "mainViewId" attribute on the <panelmultiview> node it contains.
    201   *
    202   * If the panel does not contain a <panelmultiview>, it is opened directly.
    203   * This allows consumers like page actions to accept different panel types.
    204   *
    205   * See the non-static openPopup method for details.
    206   *
    207   * @static
    208   * @memberof PanelMultiView
    209   * @param {DOMNode} panelNode
    210   *   The <panel> node that is to be opened.
    211   * @param {...*} args
    212   *   Additional arguments to be forwarded to the openPopup method of the
    213   *   panel.
    214   * @returns {Promise<boolean, Exception>|boolean}
    215   *   Returns a Promise that resolves to `true` if the panel successfully
    216   *   opened, or rejects if the panel cannot open. May also return `true`
    217   *   immediately if the panel does not contain a <panelmultiview>.
    218   */
    219  static async openPopup(panelNode, ...args) {
    220    let panelMultiViewNode = panelNode.querySelector("panelmultiview");
    221    if (panelMultiViewNode) {
    222      return this.forNode(panelMultiViewNode).openPopup(...args);
    223    }
    224    panelNode.openPopup(...args);
    225    return true;
    226  }
    227 
    228  /**
    229   * Closes the specified <panel> which contains a <panelmultiview> node.
    230   *
    231   * If the panel does not contain a <panelmultiview>, it is closed directly.
    232   * This allows consumers like page actions to accept different panel types.
    233   *
    234   * See the non-static hidePopup method for details.
    235   *
    236   * @static
    237   * @memberof PanelMultiView
    238   * @param {DOMNode} panelNode
    239   *   The <panel> node.
    240   * @param {boolean} [animate=false]
    241   *   Whether to show a fade animation.
    242   */
    243  static hidePopup(panelNode, animate = false) {
    244    let panelMultiViewNode = panelNode.querySelector("panelmultiview");
    245    if (panelMultiViewNode) {
    246      this.forNode(panelMultiViewNode).hidePopup(animate);
    247    } else {
    248      panelNode.hidePopup(animate);
    249    }
    250  }
    251 
    252  /**
    253   * Removes the specified <panel> from the document, ensuring that any
    254   * <panelmultiview> node it contains is destroyed properly.
    255   *
    256   * If the viewCacheId attribute is present on the <panelmultiview> element,
    257   * imported subviews will be moved out again to the element it specifies, so
    258   * that the panel element can be removed safely.
    259   *
    260   * If the panel does not contain a <panelmultiview>, it is removed directly.
    261   * This allows consumers like page actions to accept different panel types.
    262   *
    263   * @param {DOMNode} panelNode
    264   *   The <panel> to remove.
    265   */
    266  static removePopup(panelNode) {
    267    try {
    268      let panelMultiViewNode = panelNode.querySelector("panelmultiview");
    269      if (panelMultiViewNode) {
    270        let panelMultiView = this.forNode(panelMultiViewNode);
    271        panelMultiView._moveOutKids();
    272        panelMultiView.disconnect();
    273      }
    274    } finally {
    275      // Make sure to remove the panel element even if disconnecting fails.
    276      panelNode.remove();
    277    }
    278  }
    279 
    280  /**
    281   * Returns the element with the given id.
    282   * For nodes that are lazily loaded and not yet in the DOM, the node should
    283   * be retrieved from the view cache template.
    284   *
    285   * @param {Document} doc
    286   *   The document to retrieve the node for.
    287   * @param {string} id
    288   *   The ID of the element to retrieve from the DOM (or the
    289   *   appMenu-viewCache).
    290   * @returns {DOMNode|null}
    291   *   The found DOMNode or null if no node was found with that ID.
    292   */
    293  static getViewNode(doc, id) {
    294    let viewCacheTemplate = doc.getElementById("appMenu-viewCache");
    295 
    296    return (
    297      doc.getElementById(id) ||
    298      viewCacheTemplate?.content.querySelector("#" + id)
    299    );
    300  }
    301 
    302  /**
    303   * Ensures that when the specified window is closed all the <panelmultiview>
    304   * node it contains are destroyed properly.
    305   *
    306   * @param {DOMWindow} window
    307   *   The window to add the unload handler to.
    308   */
    309  static ensureUnloadHandlerRegistered(window) {
    310    if (gWindowsWithUnloadHandler.has(window)) {
    311      return;
    312    }
    313 
    314    window.addEventListener(
    315      "unload",
    316      () => {
    317        for (let panelMultiViewNode of window.document.querySelectorAll(
    318          "panelmultiview"
    319        )) {
    320          this.forNode(panelMultiViewNode).disconnect();
    321        }
    322      },
    323      { once: true }
    324    );
    325 
    326    gWindowsWithUnloadHandler.add(window);
    327  }
    328 
    329  /**
    330   * Returns the parent element of the <panelmultiview>, which should be a
    331   * <panel>.
    332   */
    333  get #panel() {
    334    return this.node.parentNode;
    335  }
    336 
    337  /**
    338   * Sets the `transitioning` attribute of the <panelmultiview>.
    339   *
    340   * @param {boolean} val
    341   *   If true, sets the attribute to `"true"`. If false, removes the attribute.
    342   */
    343  set #transitioning(val) {
    344    if (val) {
    345      this.node.setAttribute("transitioning", "true");
    346    } else {
    347      this.node.removeAttribute("transitioning");
    348    }
    349  }
    350 
    351  constructor(node) {
    352    super(node);
    353    this._openPopupPromise = Promise.resolve(false);
    354  }
    355 
    356  /**
    357   * Binds this PanelMultiView class to the underlying <panelmultiview> element.
    358   * Also creates the appropriate <panelmultview> child elements, like the
    359   * viewcontainer and the viewstack. Sets up popup event handlers for the
    360   * panel.
    361   */
    362  connect() {
    363    this.connected = true;
    364 
    365    PanelMultiView.ensureUnloadHandlerRegistered(this.window);
    366 
    367    let viewContainer = (this._viewContainer =
    368      this.document.createXULElement("box"));
    369    viewContainer.classList.add("panel-viewcontainer");
    370 
    371    let viewStack = (this._viewStack = this.document.createXULElement("box"));
    372    viewStack.classList.add("panel-viewstack");
    373    viewContainer.append(viewStack);
    374 
    375    let offscreenViewContainer = this.document.createXULElement("box");
    376    offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
    377 
    378    let offscreenViewStack = (this._offscreenViewStack =
    379      this.document.createXULElement("box"));
    380    offscreenViewStack.classList.add("panel-viewstack");
    381    offscreenViewContainer.append(offscreenViewStack);
    382 
    383    this.node.prepend(offscreenViewContainer);
    384    this.node.prepend(viewContainer);
    385 
    386    this.openViews = [];
    387 
    388    this.#panel.addEventListener("popupshowing", this);
    389    this.#panel.addEventListener("popuphidden", this);
    390    this.#panel.addEventListener("popupshown", this);
    391 
    392    // Proxy these public properties and methods, as used elsewhere by various
    393    // parts of the browser, to this instance.
    394    ["goBack", "showSubView"].forEach(method => {
    395      Object.defineProperty(this.node, method, {
    396        enumerable: true,
    397        value: (...args) => this[method](...args),
    398      });
    399    });
    400  }
    401 
    402  /**
    403   * Disconnects this PanelMultiView instance from a <panelmultiview> element.
    404   * This does not remove any of the nodes created in `connect`, but does clean
    405   * up the event listeners that were set up.
    406   */
    407  disconnect() {
    408    // Guard against re-entrancy.
    409    if (!this.node || !this.connected) {
    410      return;
    411    }
    412 
    413    this.#panel.removeEventListener("mousemove", this);
    414    this.#panel.removeEventListener("popupshowing", this);
    415    this.#panel.removeEventListener("popupshown", this);
    416    this.#panel.removeEventListener("popuphidden", this);
    417    this.document.documentElement.removeEventListener("keydown", this, true);
    418    this.node =
    419      this._openPopupPromise =
    420      this._openPopupCancelCallback =
    421      this._viewContainer =
    422      this._viewStack =
    423      this._transitionDetails =
    424        null;
    425  }
    426 
    427  /**
    428   * Tries to open the panel associated with this PanelMultiView, and displays
    429   * the main view specified with the "mainViewId" attribute.
    430   *
    431   * The hidePopup method can be called while the operation is in progress to
    432   * prevent the panel from being displayed. View events may also cancel the
    433   * operation, so there is no guarantee that the panel will become visible.
    434   *
    435   * The "popuphidden" event will be fired either when the operation is canceled
    436   * or when the popup is closed later. This event can be used for example to
    437   * reset the "open" state of the anchor or tear down temporary panels.
    438   *
    439   * If this method is called again before the panel is shown, the result
    440   * depends on the operation currently in progress. If the operation was not
    441   * canceled, the panel is opened using the arguments from the previous call,
    442   * and this call is ignored. If the operation was canceled, it will be
    443   * retried again using the arguments from this call.
    444   *
    445   * It's not necessary for the <panelmultiview> binding to be connected when
    446   * this method is called, but the containing panel must have its display
    447   * turned on, for example it shouldn't have the "hidden" attribute.
    448   *
    449   * @instance
    450   * @memberof PanelMultiView#
    451   * @param {DOMNode} anchor
    452   *   The node to anchor the popup to.
    453   * @param {StringOrOpenPopupOptions} options
    454   *   Either options to use or a string position. This is forwarded to
    455   *   the openPopup method of the panel.
    456   * @param {...*} args
    457   *   Additional arguments to be forwarded to the openPopup method of the
    458   *   panel.
    459   * @returns {Promise<boolean, Exception>}
    460   *   Resolves with true as soon as the request to display the panel has been
    461   *   sent, or with false if the operation was canceled. The state of
    462   *   the panel at this point is not guaranteed. It may be still
    463   *   showing, completely shown, or completely hidden.
    464   *
    465   *   Rejects if an exception is thrown at any point in the process before the
    466   *   request to display the panel is sent.
    467   */
    468  async openPopup(anchor, options, ...args) {
    469    // Set up the function that allows hidePopup or a second call to showPopup
    470    // to cancel the specific panel opening operation that we're starting below.
    471    // This function must be synchronous, meaning we can't use Promise.race,
    472    // because hidePopup wants to dispatch the "popuphidden" event synchronously
    473    // even if the panel has not been opened yet.
    474    let canCancel = true;
    475    let cancelCallback = (this._openPopupCancelCallback = () => {
    476      // If the cancel callback is called and the panel hasn't been prepared
    477      // yet, cancel showing it. Setting canCancel to false will prevent the
    478      // popup from opening. If the panel has opened by the time the cancel
    479      // callback is called, canCancel will be false already, and we will not
    480      // fire the "popuphidden" event.
    481      if (canCancel && this.node) {
    482        canCancel = false;
    483        this.dispatchCustomEvent("popuphidden");
    484      }
    485      if (cancelCallback == this._openPopupCancelCallback) {
    486        // If still current, let go of the cancel callback since it will capture
    487        // the entire scope and tie it to the main window.
    488        delete this._openPopupCancelCallback;
    489      }
    490    });
    491 
    492    // Create a promise that is resolved with the result of the last call to
    493    // this method, where errors indicate that the panel was not opened.
    494    let openPopupPromise = this._openPopupPromise.catch(() => {
    495      return false;
    496    });
    497 
    498    // Make the preparation done before showing the panel non-reentrant. The
    499    // promise created here will be resolved only after the panel preparation is
    500    // completed, even if a cancellation request is received in the meantime.
    501    return (this._openPopupPromise = openPopupPromise.then(async wasShown => {
    502      // The panel may have been destroyed in the meantime.
    503      if (!this.node) {
    504        return false;
    505      }
    506      // If the panel has been already opened there is nothing more to do. We
    507      // check the actual state of the panel rather than setting some state in
    508      // our handler of the "popuphidden" event because this has a lower chance
    509      // of locking indefinitely if events aren't raised in the expected order.
    510      if (wasShown && ["open", "showing"].includes(this.#panel.state)) {
    511        if (cancelCallback == this._openPopupCancelCallback) {
    512          // If still current, let go of the cancel callback since it will
    513          // capture the entire scope and tie it to the main window.
    514          delete this._openPopupCancelCallback;
    515        }
    516        return true;
    517      }
    518      try {
    519        if (!this.connected) {
    520          this.connect();
    521        }
    522        // Allow any of the ViewShowing handlers to prevent showing the main view.
    523        if (!(await this.#showMainView())) {
    524          cancelCallback();
    525        }
    526      } catch (ex) {
    527        cancelCallback();
    528        throw ex;
    529      }
    530      // If a cancellation request was received there is nothing more to do.
    531      if (!canCancel || !this.node) {
    532        return false;
    533      }
    534      // We have to set canCancel to false before opening the popup because the
    535      // hidePopup method of PanelMultiView can be re-entered by event handlers.
    536      // If the openPopup call fails, however, we still have to dispatch the
    537      // "popuphidden" event even if canCancel was set to false.
    538      try {
    539        canCancel = false;
    540        this.#panel.openPopup(anchor, options, ...args);
    541        if (cancelCallback == this._openPopupCancelCallback) {
    542          // If still current, let go of the cancel callback since it will
    543          // capture the entire scope and tie it to the main window.
    544          delete this._openPopupCancelCallback;
    545        }
    546        // Set an attribute on the popup to let consumers style popup elements -
    547        // for example, the anchor arrow is styled to match the color of the header
    548        // in the Protections Panel main view.
    549        this.#panel.setAttribute("mainviewshowing", true);
    550 
    551        // On Windows, if another popup is hiding while we call openPopup, the
    552        // call won't fail but the popup won't open. In this case, we have to
    553        // dispatch an artificial "popuphidden" event to reset our state.
    554        if (this.#panel.state == "closed" && this.openViews.length) {
    555          this.dispatchCustomEvent("popuphidden");
    556          return false;
    557        }
    558 
    559        if (
    560          options &&
    561          typeof options == "object" &&
    562          options.triggerEvent &&
    563          (options.triggerEvent.type == "keypress" ||
    564            options.triggerEvent.type == "keydown" ||
    565            options.triggerEvent?.inputSource ==
    566              MouseEvent.MOZ_SOURCE_KEYBOARD) &&
    567          this.openViews.length
    568        ) {
    569          // This was opened via the keyboard, so focus the first item.
    570          this.openViews[0].focusWhenActive = true;
    571        }
    572 
    573        return true;
    574      } catch (ex) {
    575        this.dispatchCustomEvent("popuphidden");
    576        throw ex;
    577      }
    578    }));
    579  }
    580 
    581  /**
    582   * Closes the panel associated with this PanelMultiView.
    583   *
    584   * If the openPopup method was called but the panel has not been displayed
    585   * yet, the operation is canceled and the panel will not be displayed, but the
    586   * "popuphidden" event is fired synchronously anyways.
    587   *
    588   * This means that by the time this method returns all the operations handled
    589   * by the "popuphidden" event are completed, for example resetting the "open"
    590   * state of the anchor, and the panel is already invisible.
    591   *
    592   * Note:
    593   *   The value of animate could be changed to true by default, in both
    594   *   this and the static method above. (see bug 1769813)
    595   *
    596   * @instance
    597   * @memberof PanelMultiView#
    598   * @param {boolean} [animate=false]
    599   *   Whether to show a fade animation. Optional.
    600   */
    601  hidePopup(animate = false) {
    602    if (!this.node || !this.connected) {
    603      return;
    604    }
    605 
    606    // If we have already reached the #panel.openPopup call in the openPopup
    607    // method, we can call hidePopup. Otherwise, we have to cancel the latest
    608    // request to open the panel, which will have no effect if the request has
    609    // been canceled already.
    610    if (["open", "showing"].includes(this.#panel.state)) {
    611      this.#panel.hidePopup(animate);
    612    } else {
    613      this._openPopupCancelCallback?.();
    614    }
    615 
    616    // We close all the views synchronously, so that they are ready to be opened
    617    // in other PanelMultiView instances. The "popuphidden" handler may also
    618    // call this function, but the second time openViews will be empty.
    619    this.closeAllViews();
    620  }
    621 
    622  /**
    623   * Move any child subviews into the element defined by "viewCacheId" to make
    624   * sure they will not be removed together with the <panelmultiview> element.
    625   *
    626   * This is "pseudo-private" with the underscore so that the static
    627   * removePopup method can call it.
    628   */
    629  _moveOutKids() {
    630    // this.node may have been set to null by a call to disconnect().
    631    let viewCacheId = this.node?.getAttribute("viewCacheId");
    632    if (!viewCacheId) {
    633      return;
    634    }
    635 
    636    // Node.children and Node.children is live to DOM changes like the
    637    // ones we're about to do, so iterate over a static copy:
    638    let subviews = Array.from(this._viewStack.children);
    639    let viewCache = this.document.getElementById("appMenu-viewCache");
    640    for (let subview of subviews) {
    641      viewCache.appendChild(subview);
    642    }
    643  }
    644 
    645  /**
    646   * Slides in the specified view as a subview. This returns synchronously,
    647   * but may eventually log an error if showing the subview fails for some
    648   * reason.
    649   *
    650   * @param {string|DOMNode} viewIdOrNode
    651   *   DOM element or string ID of the <panelview> to display.
    652   * @param {DOMNode} anchor
    653   *   DOM element that triggered the subview, which will be highlighted
    654   *   and whose "label" attribute will be used for the title of the
    655   *   subview when a "title" attribute is not specified.
    656   */
    657  showSubView(viewIdOrNode, anchor) {
    658    this.#showSubView(viewIdOrNode, anchor).catch(console.error);
    659  }
    660 
    661  /**
    662   * The asynchronous private helper method for showSubView that does most of
    663   * the heavy lifting.
    664   *
    665   * @param {string|DOMNode} viewIdOrNode
    666   *   DOM element or string ID of the <panelview> to display.
    667   * @param {DOMNode} anchor
    668   *   DOM element that triggered the subview, which will be highlighted
    669   *   and whose "label" attribute will be used for the title of the
    670   *   subview when a "title" attribute is not specified.
    671   * @returns {Promise<undefined>}
    672   *   Returns a Promise that resolves when attempting to show the subview
    673   *   completes.
    674   */
    675  async #showSubView(viewIdOrNode, anchor) {
    676    let viewNode =
    677      typeof viewIdOrNode == "string"
    678        ? PanelMultiView.getViewNode(this.document, viewIdOrNode)
    679        : viewIdOrNode;
    680    if (!viewNode) {
    681      console.error(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
    682      return;
    683    }
    684 
    685    if (!this.openViews.length) {
    686      console.error(new Error(`Cannot show a subview in a closed panel.`));
    687      return;
    688    }
    689 
    690    let prevPanelView = this.openViews[this.openViews.length - 1];
    691    let nextPanelView = PanelView.forNode(viewNode);
    692    if (this.openViews.includes(nextPanelView)) {
    693      console.error(new Error(`Subview ${viewNode.id} is already open.`));
    694      return;
    695    }
    696 
    697    // Do not re-enter the process if navigation is already in progress. Since
    698    // there is only one active view at any given time, we can do this check
    699    // safely, even considering that during the navigation process the actual
    700    // view to which prevPanelView refers will change.
    701    if (!prevPanelView.active) {
    702      return;
    703    }
    704    // If prevPanelView._doingKeyboardActivation is true, it will be reset to
    705    // false synchronously. Therefore, we must capture it before we use any
    706    // "await" statements.
    707    let doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
    708    // Marking the view that is about to scrolled out of the visible area as
    709    // inactive will prevent re-entrancy and also disable keyboard navigation.
    710    // From this point onwards, "await" statements can be used safely.
    711    prevPanelView.active = false;
    712 
    713    // Provide visual feedback while navigation is in progress, starting before
    714    // the transition starts and ending when the previous view is invisible.
    715    anchor?.setAttribute("open", "true");
    716    try {
    717      // If the ViewShowing event cancels the operation we have to re-enable
    718      // keyboard navigation, but this must be avoided if the panel was closed.
    719      if (!(await this.#openView(nextPanelView))) {
    720        if (prevPanelView.isOpenIn(this)) {
    721          // We don't raise a ViewShown event because nothing actually changed.
    722          // Technically we should use a different state flag just because there
    723          // is code that could check the "active" property to determine whether
    724          // to wait for a ViewShown event later, but this only happens in
    725          // regression tests and is less likely to be a technique used in
    726          // production code, where use of ViewShown is less common.
    727          prevPanelView.active = true;
    728        }
    729        return;
    730      }
    731 
    732      prevPanelView.captureKnownSize();
    733 
    734      // The main view of a panel can be a subview in another one. Make sure to
    735      // reset all the properties that may be set on a subview.
    736      nextPanelView.mainview = false;
    737      // The header may be set by a Fluent message with a title attribute
    738      // that has changed immediately before showing the panelview,
    739      // and so is not reflected in the DOM yet.
    740      let title;
    741      const l10nId = viewNode.getAttribute("data-l10n-id");
    742      if (l10nId) {
    743        const l10nArgs = viewNode.getAttribute("data-l10n-args");
    744        const args = l10nArgs ? JSON.parse(l10nArgs) : undefined;
    745        const [msg] = await viewNode.ownerDocument.l10n.formatMessages([
    746          { id: l10nId, args },
    747        ]);
    748        title = msg.attributes.find(a => a.name === "title")?.value;
    749      }
    750      // If not set by Fluent, the header may change based on how the subview was opened.
    751      title ??= viewNode.getAttribute("title") || anchor?.getAttribute("label");
    752      nextPanelView.headerText = title;
    753      // The constrained width of subviews may also vary between panels.
    754      nextPanelView.minMaxWidth = prevPanelView.knownWidth;
    755      let lockPanelVertical =
    756        this.openViews[0].node.getAttribute("lockpanelvertical") == "true";
    757      nextPanelView.minMaxHeight = lockPanelVertical
    758        ? prevPanelView.knownHeight
    759        : 0;
    760 
    761      if (anchor) {
    762        viewNode.classList.add("PanelUI-subView");
    763      }
    764 
    765      await this.#transitionViews(prevPanelView.node, viewNode, false);
    766    } finally {
    767      anchor?.removeAttribute("open");
    768    }
    769 
    770    nextPanelView.focusWhenActive = doingKeyboardActivation;
    771    this.#activateView(nextPanelView);
    772  }
    773 
    774  /**
    775   * Navigates backwards by sliding out the most recent subview.
    776   */
    777  goBack() {
    778    this.#goBack().catch(console.error);
    779  }
    780 
    781  /**
    782   * The asynchronous helper method for goBack that does most of the heavy
    783   * lifting.
    784   *
    785   * @returns {Promise<undefined>}
    786   *   Resolves when attempting to go back completes.
    787   */
    788  async #goBack() {
    789    if (this.openViews.length < 2) {
    790      // This may be called by keyboard navigation or external code when only
    791      // the main view is open.
    792      return;
    793    }
    794 
    795    let prevPanelView = this.openViews[this.openViews.length - 1];
    796    let nextPanelView = this.openViews[this.openViews.length - 2];
    797 
    798    // Like in the showSubView method, do not re-enter navigation while it is
    799    // in progress, and make the view inactive immediately. From this point
    800    // onwards, "await" statements can be used safely.
    801    if (!prevPanelView.active) {
    802      return;
    803    }
    804    prevPanelView.active = false;
    805 
    806    prevPanelView.captureKnownSize();
    807    await this.#transitionViews(prevPanelView.node, nextPanelView.node, true);
    808 
    809    this.#closeLatestView();
    810 
    811    this.#activateView(nextPanelView);
    812  }
    813 
    814  /**
    815   * Prepares the main view before showing the panel.
    816   *
    817   * @returns {boolean}
    818   *   Returns true if showing the main view succeeds.
    819   */
    820  async #showMainView() {
    821    let nextPanelView = PanelView.forNode(
    822      PanelMultiView.getViewNode(
    823        this.document,
    824        this.node.getAttribute("mainViewId")
    825      )
    826    );
    827 
    828    // If the view is already open in another panel, close the panel first.
    829    let oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
    830    if (oldPanelMultiViewNode) {
    831      PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
    832      // Wait for a layout flush after hiding the popup, otherwise the view may
    833      // not be displayed correctly for some time after the new panel is opened.
    834      // This is filed as bug 1441015.
    835      await this.window.promiseDocumentFlushed(() => {});
    836    }
    837 
    838    if (!(await this.#openView(nextPanelView))) {
    839      return false;
    840    }
    841 
    842    // The main view of a panel can be a subview in another one. Make sure to
    843    // reset all the properties that may be set on a subview.
    844    nextPanelView.mainview = true;
    845    nextPanelView.headerText = "";
    846    nextPanelView.minMaxWidth = 0;
    847    nextPanelView.minMaxHeight = 0;
    848 
    849    // Ensure the view will be visible once the panel is opened.
    850    nextPanelView.visible = true;
    851 
    852    return true;
    853  }
    854 
    855  /**
    856   * Opens the specified PanelView and dispatches the ViewShowing event, which
    857   * can be used to populate the subview or cancel the operation.
    858   *
    859   * This also clears all the attributes and styles that may be left by a
    860   * transition that was interrupted.
    861   *
    862   * @param {DOMNode} panelView
    863   *   The <panelview> element to show.
    864   * @returns {Promise<boolean>}
    865   *   Resolves with true if the view was opened, false otherwise.
    866   */
    867  async #openView(panelView) {
    868    if (panelView.node.parentNode != this._viewStack) {
    869      this._viewStack.appendChild(panelView.node);
    870    }
    871 
    872    panelView.node.panelMultiView = this.node;
    873    this.openViews.push(panelView);
    874 
    875    // Panels could contain out-pf-process <browser> elements, that need to be
    876    // supported with a remote attribute on the panel in order to display properly.
    877    // See bug https://bugzilla.mozilla.org/show_bug.cgi?id=1365660
    878    if (panelView.node.getAttribute("remote") == "true") {
    879      this.#panel.setAttribute("remote", "true");
    880    }
    881 
    882    let canceled = await panelView.dispatchAsyncEvent("ViewShowing");
    883 
    884    // The panel can be hidden while we are processing the ViewShowing event.
    885    // This results in all the views being closed synchronously, and at this
    886    // point the ViewHiding event has already been dispatched for all of them.
    887    if (!this.openViews.length) {
    888      return false;
    889    }
    890 
    891    // Check if the event requested cancellation but the panel is still open.
    892    if (canceled) {
    893      // Handlers for ViewShowing can't know if a different handler requested
    894      // cancellation, so this will dispatch a ViewHiding event to give a chance
    895      // to clean up.
    896      this.#closeLatestView();
    897      return false;
    898    }
    899 
    900    // Clean up all the attributes and styles related to transitions. We do this
    901    // here rather than when the view is closed because we are likely to make
    902    // other DOM modifications soon, which isn't the case when closing.
    903    let { style } = panelView.node;
    904    style.removeProperty("outline");
    905    style.removeProperty("width");
    906 
    907    return true;
    908  }
    909 
    910  /**
    911   * Activates the specified view and raises the ViewShown event, unless the
    912   * view was closed in the meantime.
    913   *
    914   * @param {DOMNode} panelView
    915   *   The <panelview> to be activated.
    916   */
    917  #activateView(panelView) {
    918    if (panelView.isOpenIn(this)) {
    919      panelView.active = true;
    920      if (panelView.focusWhenActive) {
    921        panelView.focusFirstNavigableElement(false, true);
    922        panelView.focusWhenActive = false;
    923      }
    924      panelView.dispatchCustomEvent("ViewShown");
    925    }
    926  }
    927 
    928  /**
    929   * Closes the most recent PanelView and raises the ViewHiding event.
    930   *
    931   * Note:
    932   *   The ViewHiding event is not cancelable and should probably be renamed
    933   *   to ViewHidden or ViewClosed instead, see bug 1438507.
    934   */
    935  #closeLatestView() {
    936    let panelView = this.openViews.pop();
    937    panelView.clearNavigation();
    938    panelView.dispatchCustomEvent("ViewHiding");
    939    panelView.node.panelMultiView = null;
    940    // Views become invisible synchronously when they are closed, and they won't
    941    // become visible again until they are opened. When this is called at the
    942    // end of backwards navigation, the view is already invisible.
    943    panelView.visible = false;
    944  }
    945 
    946  /**
    947   * Closes all the views that are currently open.
    948   */
    949  closeAllViews() {
    950    // Raise ViewHiding events for open views in reverse order.
    951    while (this.openViews.length) {
    952      this.#closeLatestView();
    953    }
    954  }
    955 
    956  /**
    957   * Apply a transition to 'slide' from the currently active view to the next
    958   * one.
    959   * Sliding the next subview in means that the previous panelview stays where
    960   * it is and the active panelview slides in from the left in LTR mode, right
    961   * in RTL mode.
    962   *
    963   * @param {DOMNode} previousViewNode
    964   *   The panelview node that is currently displayed, but is about to be
    965   *   transitioned away. This must be already inactive at this point.
    966   * @param {DOMNode} viewNode
    967   *   The panelview node that will becode the active view after the transition
    968   *   has finished.
    969   * @param {boolean} reverse
    970   *   Whether we're navigation back to a previous view or forward to a next
    971   *   view.
    972   */
    973  async #transitionViews(previousViewNode, viewNode, reverse) {
    974    const { window } = this;
    975 
    976    let nextPanelView = PanelView.forNode(viewNode);
    977    let prevPanelView = PanelView.forNode(previousViewNode);
    978 
    979    let details = (this._transitionDetails = {
    980      phase: TRANSITION_PHASES.START,
    981    });
    982 
    983    // Set the viewContainer dimensions to make sure only the current view is
    984    // visible.
    985    let olderView = reverse ? nextPanelView : prevPanelView;
    986    this._viewContainer.style.minHeight = olderView.knownHeight + "px";
    987    this._viewContainer.style.height = prevPanelView.knownHeight + "px";
    988    this._viewContainer.style.width = prevPanelView.knownWidth + "px";
    989    // Lock the dimensions of the window that hosts the popup panel.
    990    let rect = this._getBoundsWithoutFlushing(this.#panel);
    991    this.#panel.style.width = rect.width + "px";
    992    this.#panel.style.height = rect.height + "px";
    993 
    994    let viewRect;
    995    if (reverse) {
    996      // Use the cached size when going back to a previous view, but not when
    997      // reopening a subview, because its contents may have changed.
    998      viewRect = {
    999        width: nextPanelView.knownWidth,
   1000        height: nextPanelView.knownHeight,
   1001      };
   1002      nextPanelView.visible = true;
   1003    } else if (viewNode.customRectGetter) {
   1004      // We use a customRectGetter for WebExtensions panels, because they need
   1005      // to query the size from an embedded browser. The presence of this
   1006      // getter also provides an indication that the view node shouldn't be
   1007      // moved around, otherwise the state of the browser would get disrupted.
   1008      let width = prevPanelView.knownWidth;
   1009      let height = prevPanelView.knownHeight;
   1010      viewRect = Object.assign({ height, width }, viewNode.customRectGetter());
   1011      nextPanelView.visible = true;
   1012      // Until the header is visible, it has 0 height.
   1013      // Wait for layout before measuring it
   1014      let header = viewNode.firstElementChild;
   1015      if (header && header.classList.contains("panel-header")) {
   1016        viewRect.height += await window.promiseDocumentFlushed(() => {
   1017          return this._getBoundsWithoutFlushing(header).height;
   1018        });
   1019      }
   1020      // Bail out if the panel was closed in the meantime.
   1021      if (!nextPanelView.isOpenIn(this)) {
   1022        return;
   1023      }
   1024    } else {
   1025      this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
   1026      this._offscreenViewStack.appendChild(viewNode);
   1027      nextPanelView.visible = true;
   1028 
   1029      viewRect = await window.promiseDocumentFlushed(() => {
   1030        return this._getBoundsWithoutFlushing(viewNode);
   1031      });
   1032      // Bail out if the panel was closed in the meantime.
   1033      if (!nextPanelView.isOpenIn(this)) {
   1034        return;
   1035      }
   1036 
   1037      // Place back the view after all the other views that are already open in
   1038      // order for the transition to work as expected.
   1039      this._viewStack.appendChild(viewNode);
   1040 
   1041      this._offscreenViewStack.style.removeProperty("min-height");
   1042    }
   1043 
   1044    this.#transitioning = true;
   1045    details.phase = TRANSITION_PHASES.PREPARE;
   1046 
   1047    // The 'magic' part: build up the amount of pixels to move right or left.
   1048    let moveToLeft =
   1049      (this.window.RTL_UI && !reverse) || (!this.window.RTL_UI && reverse);
   1050    let deltaX = prevPanelView.knownWidth;
   1051    let deepestNode = reverse ? previousViewNode : viewNode;
   1052 
   1053    // With a transition when navigating backwards - user hits the 'back'
   1054    // button - we need to make sure that the views are positioned in a way
   1055    // that a translateX() unveils the previous view from the right direction.
   1056    if (reverse) {
   1057      this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
   1058    }
   1059 
   1060    // Set the transition style and listen for its end to clean up and make sure
   1061    // the box sizing becomes dynamic again.
   1062    // Somehow, putting these properties in PanelUI.css doesn't work for newly
   1063    // shown nodes in a XUL parent node.
   1064    this._viewStack.style.transition =
   1065      "transform var(--animation-easing-function)" +
   1066      " var(--panelui-subview-transition-duration)";
   1067    this._viewStack.style.willChange = "transform";
   1068    // Use an outline instead of a border so that the size is not affected.
   1069    deepestNode.style.outline = "1px solid var(--panel-separator-color)";
   1070 
   1071    // Now that all the elements are in place for the start of the transition,
   1072    // give the layout code a chance to set the initial values.
   1073    await window.promiseDocumentFlushed(() => {});
   1074    // Bail out if the panel was closed in the meantime.
   1075    if (!nextPanelView.isOpenIn(this)) {
   1076      return;
   1077    }
   1078 
   1079    // Now set the viewContainer dimensions to that of the new view, which
   1080    // kicks of the height animation.
   1081    this._viewContainer.style.height = viewRect.height + "px";
   1082    this._viewContainer.style.width = viewRect.width + "px";
   1083    this.#panel.style.removeProperty("width");
   1084    this.#panel.style.removeProperty("height");
   1085    // We're setting the width property to prevent flickering during the
   1086    // sliding animation with smaller views.
   1087    viewNode.style.width = viewRect.width + "px";
   1088 
   1089    // Kick off the transition!
   1090    details.phase = TRANSITION_PHASES.TRANSITION;
   1091 
   1092    // If we're going to show the main view, we can remove the
   1093    // min-height property on the view container. It's also time
   1094    // to set the mainviewshowing attribute on the popup.
   1095    if (viewNode.getAttribute("mainview")) {
   1096      this._viewContainer.style.removeProperty("min-height");
   1097      this.#panel.setAttribute("mainviewshowing", true);
   1098    } else {
   1099      this.#panel.removeAttribute("mainviewshowing");
   1100    }
   1101 
   1102    // Avoid transforming element if the user has prefers-reduced-motion set
   1103    if (
   1104      this.window.matchMedia("(prefers-reduced-motion: no-preference)")
   1105        .matches &&
   1106      !viewNode.getAttribute("no-panelview-transition")
   1107    ) {
   1108      this._viewStack.style.transform =
   1109        "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
   1110 
   1111      await new Promise(resolve => {
   1112        details.resolve = resolve;
   1113        this._viewContainer.addEventListener(
   1114          "transitionend",
   1115          (details.listener = ev => {
   1116            // It's quite common that `height` on the view container doesn't need
   1117            // to transition, so we make sure to do all the work on the transform
   1118            // transition-end, because that is guaranteed to happen.
   1119            if (
   1120              ev.target != this._viewStack ||
   1121              ev.propertyName != "transform"
   1122            ) {
   1123              return;
   1124            }
   1125            this._viewContainer.removeEventListener(
   1126              "transitionend",
   1127              details.listener
   1128            );
   1129            delete details.listener;
   1130            resolve();
   1131          })
   1132        );
   1133        this._viewContainer.addEventListener(
   1134          "transitioncancel",
   1135          (details.cancelListener = ev => {
   1136            if (ev.target != this._viewStack) {
   1137              return;
   1138            }
   1139            this._viewContainer.removeEventListener(
   1140              "transitioncancel",
   1141              details.cancelListener
   1142            );
   1143            delete details.cancelListener;
   1144            resolve();
   1145          })
   1146        );
   1147      });
   1148    }
   1149 
   1150    // Bail out if the panel was closed during the transition.
   1151    if (!nextPanelView.isOpenIn(this)) {
   1152      return;
   1153    }
   1154    prevPanelView.visible = false;
   1155 
   1156    // This will complete the operation by removing any transition properties.
   1157    nextPanelView.node.style.removeProperty("width");
   1158    deepestNode.style.removeProperty("outline");
   1159    this.#cleanupTransitionPhase();
   1160    // Ensure the newly-visible view has been through a layout flush before we
   1161    // attempt to focus anything in it.
   1162    // See https://firefox-source-docs.mozilla.org/performance/bestpractices.html#detecting-and-avoiding-synchronous-reflow
   1163    // for more information.
   1164    await this.window.promiseDocumentFlushed(() => {});
   1165    nextPanelView.focusSelectedElement();
   1166  }
   1167 
   1168  /**
   1169   * Attempt to clean up the attributes and properties set by `#transitionViews`
   1170   * above. Which attributes and properties depends on the phase the transition
   1171   * was left from.
   1172   */
   1173  #cleanupTransitionPhase() {
   1174    if (!this._transitionDetails) {
   1175      return;
   1176    }
   1177 
   1178    let { phase, resolve, listener, cancelListener } = this._transitionDetails;
   1179    this._transitionDetails = null;
   1180 
   1181    if (phase >= TRANSITION_PHASES.START) {
   1182      this.#panel.removeAttribute("width");
   1183      this.#panel.removeAttribute("height");
   1184      this._viewContainer.style.removeProperty("height");
   1185      this._viewContainer.style.removeProperty("width");
   1186    }
   1187    if (phase >= TRANSITION_PHASES.PREPARE) {
   1188      this.#transitioning = false;
   1189      this._viewStack.style.removeProperty("margin-inline-start");
   1190      this._viewStack.style.removeProperty("transition");
   1191    }
   1192    if (phase >= TRANSITION_PHASES.TRANSITION) {
   1193      this._viewStack.style.removeProperty("transform");
   1194      if (listener) {
   1195        this._viewContainer.removeEventListener("transitionend", listener);
   1196      }
   1197      if (cancelListener) {
   1198        this._viewContainer.removeEventListener(
   1199          "transitioncancel",
   1200          cancelListener
   1201        );
   1202      }
   1203      if (resolve) {
   1204        resolve();
   1205      }
   1206    }
   1207  }
   1208 
   1209  /**
   1210   * Centralized event handler for the associated <panelmultiview>.
   1211   *
   1212   * @param {Event} aEvent
   1213   *   The event being handled.
   1214   */
   1215  handleEvent(aEvent) {
   1216    // Only process actual popup events from the panel or events we generate
   1217    // ourselves, but not from menus being shown from within the panel.
   1218    if (
   1219      aEvent.type.startsWith("popup") &&
   1220      aEvent.target != this.#panel &&
   1221      aEvent.target != this.node
   1222    ) {
   1223      return;
   1224    }
   1225    switch (aEvent.type) {
   1226      case "keydown": {
   1227        // Since we start listening for the "keydown" event when the popup is
   1228        // already showing and stop listening when the panel is hidden, we
   1229        // always have at least one view open.
   1230        let currentView = this.openViews[this.openViews.length - 1];
   1231        currentView.keyNavigation(aEvent);
   1232        break;
   1233      }
   1234      case "mousemove": {
   1235        this.openViews.forEach(panelView => {
   1236          if (!panelView.ignoreMouseMove) {
   1237            panelView.clearNavigation();
   1238          }
   1239        });
   1240        break;
   1241      }
   1242      case "popupshowing": {
   1243        this._viewContainer.setAttribute("panelopen", "true");
   1244        if (!this.node.hasAttribute("disablekeynav")) {
   1245          // We add the keydown handler on the root so that it handles key
   1246          // presses when a panel appears but doesn't get focus, as happens
   1247          // when a button to open a panel is clicked with the mouse.
   1248          // However, this means the listener is on an ancestor of the panel,
   1249          // which means that handlers such as ToolbarKeyboardNavigator are
   1250          // deeper in the tree. Therefore, this must be a capturing listener
   1251          // so we get the event first.
   1252          this.document.documentElement.addEventListener("keydown", this, true);
   1253          this.#panel.addEventListener("mousemove", this);
   1254        }
   1255        break;
   1256      }
   1257      case "popupshown": {
   1258        // The main view is always open and visible when the panel is first
   1259        // shown, so we can check the height of the description elements it
   1260        // contains and notify consumers using the ViewShown event. In order to
   1261        // minimize flicker we need to allow synchronous reflows, and we still
   1262        // make sure the ViewShown event is dispatched synchronously.
   1263        let mainPanelView = this.openViews[0];
   1264        this.#activateView(mainPanelView);
   1265        break;
   1266      }
   1267      case "popuphidden": {
   1268        // WebExtensions consumers can hide the popup from viewshowing, or
   1269        // mid-transition, which disrupts our state:
   1270        this.#transitioning = false;
   1271        this._viewContainer.removeAttribute("panelopen");
   1272        this.#cleanupTransitionPhase();
   1273        this.document.documentElement.removeEventListener(
   1274          "keydown",
   1275          this,
   1276          true
   1277        );
   1278        this.#panel.removeEventListener("mousemove", this);
   1279        this.closeAllViews();
   1280 
   1281        // Clear the main view size caches. The dimensions could be different
   1282        // when the popup is opened again, e.g. through touch mode sizing.
   1283        this._viewContainer.style.removeProperty("min-height");
   1284        this._viewStack.style.removeProperty("max-height");
   1285        this._viewContainer.style.removeProperty("width");
   1286        this._viewContainer.style.removeProperty("height");
   1287 
   1288        this.dispatchCustomEvent("PanelMultiViewHidden");
   1289        break;
   1290      }
   1291    }
   1292  }
   1293 };
   1294 
   1295 /**
   1296 * This is associated to <panelview> elements.
   1297 */
   1298 export var PanelView = class extends AssociatedToNode {
   1299  constructor(node) {
   1300    super(node);
   1301 
   1302    /**
   1303     * Indicates whether the view is active. When this is false, consumers can
   1304     * wait for the ViewShown event to know when the view becomes active.
   1305     */
   1306    this.active = false;
   1307 
   1308    /**
   1309     * Specifies whether the view should be focused when active. When this
   1310     * is true, the first navigable element in the view will be focused
   1311     * when the view becomes active. This should be set to true when the view
   1312     * is activated from the keyboard. It will be set to false once the view
   1313     * is active.
   1314     */
   1315    this.focusWhenActive = false;
   1316  }
   1317 
   1318  /**
   1319   * Indicates whether the view is open in the specified PanelMultiView object.
   1320   *
   1321   * @param {PanelMultiView} panelMultiView
   1322   *   The PanelMultiView instance to check.
   1323   * @returns {boolean}
   1324   *   True if the PanelView is open in the specified PanelMultiView object.
   1325   */
   1326  isOpenIn(panelMultiView) {
   1327    return this.node.panelMultiView == panelMultiView.node;
   1328  }
   1329 
   1330  /**
   1331   * The "mainview" attribute is set before the panel is opened when this view
   1332   * is displayed as the main view, and is removed before the <panelview> is
   1333   * displayed as a subview. The same view element can be displayed as a main
   1334   * view and as a subview at different times.
   1335   *
   1336   * @param {boolean} value
   1337   *   True to set the `"mainview"` attribute to `true`, otherwise removes the
   1338   *   attribute.
   1339   */
   1340  set mainview(value) {
   1341    if (value) {
   1342      this.node.setAttribute("mainview", true);
   1343    } else {
   1344      this.node.removeAttribute("mainview");
   1345    }
   1346  }
   1347 
   1348  /**
   1349   * Determines whether the view is visible. Setting this to false also resets
   1350   * the "active" property.
   1351   *
   1352   * @param {boolean} value
   1353   *   True to set the `"visible"` attribute to `true`, otherwise removes the
   1354   *   attribute, and sets active and focusWhenActive to `false`.
   1355   */
   1356  set visible(value) {
   1357    if (value) {
   1358      this.node.setAttribute("visible", true);
   1359    } else {
   1360      this.node.removeAttribute("visible");
   1361      this.active = false;
   1362      this.focusWhenActive = false;
   1363    }
   1364  }
   1365 
   1366  /**
   1367   * Constrains the width of this view using the "min-width" and "max-width"
   1368   * styles. Setting this to zero removes the constraints.
   1369   *
   1370   * @param {number} value
   1371   *   Sets the min and max width of the element to ${value}px.
   1372   */
   1373  set minMaxWidth(value) {
   1374    let style = this.node.style;
   1375    if (value) {
   1376      style.minWidth = style.maxWidth = value + "px";
   1377    } else {
   1378      style.removeProperty("min-width");
   1379      style.removeProperty("max-width");
   1380    }
   1381  }
   1382 
   1383  /**
   1384   * Constrains the height of this view using the "min-height" and "max-height"
   1385   * styles. Setting this to zero removes the constraints.
   1386   *
   1387   * @param {number} value
   1388   *   Sets the min and max height of the element to ${value}px.
   1389   */
   1390  set minMaxHeight(value) {
   1391    let style = this.node.style;
   1392    if (value) {
   1393      style.minHeight = style.maxHeight = value + "px";
   1394    } else {
   1395      style.removeProperty("min-height");
   1396      style.removeProperty("max-height");
   1397    }
   1398  }
   1399 
   1400  /**
   1401   * Adds a header with the given title, or removes it if the title is empty.
   1402   *
   1403   * If an element matching `.panel-header` is found in the PanelView, then
   1404   * this method will attempt to set the textContent of the first `h1 > span`
   1405   * underneath that `.panel-header` to `value`.
   1406   *
   1407   * Otherwise, this will attempt to insert a header element and a separator
   1408   * beneath it, with the text of the header set to `value`.
   1409   *
   1410   * If `value` is null, then these elements are cleared and removed.
   1411   *
   1412   * @param {string} value
   1413   *   The header to set.
   1414   */
   1415  set headerText(value) {
   1416    let ensureHeaderSeparator = headerNode => {
   1417      if (headerNode.nextSibling.tagName != "toolbarseparator") {
   1418        let separator = this.document.createXULElement("toolbarseparator");
   1419        this.node.insertBefore(separator, headerNode.nextSibling);
   1420      }
   1421    };
   1422 
   1423    // If the header already exists, update or remove it as requested.
   1424    let isMainView = this.node.getAttribute("mainview");
   1425    let header = this.node.querySelector(".panel-header");
   1426    if (header) {
   1427      let headerBackButton = header.querySelector(".subviewbutton-back");
   1428      if (isMainView) {
   1429        if (headerBackButton) {
   1430          // A back button should not appear in a mainview.
   1431          // This codepath can be reached if a user enters a panelview in
   1432          // the overflow panel (like the Profiler), and then unpins it back to the toolbar.
   1433          headerBackButton.remove();
   1434        }
   1435      }
   1436      if (value) {
   1437        if (
   1438          !isMainView &&
   1439          !headerBackButton &&
   1440          !this.node.getAttribute("no-back-button")
   1441        ) {
   1442          // Add a back button when not in mainview (if it doesn't exist already),
   1443          // also when a panelview specifies it doesn't want a back button,
   1444          // like the Report Broken Site (sent) panelview.
   1445          header.prepend(this.createHeaderBackButton());
   1446        }
   1447        // Set the header title based on the value given.
   1448        header.querySelector(".panel-header > h1 > span").textContent = value;
   1449        ensureHeaderSeparator(header);
   1450      } else if (
   1451        !this.node.getAttribute("has-custom-header") &&
   1452        !this.node.getAttribute("mainview-with-header")
   1453      ) {
   1454        // No value supplied, and the panelview doesn't have a certain requirement
   1455        // for any kind of header, so remove it and the following toolbarseparator.
   1456        if (header.nextSibling.tagName == "toolbarseparator") {
   1457          header.nextSibling.remove();
   1458        }
   1459        header.remove();
   1460        return;
   1461      }
   1462      // Either the header exists and has been adjusted accordingly by now,
   1463      // or it doesn't (or shouldn't) exist. Bail out to not create a duplicate header.
   1464      return;
   1465    }
   1466 
   1467    // The header doesn't and shouldn't exist, only create it if needed.
   1468    if (!value) {
   1469      return;
   1470    }
   1471 
   1472    header = this.document.createXULElement("box");
   1473    header.classList.add("panel-header");
   1474 
   1475    if (!isMainView) {
   1476      let backButton = this.createHeaderBackButton();
   1477      header.append(backButton);
   1478    }
   1479 
   1480    let h1 = this.document.createElement("h1");
   1481    let span = this.document.createElement("span");
   1482    span.textContent = value;
   1483    h1.appendChild(span);
   1484 
   1485    header.append(h1);
   1486    this.node.prepend(header);
   1487 
   1488    ensureHeaderSeparator(header);
   1489  }
   1490 
   1491  /**
   1492   * Creates and returns a panel header back toolbarbutton.
   1493   */
   1494  createHeaderBackButton() {
   1495    let backButton = this.document.createXULElement("toolbarbutton");
   1496    backButton.className =
   1497      "subviewbutton subviewbutton-iconic subviewbutton-back";
   1498    backButton.setAttribute("closemenu", "none");
   1499    backButton.setAttribute("tabindex", "0");
   1500    backButton.setAttribute(
   1501      "aria-label",
   1502      lazy.gBundle.GetStringFromName("panel.back")
   1503    );
   1504    backButton.addEventListener("command", () => {
   1505      // The panelmultiview element may change if the view is reused.
   1506      this.node.panelMultiView.goBack();
   1507      backButton.blur();
   1508    });
   1509    return backButton;
   1510  }
   1511 
   1512  /**
   1513   * Dispatches a custom event on the PanelView, and also makes sure that the
   1514   * correct method is called on CustomizableWidget if applicable.
   1515   *
   1516   * @see AssociatedToNode.dispatchCustomEvent
   1517   * @param {...*} args
   1518   *   Additional arguments to be forwarded to the dispatchCustomEvent method of
   1519   *   AssociatedToNode.
   1520   */
   1521  dispatchCustomEvent(...args) {
   1522    lazy.CustomizableUI.ensureSubviewListeners(this.node);
   1523    return super.dispatchCustomEvent(...args);
   1524  }
   1525 
   1526  /**
   1527   * Populates the "knownWidth" and "knownHeight" properties with the current
   1528   * dimensions of the view. These may be zero if the view is invisible.
   1529   *
   1530   * These values are relevant during transitions and are retained for backwards
   1531   * navigation if the view is still open but is invisible.
   1532   */
   1533  captureKnownSize() {
   1534    let rect = this._getBoundsWithoutFlushing(this.node);
   1535    this.knownWidth = rect.width;
   1536    this.knownHeight = rect.height;
   1537  }
   1538 
   1539  /**
   1540   * Determine whether an element can only be navigated to with tab/shift+tab,
   1541   * not the arrow keys.
   1542   *
   1543   * @param {DOMNode} element
   1544   *   The element to check for navigation with tab only.
   1545   * @returns {boolean}
   1546   *   True if the element can only be navigated to with tab/shift+tab.
   1547   */
   1548  #isNavigableWithTabOnly(element) {
   1549    let tag = element.localName;
   1550    return (
   1551      tag == "menulist" ||
   1552      tag == "select" ||
   1553      tag == "radiogroup" ||
   1554      tag == "input" ||
   1555      tag == "textarea" ||
   1556      // Allow tab to reach embedded documents.
   1557      tag == "browser" ||
   1558      tag == "iframe" ||
   1559      // This is currently needed for the unified extensions panel to allow
   1560      // users to use up/down arrow to more quickly move between the extension
   1561      // items. See Bug 1784118
   1562      element.dataset?.navigableWithTabOnly === "true"
   1563    );
   1564  }
   1565 
   1566  /**
   1567   * Make a TreeWalker for keyboard navigation.
   1568   *
   1569   * @param {boolean} arrowKey
   1570   *   If `true`, elements only navigable with tab are excluded.
   1571   * @returns {TreeWalker}
   1572   *   The created TreeWalker instance.
   1573   */
   1574  #makeNavigableTreeWalker(arrowKey) {
   1575    let filter = node => {
   1576      if (node.disabled) {
   1577        return NodeFilter.FILTER_REJECT;
   1578      }
   1579      let bounds = this._getBoundsWithoutFlushing(node);
   1580      if (bounds.width == 0 || bounds.height == 0) {
   1581        return NodeFilter.FILTER_REJECT;
   1582      }
   1583      let isNavigableWithTabOnly = this.#isNavigableWithTabOnly(node);
   1584      // Early return when the node is navigable with tab only and we are using
   1585      // arrow keys so that nodes like button, toolbarbutton, checkbox, etc.
   1586      // can also be marked as "navigable with tab only", otherwise the next
   1587      // condition will unconditionally make them focusable.
   1588      if (arrowKey && isNavigableWithTabOnly) {
   1589        return NodeFilter.FILTER_REJECT;
   1590      }
   1591      let localName = node.localName.toLowerCase();
   1592      if (
   1593        localName == "button" ||
   1594        localName == "toolbarbutton" ||
   1595        localName == "checkbox" ||
   1596        localName == "a" ||
   1597        localName == "moz-button" ||
   1598        localName == "moz-box-button" ||
   1599        localName == "moz-toggle" ||
   1600        node.classList.contains("text-link") ||
   1601        (!arrowKey && isNavigableWithTabOnly) ||
   1602        node.dataset?.capturesFocus === "true"
   1603      ) {
   1604        // Set the tabindex attribute to make sure the node is focusable.
   1605        // Don't do this for browser and iframe elements because this breaks
   1606        // tabbing behavior. They're already focusable anyway.
   1607        if (
   1608          localName != "browser" &&
   1609          localName != "iframe" &&
   1610          !node.hasAttribute("tabindex") &&
   1611          node.dataset?.capturesFocus !== "true"
   1612        ) {
   1613          node.setAttribute("tabindex", "-1");
   1614        }
   1615        return NodeFilter.FILTER_ACCEPT;
   1616      }
   1617      return NodeFilter.FILTER_SKIP;
   1618    };
   1619    return this.document.createTreeWalker(
   1620      this.node,
   1621      NodeFilter.SHOW_ELEMENT,
   1622      filter
   1623    );
   1624  }
   1625 
   1626  /**
   1627   * Get a TreeWalker which finds elements navigable with tab/shift+tab.
   1628   *
   1629   * This is currently pseudo private with the underscore because
   1630   * AccessibilityUtils.js appears to be accessing this.
   1631   *
   1632   * @returns {TreeWalker}
   1633   *   The TreeWalker for tab/shift+tab navigable elements.
   1634   */
   1635  #_tabNavigableWalker = null;
   1636  get _tabNavigableWalker() {
   1637    if (!this.#_tabNavigableWalker) {
   1638      this.#_tabNavigableWalker = this.#makeNavigableTreeWalker(false);
   1639    }
   1640    return this.#_tabNavigableWalker;
   1641  }
   1642 
   1643  /**
   1644   * Get a TreeWalker which finds elements navigable with up/down arrow keys.
   1645   *
   1646   * @returns {TreeWalker}
   1647   *   The TreeWalker for arrow key navigable elements.
   1648   */
   1649  #_arrowNavigableWalker = null;
   1650  get #arrowNavigableWalker() {
   1651    if (!this.#_arrowNavigableWalker) {
   1652      this.#_arrowNavigableWalker = this.#makeNavigableTreeWalker(true);
   1653    }
   1654    return this.#_arrowNavigableWalker;
   1655  }
   1656 
   1657  /**
   1658   * Element that is currently selected with the keyboard, or null if no element
   1659   * is selected. Since the reference is held weakly, it can become null or
   1660   * undefined at any time.
   1661   *
   1662   * @type {DOMNode|null}
   1663   *   The selected element, or null if no element is selected.
   1664   */
   1665  get selectedElement() {
   1666    return this._selectedElement && this._selectedElement.get();
   1667  }
   1668 
   1669  set selectedElement(value) {
   1670    if (!value) {
   1671      delete this._selectedElement;
   1672    } else {
   1673      this._selectedElement = Cu.getWeakReference(value);
   1674    }
   1675  }
   1676 
   1677  /**
   1678   * Focuses and moves keyboard selection to the first navigable element.
   1679   * This is a no-op if there are no navigable elements.
   1680   *
   1681   * @param {boolean} [homeKey=false]
   1682   *   True if this is for the home key.
   1683   * @param {boolean} [skipBack=false]
   1684   *   True if the Back button should be skipped.
   1685   */
   1686  focusFirstNavigableElement(homeKey = false, skipBack = false) {
   1687    // The home key is conceptually similar to the up/down arrow keys.
   1688    let walker = homeKey
   1689      ? this.#arrowNavigableWalker
   1690      : this._tabNavigableWalker;
   1691    walker.currentNode = walker.root;
   1692    this.selectedElement = walker.firstChild();
   1693    if (
   1694      skipBack &&
   1695      walker.currentNode &&
   1696      walker.currentNode.classList.contains("subviewbutton-back") &&
   1697      walker.nextNode()
   1698    ) {
   1699      this.selectedElement = walker.currentNode;
   1700    }
   1701    this.focusSelectedElement(/* byKey */ true);
   1702  }
   1703 
   1704  /**
   1705   * Focuses and moves keyboard selection to the last navigable element.
   1706   * This is a no-op if there are no navigable elements.
   1707   *
   1708   * @param {boolean} [endKey=false]
   1709   *   True if this is for the end key.
   1710   */
   1711  focusLastNavigableElement(endKey = false) {
   1712    // The end key is conceptually similar to the up/down arrow keys.
   1713    let walker = endKey ? this.#arrowNavigableWalker : this._tabNavigableWalker;
   1714    walker.currentNode = walker.root;
   1715    this.selectedElement = walker.lastChild();
   1716    this.focusSelectedElement(/* byKey */ true);
   1717  }
   1718 
   1719  /**
   1720   * Based on going up or down, select the previous or next focusable element.
   1721   *
   1722   * @param {boolean} isDown
   1723   *   True if the selection is going down, false if going up.
   1724   * @param {boolean} [arrowKey=false]
   1725   *   True if this is for the up/down arrow keys.
   1726   * @returns {DOMNode} the element we selected.
   1727   */
   1728  moveSelection(isDown, arrowKey = false) {
   1729    let walker = arrowKey
   1730      ? this.#arrowNavigableWalker
   1731      : this._tabNavigableWalker;
   1732    let oldSel = this.selectedElement;
   1733    let newSel;
   1734    if (oldSel) {
   1735      walker.currentNode = oldSel;
   1736      newSel = isDown ? walker.nextNode() : walker.previousNode();
   1737    }
   1738    // If we couldn't find something, select the first or last item:
   1739    if (!newSel) {
   1740      walker.currentNode = walker.root;
   1741      newSel = isDown ? walker.firstChild() : walker.lastChild();
   1742    }
   1743    this.selectedElement = newSel;
   1744    return newSel;
   1745  }
   1746 
   1747  /**
   1748   * Allow for navigating subview buttons using the arrow keys and the Enter key.
   1749   * The Up and Down keys can be used to navigate the list up and down and the
   1750   * Enter, Right or Left - depending on the text direction - key can be used to
   1751   * simulate a click on the currently selected button.
   1752   * The Right or Left key - depending on the text direction - can be used to
   1753   * navigate to the previous view, functioning as a shortcut for the view's
   1754   * back button.
   1755   * Thus, in LTR mode:
   1756   *  - The Right key functions the same as the Enter key, simulating a click
   1757   *  - The Left key triggers a navigation back to the previous view.
   1758   *
   1759   * Key navigation is only enabled while the view is active, meaning that this
   1760   * method will return early if it is invoked during a sliding transition.
   1761   *
   1762   * @param {KeyEvent} event
   1763   *   The KeyEvent to potentially perform navigation for.
   1764   */
   1765  keyNavigation(event) {
   1766    if (!this.active) {
   1767      return;
   1768    }
   1769 
   1770    let focus = this.document.activeElement;
   1771    // Make sure the focus is actually inside the panel. (It might not be if
   1772    // the panel was opened with the mouse.) If it isn't, we don't care
   1773    // about it for our purposes.
   1774    // We use Node.compareDocumentPosition because Node.contains doesn't
   1775    // behave as expected for anonymous content; e.g. the input inside a
   1776    // textbox.
   1777    if (
   1778      focus &&
   1779      !(
   1780        this.node.compareDocumentPosition(focus) &
   1781        Node.DOCUMENT_POSITION_CONTAINED_BY
   1782      )
   1783    ) {
   1784      focus = null;
   1785    }
   1786 
   1787    // Some panels contain embedded documents or need to capture focus events.
   1788    // We can't manage keyboard navigation within those.
   1789    if (
   1790      focus &&
   1791      (focus.tagName == "browser" ||
   1792        focus.tagName == "iframe" ||
   1793        focus.dataset?.capturesFocus === "true")
   1794    ) {
   1795      return;
   1796    }
   1797 
   1798    let stop = () => {
   1799      event.stopPropagation();
   1800      event.preventDefault();
   1801    };
   1802 
   1803    // If the focused element is only navigable with tab, it wants the arrow
   1804    // keys, etc. We shouldn't handle any keys except tab and shift+tab.
   1805    // We make a function for this for performance reasons: we only want to
   1806    // check this for keys we potentially care about, not *all* keys.
   1807    let tabOnly = () => {
   1808      // We use the real focus rather than this.selectedElement because focus
   1809      // might have been moved without keyboard navigation (e.g. mouse click)
   1810      // and this.selectedElement is only updated for keyboard navigation.
   1811      return focus && this.#isNavigableWithTabOnly(focus);
   1812    };
   1813 
   1814    // If a context menu is open, we must let it handle all keys.
   1815    // Normally, this just happens, but because we have a capturing root
   1816    // element keydown listener, our listener takes precedence.
   1817    // Again, we only want to do this check on demand for performance.
   1818    let isContextMenuOpen = () => {
   1819      if (!focus) {
   1820        return false;
   1821      }
   1822      let contextNode = focus.closest("[context]");
   1823      if (!contextNode) {
   1824        return false;
   1825      }
   1826      let context = contextNode.getAttribute("context");
   1827      if (!context) {
   1828        return false;
   1829      }
   1830      let popup = this.document.getElementById(context);
   1831      return popup && popup.state == "open";
   1832    };
   1833 
   1834    this.ignoreMouseMove = false;
   1835 
   1836    let keyCode = event.code;
   1837    switch (keyCode) {
   1838      case "ArrowDown":
   1839      case "ArrowUp":
   1840        if (tabOnly()) {
   1841          break;
   1842        }
   1843      // Fall-through...
   1844      case "Tab": {
   1845        if (
   1846          isContextMenuOpen() ||
   1847          // Tab in an open menulist should close it.
   1848          (focus && focus.localName == "menulist" && focus.open)
   1849        ) {
   1850          break;
   1851        }
   1852        stop();
   1853        let isDown =
   1854          keyCode == "ArrowDown" || (keyCode == "Tab" && !event.shiftKey);
   1855        let button = this.moveSelection(isDown, keyCode != "Tab");
   1856        button.focus();
   1857        break;
   1858      }
   1859      case "Home":
   1860        if (tabOnly() || isContextMenuOpen()) {
   1861          break;
   1862        }
   1863        stop();
   1864        this.focusFirstNavigableElement(true);
   1865        break;
   1866      case "End":
   1867        if (tabOnly() || isContextMenuOpen()) {
   1868          break;
   1869        }
   1870        stop();
   1871        this.focusLastNavigableElement(true);
   1872        break;
   1873      case "ArrowLeft":
   1874      case "ArrowRight": {
   1875        if (tabOnly() || isContextMenuOpen()) {
   1876          break;
   1877        }
   1878        stop();
   1879        if (
   1880          (!this.window.RTL_UI && keyCode == "ArrowLeft") ||
   1881          (this.window.RTL_UI && keyCode == "ArrowRight")
   1882        ) {
   1883          this.node.panelMultiView.goBack();
   1884          break;
   1885        }
   1886        // If the current button is _not_ one that points to a subview, pressing
   1887        // the arrow key shouldn't do anything.
   1888        let button = this.selectedElement;
   1889        if (
   1890          !button ||
   1891          !(
   1892            button.classList.contains("subviewbutton-nav") ||
   1893            button.classList.contains("moz-button-subviewbutton-nav")
   1894          )
   1895        ) {
   1896          break;
   1897        }
   1898      }
   1899      // Fall-through...
   1900      case "Space":
   1901      case "NumpadEnter":
   1902      case "Enter": {
   1903        if (tabOnly() || isContextMenuOpen()) {
   1904          break;
   1905        }
   1906        let button = this.selectedElement;
   1907        if (!button || button?.localName == "moz-toggle") {
   1908          break;
   1909        }
   1910        stop();
   1911 
   1912        this._doingKeyboardActivation = true;
   1913        const details = {
   1914          bubbles: true,
   1915          ctrlKey: event.ctrlKey,
   1916          altKey: event.altKey,
   1917          shiftKey: event.shiftKey,
   1918          metaKey: event.metaKey,
   1919        };
   1920        // The a11y-checks want the target to be accessible. For moz-button the
   1921        // focus is really on the inner button which is accessible, but we check
   1922        // a11y against the event target (moz-button) which fails. Dispatch from
   1923        // the inner button element instead.
   1924        let target = button;
   1925        if (
   1926          button.localName == "moz-button" ||
   1927          button.localName == "moz-box-button"
   1928        ) {
   1929          target = button.buttonEl;
   1930          details.composed = true;
   1931        }
   1932        let dispEvent = new event.target.ownerGlobal.MouseEvent(
   1933          "mousedown",
   1934          details
   1935        );
   1936        target.dispatchEvent(dispEvent);
   1937        // This event will trigger a command event too.
   1938        dispEvent = new event.target.ownerGlobal.PointerEvent("click", details);
   1939        target.dispatchEvent(dispEvent);
   1940        this._doingKeyboardActivation = false;
   1941        break;
   1942      }
   1943    }
   1944  }
   1945 
   1946  /**
   1947   * Focus the last selected element in the view, if any.
   1948   *
   1949   * @param {boolean} [byKey=false]
   1950   *   True if focus was moved by the user pressing a key. Needed to ensure we
   1951   *   show focus styles in the right cases.
   1952   */
   1953  focusSelectedElement(byKey = false) {
   1954    let selected = this.selectedElement;
   1955    if (selected) {
   1956      let flag = byKey ? Services.focus.FLAG_BYKEY : 0;
   1957      Services.focus.setFocus(selected, flag);
   1958    }
   1959  }
   1960 
   1961  /**
   1962   * Clear all traces of keyboard navigation happening right now.
   1963   */
   1964  clearNavigation() {
   1965    let selected = this.selectedElement;
   1966    if (selected) {
   1967      selected.blur();
   1968      this.selectedElement = null;
   1969    }
   1970  }
   1971 };