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