tor-browser

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

browser-pageActions.js (30797B)


      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 var BrowserPageActions = {
      6  _panelNode: null,
      7  /**
      8   * The main page action button in the urlbar (DOM node)
      9   */
     10  get mainButtonNode() {
     11    delete this.mainButtonNode;
     12    return (this.mainButtonNode = document.getElementById("pageActionButton"));
     13  },
     14 
     15  /**
     16   * The main page action panel DOM node (DOM node)
     17   */
     18  get panelNode() {
     19    // Lazy load the page action panel the first time we need to display it
     20    if (!this._panelNode) {
     21      this.initializePanel();
     22    }
     23    delete this.panelNode;
     24    return (this.panelNode = this._panelNode);
     25  },
     26 
     27  /**
     28   * The panelmultiview node in the main page action panel (DOM node)
     29   */
     30  get multiViewNode() {
     31    delete this.multiViewNode;
     32    return (this.multiViewNode = document.getElementById(
     33      "pageActionPanelMultiView"
     34    ));
     35  },
     36 
     37  /**
     38   * The main panelview node in the main page action panel (DOM node)
     39   */
     40  get mainViewNode() {
     41    delete this.mainViewNode;
     42    return (this.mainViewNode = document.getElementById(
     43      "pageActionPanelMainView"
     44    ));
     45  },
     46 
     47  /**
     48   * The vbox body node in the main panelview node (DOM node)
     49   */
     50  get mainViewBodyNode() {
     51    delete this.mainViewBodyNode;
     52    return (this.mainViewBodyNode = this.mainViewNode.querySelector(
     53      ".panel-subview-body"
     54    ));
     55  },
     56 
     57  /**
     58   * Inits.  Call to init.
     59   */
     60  init() {
     61    this.placeAllActionsInUrlbar();
     62    this._onPanelShowing = this._onPanelShowing.bind(this);
     63  },
     64 
     65  _onPanelShowing() {
     66    this.initializePanel();
     67    for (let action of PageActions.actionsInPanel(window)) {
     68      let buttonNode = this.panelButtonNodeForActionID(action.id);
     69      action.onShowingInPanel(buttonNode);
     70    }
     71  },
     72 
     73  placeLazyActionsInPanel() {
     74    let actions = this._actionsToLazilyPlaceInPanel;
     75    this._actionsToLazilyPlaceInPanel = [];
     76    for (let action of actions) {
     77      this._placeActionInPanelNow(action);
     78    }
     79  },
     80 
     81  // Actions placed in the panel aren't actually placed until the panel is
     82  // subsequently opened.
     83  _actionsToLazilyPlaceInPanel: [],
     84 
     85  /**
     86   * Places all registered actions in the urlbar.
     87   */
     88  placeAllActionsInUrlbar() {
     89    let urlbarActions = PageActions.actionsInUrlbar(window);
     90    for (let action of urlbarActions) {
     91      this.placeActionInUrlbar(action);
     92    }
     93    this._updateMainButtonAttributes();
     94  },
     95 
     96  /**
     97   * Initializes the panel if necessary.
     98   */
     99  initializePanel() {
    100    // Lazy load the page action panel the first time we need to display it
    101    if (!this._panelNode) {
    102      let template = document.getElementById("pageActionPanelTemplate");
    103      template.replaceWith(template.content);
    104      this._panelNode = document.getElementById("pageActionPanel");
    105      this._panelNode.addEventListener("popupshowing", this._onPanelShowing);
    106    }
    107 
    108    for (let action of PageActions.actionsInPanel(window)) {
    109      this.placeActionInPanel(action);
    110    }
    111    this.placeLazyActionsInPanel();
    112  },
    113 
    114  /**
    115   * Adds or removes as necessary DOM nodes for the given action.
    116   *
    117   * @param  action (PageActions.Action, required)
    118   *         The action to place.
    119   */
    120  placeAction(action) {
    121    this.placeActionInPanel(action);
    122    this.placeActionInUrlbar(action);
    123    this._updateMainButtonAttributes();
    124  },
    125 
    126  /**
    127   * Adds or removes as necessary DOM nodes for the action in the panel.
    128   *
    129   * @param  action (PageActions.Action, required)
    130   *         The action to place.
    131   */
    132  placeActionInPanel(action) {
    133    if (this._panelNode && this.panelNode.state != "closed") {
    134      this._placeActionInPanelNow(action);
    135    } else {
    136      // This method may be called for the same action more than once
    137      // (e.g. when an extension does call pageAction.show/hidden to
    138      // enable or disable its own pageAction and we will have to
    139      // update the urlbar overflow panel accordingly).
    140      //
    141      // Ensure we don't add the same actions more than once (otherwise we will
    142      // not remove all the entries in _removeActionFromPanel).
    143      if (
    144        this._actionsToLazilyPlaceInPanel.findIndex(a => a.id == action.id) >= 0
    145      ) {
    146        return;
    147      }
    148      // Lazily place the action in the panel the next time it opens.
    149      this._actionsToLazilyPlaceInPanel.push(action);
    150    }
    151  },
    152 
    153  _placeActionInPanelNow(action) {
    154    if (action.shouldShowInPanel(window)) {
    155      this._addActionToPanel(action);
    156    } else {
    157      this._removeActionFromPanel(action);
    158    }
    159  },
    160 
    161  _addActionToPanel(action) {
    162    let id = this.panelButtonNodeIDForActionID(action.id);
    163    let node = document.getElementById(id);
    164    if (node) {
    165      return;
    166    }
    167    this._maybeNotifyBeforePlacedInWindow(action);
    168    node = this._makePanelButtonNodeForAction(action);
    169    node.id = id;
    170    let insertBeforeNode = this._getNextNode(action, false);
    171    this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
    172    this.updateAction(action, null, {
    173      panelNode: node,
    174    });
    175    this._updateActionDisabledInPanel(action, node);
    176    action.onPlacedInPanel(node);
    177    this._addOrRemoveSeparatorsInPanel();
    178  },
    179 
    180  _removeActionFromPanel(action) {
    181    let lazyIndex = this._actionsToLazilyPlaceInPanel.findIndex(
    182      a => a.id == action.id
    183    );
    184    if (lazyIndex >= 0) {
    185      this._actionsToLazilyPlaceInPanel.splice(lazyIndex, 1);
    186    }
    187    let node = this.panelButtonNodeForActionID(action.id);
    188    if (!node) {
    189      return;
    190    }
    191    node.remove();
    192    if (action.getWantsSubview(window)) {
    193      let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
    194      let panelViewNode = document.getElementById(panelViewNodeID);
    195      if (panelViewNode) {
    196        panelViewNode.remove();
    197      }
    198    }
    199    this._addOrRemoveSeparatorsInPanel();
    200  },
    201 
    202  _addOrRemoveSeparatorsInPanel() {
    203    let actions = PageActions.actionsInPanel(window);
    204    let ids = [
    205      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
    206      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
    207    ];
    208    for (let id of ids) {
    209      let sep = actions.find(a => a.id == id);
    210      if (sep) {
    211        this._addActionToPanel(sep);
    212      } else {
    213        let node = this.panelButtonNodeForActionID(id);
    214        if (node) {
    215          node.remove();
    216        }
    217      }
    218    }
    219  },
    220 
    221  _updateMainButtonAttributes() {
    222    this.mainButtonNode.toggleAttribute(
    223      "multiple-children",
    224      PageActions.actions.length > 1
    225    );
    226  },
    227 
    228  /**
    229   * Returns the node before which an action's node should be inserted.
    230   *
    231   * @param  action (PageActions.Action, required)
    232   *         The action that will be inserted.
    233   * @param  forUrlbar (bool, required)
    234   *         True if you're inserting into the urlbar, false if you're inserting
    235   *         into the panel.
    236   * @return (DOM node, maybe null) The DOM node before which to insert the
    237   *         given action.  Null if the action should be inserted at the end.
    238   */
    239  _getNextNode(action, forUrlbar) {
    240    let actions = forUrlbar
    241      ? PageActions.actionsInUrlbar(window)
    242      : PageActions.actionsInPanel(window);
    243    let index = actions.findIndex(a => a.id == action.id);
    244    if (index < 0) {
    245      return null;
    246    }
    247    for (let i = index + 1; i < actions.length; i++) {
    248      let node = forUrlbar
    249        ? this.urlbarButtonNodeForActionID(actions[i].id)
    250        : this.panelButtonNodeForActionID(actions[i].id);
    251      if (node) {
    252        return node;
    253      }
    254    }
    255    return null;
    256  },
    257 
    258  _maybeNotifyBeforePlacedInWindow(action) {
    259    if (!this._isActionPlacedInWindow(action)) {
    260      action.onBeforePlacedInWindow(window);
    261    }
    262  },
    263 
    264  _isActionPlacedInWindow(action) {
    265    if (this.panelButtonNodeForActionID(action.id)) {
    266      return true;
    267    }
    268    let urlbarNode = this.urlbarButtonNodeForActionID(action.id);
    269    return urlbarNode && !urlbarNode.hidden;
    270  },
    271 
    272  _makePanelButtonNodeForAction(action) {
    273    if (action.__isSeparator) {
    274      let node = document.createXULElement("toolbarseparator");
    275      return node;
    276    }
    277    let buttonNode = document.createXULElement("toolbarbutton");
    278    buttonNode.classList.add(
    279      "subviewbutton",
    280      "subviewbutton-iconic",
    281      "pageAction-panel-button"
    282    );
    283    if (action.isBadged) {
    284      buttonNode.setAttribute("badged", "true");
    285    }
    286    buttonNode.setAttribute("actionid", action.id);
    287    buttonNode.addEventListener("command", event => {
    288      this.doCommandForAction(action, event, buttonNode);
    289    });
    290    return buttonNode;
    291  },
    292 
    293  _makePanelViewNodeForAction(action, forUrlbar) {
    294    let panelViewNode = document.createXULElement("panelview");
    295    panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
    296    panelViewNode.classList.add("PanelUI-subView");
    297    let bodyNode = document.createXULElement("vbox");
    298    bodyNode.id = panelViewNode.id + "-body";
    299    bodyNode.classList.add("panel-subview-body");
    300    panelViewNode.appendChild(bodyNode);
    301    return panelViewNode;
    302  },
    303 
    304  /**
    305   * Shows or hides a panel for an action.  You can supply your own panel;
    306   * otherwise one is created.
    307   *
    308   * @param  action (PageActions.Action, required)
    309   *         The action for which to toggle the panel.  If the action is in the
    310   *         urlbar, then the panel will be anchored to it.  Otherwise, a
    311   *         suitable anchor will be used.
    312   * @param  panelNode (DOM node, optional)
    313   *         The panel to use.  This method takes a hands-off approach with
    314   *         regard to your panel in terms of attributes, styling, etc.
    315   * @param  event (DOM event, optional)
    316   *         The event which triggered this panel.
    317   */
    318  togglePanelForAction(action, panelNode = null, event = null) {
    319    let aaPanelNode = this.activatedActionPanelNode;
    320    if (panelNode) {
    321      // Note that this particular code path will not prevent the panel from
    322      // opening later if PanelMultiView.showPopup was called but the panel has
    323      // not been opened yet.
    324      if (panelNode.state != "closed") {
    325        PanelMultiView.hidePopup(panelNode);
    326        return;
    327      }
    328      if (aaPanelNode) {
    329        PanelMultiView.hidePopup(aaPanelNode);
    330      }
    331    } else if (aaPanelNode) {
    332      PanelMultiView.hidePopup(aaPanelNode);
    333      return;
    334    } else {
    335      panelNode = this._makeActivatedActionPanelForAction(action);
    336    }
    337 
    338    // Hide the main panel before showing the action's panel.
    339    PanelMultiView.hidePopup(this.panelNode);
    340 
    341    let anchorNode = this.panelAnchorNodeForAction(action);
    342    PanelMultiView.openPopup(panelNode, anchorNode, {
    343      position: "bottomright topright",
    344      triggerEvent: event,
    345    }).catch(console.error);
    346  },
    347 
    348  _makeActivatedActionPanelForAction(action) {
    349    let panelNode = document.createXULElement("panel");
    350    panelNode.id = this._activatedActionPanelID;
    351    panelNode.classList.add("cui-widget-panel", "panel-no-padding");
    352    panelNode.setAttribute("actionID", action.id);
    353    panelNode.setAttribute("role", "group");
    354    panelNode.setAttribute("type", "arrow");
    355    panelNode.setAttribute("flip", "slide");
    356    panelNode.setAttribute("noautofocus", "true");
    357    panelNode.setAttribute("tabspecific", "true");
    358 
    359    let panelViewNode = null;
    360    let iframeNode = null;
    361 
    362    if (action.getWantsSubview(window)) {
    363      let multiViewNode = document.createXULElement("panelmultiview");
    364      panelViewNode = this._makePanelViewNodeForAction(action, true);
    365      multiViewNode.setAttribute("mainViewId", panelViewNode.id);
    366      multiViewNode.appendChild(panelViewNode);
    367      panelNode.appendChild(multiViewNode);
    368    } else if (action.wantsIframe) {
    369      iframeNode = document.createXULElement("iframe");
    370      iframeNode.setAttribute("type", "content");
    371      panelNode.appendChild(iframeNode);
    372    }
    373 
    374    let popupSet = document.getElementById("mainPopupSet");
    375    popupSet.appendChild(panelNode);
    376    panelNode.addEventListener(
    377      "popuphidden",
    378      () => {
    379        PanelMultiView.removePopup(panelNode);
    380      },
    381      { once: true }
    382    );
    383 
    384    if (iframeNode) {
    385      panelNode.addEventListener(
    386        "popupshowing",
    387        () => {
    388          action.onIframeShowing(iframeNode, panelNode);
    389        },
    390        { once: true }
    391      );
    392      panelNode.addEventListener(
    393        "popupshown",
    394        () => {
    395          iframeNode.focus();
    396        },
    397        { once: true }
    398      );
    399      panelNode.addEventListener(
    400        "popuphiding",
    401        () => {
    402          action.onIframeHiding(iframeNode, panelNode);
    403        },
    404        { once: true }
    405      );
    406      panelNode.addEventListener(
    407        "popuphidden",
    408        () => {
    409          action.onIframeHidden(iframeNode, panelNode);
    410        },
    411        { once: true }
    412      );
    413    }
    414 
    415    if (panelViewNode) {
    416      action.onSubviewPlaced(panelViewNode);
    417      panelNode.addEventListener(
    418        "popupshowing",
    419        () => {
    420          action.onSubviewShowing(panelViewNode);
    421        },
    422        { once: true }
    423      );
    424    }
    425 
    426    return panelNode;
    427  },
    428 
    429  /**
    430   * Returns the node in the urlbar to which popups for the given action should
    431   * be anchored.  If the action is null, a sensible anchor is returned.
    432   *
    433   * @param  action (PageActions.Action, optional)
    434   *         The action you want to anchor.
    435   * @param  event (DOM event, optional)
    436   *         This is used to display the feedback panel on the right node when
    437   *         the command can be invoked from both the main panel and another
    438   *         location, such as an activated action panel or a button.
    439   * @return (DOM node) The node to which the action should be anchored.
    440   */
    441  panelAnchorNodeForAction(action, event) {
    442    if (event && event.target.closest("panel") == this.panelNode) {
    443      return this.mainButtonNode;
    444    }
    445 
    446    // Try each of the following nodes in order, using the first that's visible.
    447    let potentialAnchorNodes = [
    448      document.getElementById(action?.anchorIDOverride),
    449      document.getElementById(
    450        action && this.urlbarButtonNodeIDForActionID(action.id)
    451      ),
    452      document.getElementById(this.mainButtonNode.id),
    453      document.getElementById("identity-icon"),
    454    ];
    455    for (let node of potentialAnchorNodes) {
    456      if (node && !node.hidden) {
    457        let bounds = window.windowUtils.getBoundsWithoutFlushing(node);
    458        if (bounds.height > 0 && bounds.width > 0) {
    459          return node;
    460        }
    461      }
    462    }
    463    let id = action ? action.id : "<no action>";
    464    throw new Error(`PageActions: No anchor node for ${id}`);
    465  },
    466 
    467  get activatedActionPanelNode() {
    468    return document.getElementById(this._activatedActionPanelID);
    469  },
    470 
    471  get _activatedActionPanelID() {
    472    return "pageActionActivatedActionPanel";
    473  },
    474 
    475  /**
    476   * Adds or removes as necessary a DOM node for the given action in the urlbar.
    477   *
    478   * @param  action (PageActions.Action, required)
    479   *         The action to place.
    480   */
    481  placeActionInUrlbar(action) {
    482    let id = this.urlbarButtonNodeIDForActionID(action.id);
    483    let node = document.getElementById(id);
    484 
    485    if (!action.shouldShowInUrlbar(window)) {
    486      if (node) {
    487        if (action.__urlbarNodeInMarkup) {
    488          node.hidden = true;
    489        } else {
    490          node.remove();
    491        }
    492      }
    493      return;
    494    }
    495 
    496    let newlyPlaced = false;
    497    if (action.__urlbarNodeInMarkup) {
    498      this._maybeNotifyBeforePlacedInWindow(action);
    499      // Allow the consumer to add the node in response to the
    500      // onBeforePlacedInWindow notification.
    501      node = document.getElementById(id);
    502      if (!node) {
    503        return;
    504      }
    505      newlyPlaced = node.hidden;
    506      node.hidden = false;
    507    } else if (!node) {
    508      newlyPlaced = true;
    509      this._maybeNotifyBeforePlacedInWindow(action);
    510      node = this._makeUrlbarButtonNode(action);
    511      node.id = id;
    512    }
    513 
    514    if (!newlyPlaced) {
    515      return;
    516    }
    517 
    518    let insertBeforeNode = this._getNextNode(action, true);
    519    this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
    520    this.updateAction(action, null, {
    521      urlbarNode: node,
    522    });
    523    action.onPlacedInUrlbar(node);
    524  },
    525 
    526  _makeUrlbarButtonNode(action) {
    527    let buttonNode = document.createXULElement("hbox");
    528    buttonNode.classList.add("urlbar-page-action");
    529    if (action.extensionID) {
    530      buttonNode.classList.add("urlbar-addon-page-action");
    531    }
    532    buttonNode.setAttribute("actionid", action.id);
    533    buttonNode.setAttribute("role", "button");
    534    let commandHandler = event => {
    535      this.doCommandForAction(action, event, buttonNode);
    536    };
    537    buttonNode.addEventListener("click", commandHandler);
    538    buttonNode.addEventListener("keypress", commandHandler);
    539 
    540    let imageNode = document.createXULElement("image");
    541    imageNode.classList.add("urlbar-icon");
    542    buttonNode.appendChild(imageNode);
    543    return buttonNode;
    544  },
    545 
    546  /**
    547   * Removes all the DOM nodes of the given action.
    548   *
    549   * @param  action (PageActions.Action, required)
    550   *         The action to remove.
    551   */
    552  removeAction(action) {
    553    this._removeActionFromPanel(action);
    554    this._removeActionFromUrlbar(action);
    555    action.onRemovedFromWindow(window);
    556    this._updateMainButtonAttributes();
    557  },
    558 
    559  _removeActionFromUrlbar(action) {
    560    let node = this.urlbarButtonNodeForActionID(action.id);
    561    if (node) {
    562      node.remove();
    563    }
    564  },
    565 
    566  /**
    567   * Updates the DOM nodes of an action to reflect either a changed property or
    568   * all properties.
    569   *
    570   * @param  action (PageActions.Action, required)
    571   *         The action to update.
    572   * @param  propertyName (string, optional)
    573   *         The name of the property to update.  If not given, then DOM nodes
    574   *         will be updated to reflect the current values of all properties.
    575   * @param  opts (object, optional)
    576   *         - panelNode: The action's node in the panel to update.
    577   *         - urlbarNode: The action's node in the urlbar to update.
    578   *         - value: If a property name is passed, this argument may contain
    579   *           its current value, in order to prevent a further look-up.
    580   */
    581  updateAction(action, propertyName = null, opts = {}) {
    582    let anyNodeGiven = "panelNode" in opts || "urlbarNode" in opts;
    583    let panelNode = anyNodeGiven
    584      ? opts.panelNode || null
    585      : this.panelButtonNodeForActionID(action.id);
    586    let urlbarNode = anyNodeGiven
    587      ? opts.urlbarNode || null
    588      : this.urlbarButtonNodeForActionID(action.id);
    589    let value = opts.value || undefined;
    590    if (propertyName) {
    591      this[this._updateMethods[propertyName]](
    592        action,
    593        panelNode,
    594        urlbarNode,
    595        value
    596      );
    597    } else {
    598      for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
    599        this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
    600      }
    601    }
    602  },
    603 
    604  _updateMethods: {
    605    disabled: "_updateActionDisabled",
    606    iconURL: "_updateActionIconURL",
    607    title: "_updateActionLabeling",
    608    tooltip: "_updateActionTooltip",
    609    wantsSubview: "_updateActionWantsSubview",
    610  },
    611 
    612  _updateActionDisabled(
    613    action,
    614    panelNode,
    615    urlbarNode,
    616    disabled = action.getDisabled(window)
    617  ) {
    618    // Extension page actions should behave like a transient action,
    619    // and be hidden from the urlbar overflow menu if they
    620    // are disabled (as in the urlbar when the overflow menu isn't available)
    621    //
    622    // TODO(Bug 1704139): as a follow up we may look into just set on all
    623    // extension pageActions `_transient: true`, at least once we sunset
    624    // the proton preference and we don't need the pre-Proton behavior anymore,
    625    // and remove this special case.
    626    const isProtonExtensionAction = action.extensionID;
    627 
    628    if (action.__transient || isProtonExtensionAction) {
    629      this.placeActionInPanel(action);
    630    } else {
    631      this._updateActionDisabledInPanel(action, panelNode, disabled);
    632    }
    633    this.placeActionInUrlbar(action);
    634  },
    635 
    636  _updateActionDisabledInPanel(
    637    action,
    638    panelNode,
    639    disabled = action.getDisabled(window)
    640  ) {
    641    if (panelNode) {
    642      if (disabled) {
    643        panelNode.setAttribute("disabled", "true");
    644      } else {
    645        panelNode.removeAttribute("disabled");
    646      }
    647    }
    648  },
    649 
    650  _updateActionIconURL(
    651    action,
    652    panelNode,
    653    urlbarNode,
    654    properties = action.getIconProperties(window)
    655  ) {
    656    for (let [prop, value] of Object.entries(properties)) {
    657      if (panelNode) {
    658        panelNode.style.setProperty(prop, value);
    659      }
    660      if (urlbarNode) {
    661        urlbarNode.style.setProperty(prop, value);
    662      }
    663    }
    664  },
    665 
    666  _updateActionLabeling(
    667    action,
    668    panelNode,
    669    urlbarNode,
    670    title = action.getTitle(window)
    671  ) {
    672    if (panelNode) {
    673      panelNode.setAttribute("label", title);
    674    }
    675    if (urlbarNode) {
    676      urlbarNode.setAttribute("aria-label", title);
    677      // tooltiptext falls back to the title, so update it too if necessary.
    678      let tooltip = action.getTooltip(window);
    679      if (!tooltip) {
    680        urlbarNode.setAttribute("tooltiptext", title);
    681      }
    682    }
    683  },
    684 
    685  _updateActionTooltip(
    686    action,
    687    panelNode,
    688    urlbarNode,
    689    tooltip = action.getTooltip(window)
    690  ) {
    691    if (urlbarNode) {
    692      if (!tooltip) {
    693        tooltip = action.getTitle(window);
    694      }
    695      if (tooltip) {
    696        urlbarNode.setAttribute("tooltiptext", tooltip);
    697      }
    698    }
    699  },
    700 
    701  _updateActionWantsSubview(
    702    action,
    703    panelNode,
    704    urlbarNode,
    705    wantsSubview = action.getWantsSubview(window)
    706  ) {
    707    if (!panelNode) {
    708      return;
    709    }
    710    let panelViewID = this._panelViewNodeIDForActionID(action.id, false);
    711    let panelViewNode = document.getElementById(panelViewID);
    712    panelNode.classList.toggle("subviewbutton-nav", wantsSubview);
    713    if (!wantsSubview) {
    714      if (panelViewNode) {
    715        panelViewNode.remove();
    716      }
    717      return;
    718    }
    719    if (!panelViewNode) {
    720      panelViewNode = this._makePanelViewNodeForAction(action, false);
    721      this.multiViewNode.appendChild(panelViewNode);
    722      action.onSubviewPlaced(panelViewNode);
    723    }
    724  },
    725 
    726  doCommandForAction(action, event, buttonNode) {
    727    if (event && event.type == "click" && event.button != 0) {
    728      return;
    729    }
    730    if (event && event.type == "keypress") {
    731      if (event.key != " " && event.key != "Enter") {
    732        return;
    733      }
    734      event.stopPropagation();
    735    }
    736    // If we're in the panel, open a subview inside the panel:
    737    // Note that we can't use this.panelNode.contains(buttonNode) here
    738    // because of XBL boundaries breaking Element.contains.
    739    if (
    740      action.getWantsSubview(window) &&
    741      buttonNode &&
    742      buttonNode.closest("panel") == this.panelNode
    743    ) {
    744      let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
    745      let panelViewNode = document.getElementById(panelViewNodeID);
    746      action.onSubviewShowing(panelViewNode);
    747      this.multiViewNode.showSubView(panelViewNode, buttonNode);
    748      return;
    749    }
    750    // Otherwise, hide the main popup in case it was open:
    751    PanelMultiView.hidePopup(this.panelNode);
    752 
    753    let aaPanelNode = this.activatedActionPanelNode;
    754    if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) {
    755      action.onCommand(event, buttonNode);
    756    }
    757    if (action.getWantsSubview(window) || action.wantsIframe) {
    758      this.togglePanelForAction(action, null, event);
    759    }
    760  },
    761 
    762  /**
    763   * Returns the action for a node.
    764   *
    765   * @param  node (DOM node, required)
    766   *         A button DOM node, either one that's shown in the page action panel
    767   *         or the urlbar.
    768   * @return (PageAction.Action) If the node has a related action and the action
    769   *         is not a separator, then the action is returned.  Otherwise null is
    770   *         returned.
    771   */
    772  actionForNode(node) {
    773    if (!node) {
    774      return null;
    775    }
    776    let actionID = this._actionIDForNodeID(node.id);
    777    let action = PageActions.actionForID(actionID);
    778    if (!action) {
    779      // When a page action is clicked, `node` will be an ancestor of
    780      // a node corresponding to an action. `node` will be the page action node
    781      // itself when a page action is selected with the keyboard. That's because
    782      // the semantic meaning of page action is on an hbox that contains an
    783      // <image>.
    784      for (let n = node.parentNode; n && !action; n = n.parentNode) {
    785        if (n.id == "page-action-buttons" || n.localName == "panelview") {
    786          // We reached the page-action-buttons or panelview container.
    787          // Stop looking; no action was found.
    788          break;
    789        }
    790        actionID = this._actionIDForNodeID(n.id);
    791        action = PageActions.actionForID(actionID);
    792      }
    793    }
    794    return action && !action.__isSeparator ? action : null;
    795  },
    796 
    797  /**
    798   * The given action's top-level button in the main panel.
    799   *
    800   * @param  actionID (string, required)
    801   *         The action ID.
    802   * @return (DOM node) The action's button in the main panel.
    803   */
    804  panelButtonNodeForActionID(actionID) {
    805    return document.getElementById(this.panelButtonNodeIDForActionID(actionID));
    806  },
    807 
    808  /**
    809   * The ID of the given action's top-level button in the main panel.
    810   *
    811   * @param  actionID (string, required)
    812   *         The action ID.
    813   * @return (string) The ID of the action's button in the main panel.
    814   */
    815  panelButtonNodeIDForActionID(actionID) {
    816    return `pageAction-panel-${actionID}`;
    817  },
    818 
    819  /**
    820   * The given action's button in the urlbar.
    821   *
    822   * @param  actionID (string, required)
    823   *         The action ID.
    824   * @return (DOM node) The action's urlbar button node.
    825   */
    826  urlbarButtonNodeForActionID(actionID) {
    827    return document.getElementById(
    828      this.urlbarButtonNodeIDForActionID(actionID)
    829    );
    830  },
    831 
    832  /**
    833   * The ID of the given action's button in the urlbar.
    834   *
    835   * @param  actionID (string, required)
    836   *         The action ID.
    837   * @return (string) The ID of the action's urlbar button node.
    838   */
    839  urlbarButtonNodeIDForActionID(actionID) {
    840    let action = PageActions.actionForID(actionID);
    841    if (action && action.urlbarIDOverride) {
    842      return action.urlbarIDOverride;
    843    }
    844    return `pageAction-urlbar-${actionID}`;
    845  },
    846 
    847  // The ID of the given action's panelview.
    848  _panelViewNodeIDForActionID(actionID, forUrlbar) {
    849    let placementID = forUrlbar ? "urlbar" : "panel";
    850    return `pageAction-${placementID}-${actionID}-subview`;
    851  },
    852 
    853  // The ID of the action corresponding to the given top-level button in the
    854  // panel or button in the urlbar.
    855  _actionIDForNodeID(nodeID) {
    856    if (!nodeID) {
    857      return null;
    858    }
    859    let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
    860    if (match) {
    861      return match[1];
    862    }
    863    // Check all the urlbar ID overrides.
    864    for (let action of PageActions.actions) {
    865      if (action.urlbarIDOverride && action.urlbarIDOverride == nodeID) {
    866        return action.id;
    867      }
    868    }
    869    return null;
    870  },
    871 
    872  /**
    873   * Call this when the main page action button in the urlbar is activated.
    874   *
    875   * @param  event (DOM event, required)
    876   *         The click or whatever event.
    877   */
    878  mainButtonClicked(event) {
    879    event.stopPropagation();
    880    if (
    881      // On mac, ctrl-click will send a context menu event from the widget, so
    882      // we don't want to bring up the panel when ctrl key is pressed.
    883      (event.type == "mousedown" &&
    884        (event.button != 0 ||
    885          (AppConstants.platform == "macosx" && event.ctrlKey))) ||
    886      (event.type == "keypress" &&
    887        event.charCode != KeyEvent.DOM_VK_SPACE &&
    888        event.keyCode != KeyEvent.DOM_VK_RETURN)
    889    ) {
    890      return;
    891    }
    892 
    893    // If the activated-action panel is open and anchored to the main button,
    894    // close it.
    895    let panelNode = this.activatedActionPanelNode;
    896    if (panelNode && panelNode.anchorNode.id == this.mainButtonNode.id) {
    897      PanelMultiView.hidePopup(panelNode);
    898      return;
    899    }
    900 
    901    if (this.panelNode.state == "open") {
    902      PanelMultiView.hidePopup(this.panelNode);
    903    } else if (this.panelNode.state == "closed") {
    904      this.showPanel(event);
    905    }
    906  },
    907 
    908  /**
    909   * Show the page action panel
    910   *
    911   * @param  event (DOM event, optional)
    912   *         The event that triggers showing the panel. (such as a mouse click,
    913   *         if the user clicked something to open the panel)
    914   */
    915  showPanel(event = null) {
    916    this.panelNode.hidden = false;
    917    PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
    918      position: "bottomright topright",
    919      triggerEvent: event,
    920    }).catch(console.error);
    921  },
    922 
    923  /**
    924   * Call this on the context menu's popupshowing event.
    925   *
    926   * @param  event (DOM event, required)
    927   *         The popupshowing event.
    928   * @param  popup (DOM node, required)
    929   *         The context menu popup DOM node.
    930   */
    931  async onContextMenuShowing(event, popup) {
    932    if (event.target != popup) {
    933      return;
    934    }
    935 
    936    let action = this.actionForNode(popup.triggerNode);
    937    // Only extension actions provide a context menu.
    938    if (!action?.extensionID) {
    939      this._contextAction = null;
    940      event.preventDefault();
    941      return;
    942    }
    943    this._contextAction = action;
    944 
    945    let removeExtension = popup.querySelector(".removeExtensionItem");
    946    let { extensionID } = this._contextAction;
    947    let addon = extensionID && (await AddonManager.getAddonByID(extensionID));
    948    removeExtension.hidden = !addon;
    949    if (addon) {
    950      removeExtension.disabled = !(
    951        addon.permissions & AddonManager.PERM_CAN_UNINSTALL
    952      );
    953    }
    954  },
    955 
    956  /**
    957   * Call this from the menu item in the context menu that opens about:addons.
    958   */
    959  openAboutAddonsForContextAction() {
    960    if (!this._contextAction) {
    961      return;
    962    }
    963    let action = this._contextAction;
    964    this._contextAction = null;
    965 
    966    let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
    967    window.BrowserAddonUI.openAddonsMgr(viewID);
    968  },
    969 
    970  /**
    971   * Call this from the menu item in the context menu that removes an add-on.
    972   */
    973  removeExtensionForContextAction() {
    974    if (!this._contextAction) {
    975      return;
    976    }
    977    let action = this._contextAction;
    978    this._contextAction = null;
    979 
    980    BrowserAddonUI.removeAddon(action.extensionID, "pageAction");
    981  },
    982 
    983  _contextAction: null,
    984 
    985  /**
    986   * Call this on tab switch or when the current <browser>'s location changes.
    987   */
    988  onLocationChange() {
    989    for (let action of PageActions.actions) {
    990      action.onLocationChange(window);
    991    }
    992  },
    993 };
    994 
    995 // built-in actions below //////////////////////////////////////////////////////
    996 
    997 // bookmark
    998 BrowserPageActions.bookmark = {
    999  onShowingInPanel(buttonNode) {
   1000    if (buttonNode.label == "null") {
   1001      BookmarkingUI.updateBookmarkPageMenuItem();
   1002    }
   1003  },
   1004 
   1005  onCommand(event) {
   1006    PanelMultiView.hidePopup(BrowserPageActions.panelNode);
   1007    BookmarkingUI.onStarCommand(event);
   1008  },
   1009 };