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
.defineModuleGetter(
8 "resource:///modules/SearchUIUtils.jsm"
11 var BrowserPageActions
= {
14 * The main page action button in the urlbar (DOM node)
16 get mainButtonNode() {
17 delete this.mainButtonNode
;
18 return (this.mainButtonNode
= document
.getElementById("pageActionButton"));
22 * The main page action panel DOM node (DOM node)
25 // Lazy load the page action panel the first time we need to display it
26 if (!this._panelNode
) {
27 this.initializePanel();
29 delete this.panelNode
;
30 return (this.panelNode
= this._panelNode
);
34 * The panelmultiview node in the main page action panel (DOM node)
37 delete this.multiViewNode
;
38 return (this.multiViewNode
= document
.getElementById(
39 "pageActionPanelMultiView"
44 * The main panelview node in the main page action panel (DOM node)
47 delete this.mainViewNode
;
48 return (this.mainViewNode
= document
.getElementById(
49 "pageActionPanelMainView"
54 * The vbox body node in the main panelview node (DOM node)
56 get mainViewBodyNode() {
57 delete this.mainViewBodyNode
;
58 return (this.mainViewBodyNode
= this.mainViewNode
.querySelector(
64 * Inits. Call to init.
67 this.placeAllActionsInUrlbar();
68 this._onPanelShowing
= this._onPanelShowing
.bind(this);
72 this.initializePanel();
73 for (let action
of PageActions
.actionsInPanel(window
)) {
74 let buttonNode
= this.panelButtonNodeForActionID(action
.id
);
75 action
.onShowingInPanel(buttonNode
);
79 placeLazyActionsInPanel() {
80 let actions
= this._actionsToLazilyPlaceInPanel
;
81 this._actionsToLazilyPlaceInPanel
= [];
82 for (let action
of actions
) {
83 this._placeActionInPanelNow(action
);
87 // Actions placed in the panel aren't actually placed until the panel is
88 // subsequently opened.
89 _actionsToLazilyPlaceInPanel
: [],
92 * Places all registered actions in the urlbar.
94 placeAllActionsInUrlbar() {
95 let urlbarActions
= PageActions
.actionsInUrlbar(window
);
96 for (let action
of urlbarActions
) {
97 this.placeActionInUrlbar(action
);
102 * Initializes the panel if necessary.
105 // Lazy load the page action panel the first time we need to display it
106 if (!this._panelNode
) {
107 let template
= document
.getElementById("pageActionPanelTemplate");
108 template
.replaceWith(template
.content
);
109 this._panelNode
= document
.getElementById("pageActionPanel");
110 this._panelNode
.addEventListener("popupshowing", this._onPanelShowing
);
111 this._panelNode
.addEventListener("popuphiding", () => {
112 this.mainButtonNode
.removeAttribute("open");
116 for (let action
of PageActions
.actionsInPanel(window
)) {
117 this.placeActionInPanel(action
);
119 this.placeLazyActionsInPanel();
123 * Adds or removes as necessary DOM nodes for the given action.
125 * @param action (PageActions.Action, required)
126 * The action to place.
128 placeAction(action
) {
129 this.placeActionInPanel(action
);
130 this.placeActionInUrlbar(action
);
134 * Adds or removes as necessary DOM nodes for the action in the panel.
136 * @param action (PageActions.Action, required)
137 * The action to place.
139 placeActionInPanel(action
) {
140 if (this._panelNode
&& this.panelNode
.state
!= "closed") {
141 this._placeActionInPanelNow(action
);
143 // Lazily place the action in the panel the next time it opens.
144 this._actionsToLazilyPlaceInPanel
.push(action
);
148 _placeActionInPanelNow(action
) {
149 if (action
.shouldShowInPanel(window
)) {
150 this._addActionToPanel(action
);
152 this._removeActionFromPanel(action
);
156 _addActionToPanel(action
) {
157 let id
= this.panelButtonNodeIDForActionID(action
.id
);
158 let node
= document
.getElementById(id
);
162 this._maybeNotifyBeforePlacedInWindow(action
);
163 node
= this._makePanelButtonNodeForAction(action
);
165 let insertBeforeNode
= this._getNextNode(action
, false);
166 this.mainViewBodyNode
.insertBefore(node
, insertBeforeNode
);
167 this.updateAction(action
, null, {
170 this._updateActionDisabledInPanel(action
, node
);
171 action
.onPlacedInPanel(node
);
172 this._addOrRemoveSeparatorsInPanel();
175 _removeActionFromPanel(action
) {
176 let lazyIndex
= this._actionsToLazilyPlaceInPanel
.findIndex(
177 a
=> a
.id
== action
.id
179 if (lazyIndex
>= 0) {
180 this._actionsToLazilyPlaceInPanel
.splice(lazyIndex
, 1);
182 let node
= this.panelButtonNodeForActionID(action
.id
);
187 if (action
.getWantsSubview(window
)) {
188 let panelViewNodeID
= this._panelViewNodeIDForActionID(action
.id
, false);
189 let panelViewNode
= document
.getElementById(panelViewNodeID
);
191 panelViewNode
.remove();
194 this._addOrRemoveSeparatorsInPanel();
197 _addOrRemoveSeparatorsInPanel() {
198 let actions
= PageActions
.actionsInPanel(window
);
200 PageActions
.ACTION_ID_BUILT_IN_SEPARATOR
,
201 PageActions
.ACTION_ID_TRANSIENT_SEPARATOR
,
203 for (let id
of ids
) {
204 let sep
= actions
.find(a
=> a
.id
== id
);
206 this._addActionToPanel(sep
);
208 let node
= this.panelButtonNodeForActionID(id
);
217 * Returns the node before which an action's node should be inserted.
219 * @param action (PageActions.Action, required)
220 * The action that will be inserted.
221 * @param forUrlbar (bool, required)
222 * True if you're inserting into the urlbar, false if you're inserting
224 * @return (DOM node, maybe null) The DOM node before which to insert the
225 * given action. Null if the action should be inserted at the end.
227 _getNextNode(action
, forUrlbar
) {
228 let actions
= forUrlbar
229 ? PageActions
.actionsInUrlbar(window
)
230 : PageActions
.actionsInPanel(window
);
231 let index
= actions
.findIndex(a
=> a
.id
== action
.id
);
235 for (let i
= index
+ 1; i
< actions
.length
; i
++) {
237 ? this.urlbarButtonNodeForActionID(actions
[i
].id
)
238 : this.panelButtonNodeForActionID(actions
[i
].id
);
246 _maybeNotifyBeforePlacedInWindow(action
) {
247 if (!this._isActionPlacedInWindow(action
)) {
248 action
.onBeforePlacedInWindow(window
);
252 _isActionPlacedInWindow(action
) {
253 if (this.panelButtonNodeForActionID(action
.id
)) {
256 let urlbarNode
= this.urlbarButtonNodeForActionID(action
.id
);
257 return urlbarNode
&& !urlbarNode
.hidden
;
260 _makePanelButtonNodeForAction(action
) {
261 if (action
.__isSeparator
) {
262 let node
= document
.createXULElement("toolbarseparator");
265 let buttonNode
= document
.createXULElement("toolbarbutton");
266 buttonNode
.classList
.add(
268 "subviewbutton-iconic",
269 "pageAction-panel-button"
271 if (action
.isBadged
) {
272 buttonNode
.setAttribute("badged", "true");
274 buttonNode
.setAttribute("actionid", action
.id
);
275 buttonNode
.addEventListener("command", event
=> {
276 this.doCommandForAction(action
, event
, buttonNode
);
281 _makePanelViewNodeForAction(action
, forUrlbar
) {
282 let panelViewNode
= document
.createXULElement("panelview");
283 panelViewNode
.id
= this._panelViewNodeIDForActionID(action
.id
, forUrlbar
);
284 panelViewNode
.classList
.add("PanelUI-subView");
285 let bodyNode
= document
.createXULElement("vbox");
286 bodyNode
.id
= panelViewNode
.id
+ "-body";
287 bodyNode
.classList
.add("panel-subview-body");
288 panelViewNode
.appendChild(bodyNode
);
289 return panelViewNode
;
293 * Shows or hides a panel for an action. You can supply your own panel;
294 * otherwise one is created.
296 * @param action (PageActions.Action, required)
297 * The action for which to toggle the panel. If the action is in the
298 * urlbar, then the panel will be anchored to it. Otherwise, a
299 * suitable anchor will be used.
300 * @param panelNode (DOM node, optional)
301 * The panel to use. This method takes a hands-off approach with
302 * regard to your panel in terms of attributes, styling, etc.
303 * @param event (DOM event, optional)
304 * The event which triggered this panel.
306 togglePanelForAction(action
, panelNode
= null, event
= null) {
307 let aaPanelNode
= this.activatedActionPanelNode
;
309 // Note that this particular code path will not prevent the panel from
310 // opening later if PanelMultiView.showPopup was called but the panel has
311 // not been opened yet.
312 if (panelNode
.state
!= "closed") {
313 PanelMultiView
.hidePopup(panelNode
);
317 PanelMultiView
.hidePopup(aaPanelNode
);
319 } else if (aaPanelNode
) {
320 PanelMultiView
.hidePopup(aaPanelNode
);
323 panelNode
= this._makeActivatedActionPanelForAction(action
);
326 // Hide the main panel before showing the action's panel.
327 PanelMultiView
.hidePopup(this.panelNode
);
329 let anchorNode
= this.panelAnchorNodeForAction(action
);
330 anchorNode
.setAttribute("open", "true");
331 panelNode
.addEventListener(
334 anchorNode
.removeAttribute("open");
339 PanelMultiView
.openPopup(panelNode
, anchorNode
, {
340 position
: "bottomcenter topright",
342 }).catch(Cu
.reportError
);
345 _makeActivatedActionPanelForAction(action
) {
346 let panelNode
= document
.createXULElement("panel");
347 panelNode
.id
= this._activatedActionPanelID
;
348 panelNode
.classList
.add("cui-widget-panel", "panel-no-padding");
349 panelNode
.setAttribute("actionID", action
.id
);
350 panelNode
.setAttribute("role", "group");
351 panelNode
.setAttribute("type", "arrow");
352 panelNode
.setAttribute("flip", "slide");
353 panelNode
.setAttribute("noautofocus", "true");
354 panelNode
.setAttribute("tabspecific", "true");
356 let panelViewNode
= null;
357 let iframeNode
= null;
359 if (action
.getWantsSubview(window
)) {
360 let multiViewNode
= document
.createXULElement("panelmultiview");
361 panelViewNode
= this._makePanelViewNodeForAction(action
, true);
362 multiViewNode
.setAttribute("mainViewId", panelViewNode
.id
);
363 multiViewNode
.appendChild(panelViewNode
);
364 panelNode
.appendChild(multiViewNode
);
365 } else if (action
.wantsIframe
) {
366 iframeNode
= document
.createXULElement("iframe");
367 iframeNode
.setAttribute("type", "content");
368 panelNode
.appendChild(iframeNode
);
371 let popupSet
= document
.getElementById("mainPopupSet");
372 popupSet
.appendChild(panelNode
);
373 panelNode
.addEventListener(
376 PanelMultiView
.removePopup(panelNode
);
382 panelNode
.addEventListener(
385 action
.onIframeShowing(iframeNode
, panelNode
);
389 panelNode
.addEventListener(
396 panelNode
.addEventListener(
399 action
.onIframeHiding(iframeNode
, panelNode
);
403 panelNode
.addEventListener(
406 action
.onIframeHidden(iframeNode
, panelNode
);
413 action
.onSubviewPlaced(panelViewNode
);
414 panelNode
.addEventListener(
417 action
.onSubviewShowing(panelViewNode
);
427 * Returns the node in the urlbar to which popups for the given action should
428 * be anchored. If the action is null, a sensible anchor is returned.
430 * @param action (PageActions.Action, optional)
431 * The action you want to anchor.
432 * @param event (DOM event, optional)
433 * This is used to display the feedback panel on the right node when
434 * the command can be invoked from both the main panel and another
435 * location, such as an activated action panel or a button.
436 * @return (DOM node) The node to which the action should be anchored.
438 panelAnchorNodeForAction(action
, event
) {
439 if (event
&& event
.target
.closest("panel") == this.panelNode
) {
440 return this.mainButtonNode
;
443 // Try each of the following nodes in order, using the first that's visible.
444 let potentialAnchorNodeIDs
= [
445 action
&& action
.anchorIDOverride
,
446 action
&& this.urlbarButtonNodeIDForActionID(action
.id
),
447 this.mainButtonNode
.id
,
449 "urlbar-search-button",
451 for (let id
of potentialAnchorNodeIDs
) {
453 let node
= document
.getElementById(id
);
454 if (node
&& !node
.hidden
) {
455 let bounds
= window
.windowUtils
.getBoundsWithoutFlushing(node
);
456 if (bounds
.height
> 0 && bounds
.width
> 0) {
462 let id
= action
? action
.id
: "<no action>";
463 throw new Error(`PageActions: No anchor node for ${id}`);
466 get activatedActionPanelNode() {
467 return document
.getElementById(this._activatedActionPanelID
);
470 get _activatedActionPanelID() {
471 return "pageActionActivatedActionPanel";
475 * Adds or removes as necessary a DOM node for the given action in the urlbar.
477 * @param action (PageActions.Action, required)
478 * The action to place.
480 placeActionInUrlbar(action
) {
481 let id
= this.urlbarButtonNodeIDForActionID(action
.id
);
482 let node
= document
.getElementById(id
);
484 if (!action
.shouldShowInUrlbar(window
)) {
486 if (action
.__urlbarNodeInMarkup
) {
495 let newlyPlaced
= false;
496 if (action
.__urlbarNodeInMarkup
) {
497 this._maybeNotifyBeforePlacedInWindow(action
);
498 // Allow the consumer to add the node in response to the
499 // onBeforePlacedInWindow notification.
500 node
= document
.getElementById(id
);
504 newlyPlaced
= node
.hidden
;
508 this._maybeNotifyBeforePlacedInWindow(action
);
509 node
= this._makeUrlbarButtonNode(action
);
517 let insertBeforeNode
= this._getNextNode(action
, true);
518 this.mainButtonNode
.parentNode
.insertBefore(node
, insertBeforeNode
);
519 this.updateAction(action
, null, {
522 action
.onPlacedInUrlbar(node
);
525 _makeUrlbarButtonNode(action
) {
526 let buttonNode
= document
.createXULElement("image");
527 buttonNode
.classList
.add("urlbar-icon", "urlbar-page-action");
528 buttonNode
.setAttribute("actionid", action
.id
);
529 buttonNode
.setAttribute("role", "button");
530 let commandHandler
= event
=> {
531 this.doCommandForAction(action
, event
, buttonNode
);
533 buttonNode
.addEventListener("click", commandHandler
);
534 buttonNode
.addEventListener("keypress", commandHandler
);
539 * Removes all the DOM nodes of the given action.
541 * @param action (PageActions.Action, required)
542 * The action to remove.
544 removeAction(action
) {
545 this._removeActionFromPanel(action
);
546 this._removeActionFromUrlbar(action
);
547 action
.onRemovedFromWindow(window
);
550 _removeActionFromUrlbar(action
) {
551 let node
= this.urlbarButtonNodeForActionID(action
.id
);
558 * Updates the DOM nodes of an action to reflect either a changed property or
561 * @param action (PageActions.Action, required)
562 * The action to update.
563 * @param propertyName (string, optional)
564 * The name of the property to update. If not given, then DOM nodes
565 * will be updated to reflect the current values of all properties.
566 * @param opts (object, optional)
567 * - panelNode: The action's node in the panel to update.
568 * - urlbarNode: The action's node in the urlbar to update.
569 * - value: If a property name is passed, this argument may contain
570 * its current value, in order to prevent a further look-up.
572 updateAction(action
, propertyName
= null, opts
= {}) {
573 let anyNodeGiven
= "panelNode" in opts
|| "urlbarNode" in opts
;
574 let panelNode
= anyNodeGiven
575 ? opts
.panelNode
|| null
576 : this.panelButtonNodeForActionID(action
.id
);
577 let urlbarNode
= anyNodeGiven
578 ? opts
.urlbarNode
|| null
579 : this.urlbarButtonNodeForActionID(action
.id
);
580 let value
= opts
.value
|| undefined;
582 this[this._updateMethods
[propertyName
]](
589 for (let name
of ["iconURL", "title", "tooltip", "wantsSubview"]) {
590 this[this._updateMethods
[name
]](action
, panelNode
, urlbarNode
, value
);
596 disabled
: "_updateActionDisabled",
597 iconURL
: "_updateActionIconURL",
598 title
: "_updateActionLabeling",
599 tooltip
: "_updateActionTooltip",
600 wantsSubview
: "_updateActionWantsSubview",
603 _updateActionDisabled(
607 disabled
= action
.getDisabled(window
)
609 if (action
.__transient
) {
610 this.placeActionInPanel(action
);
612 this._updateActionDisabledInPanel(action
, panelNode
, disabled
);
614 this.placeActionInUrlbar(action
);
617 _updateActionDisabledInPanel(
620 disabled
= action
.getDisabled(window
)
624 panelNode
.setAttribute("disabled", "true");
626 panelNode
.removeAttribute("disabled");
631 _updateActionIconURL(
635 properties
= action
.getIconProperties(window
)
637 for (let [prop
, value
] of Object
.entries(properties
)) {
639 panelNode
.style
.setProperty(prop
, value
);
642 urlbarNode
.style
.setProperty(prop
, value
);
647 _updateActionLabeling(
651 title
= action
.getTitle(window
)
653 let tabCount
= gBrowser
.selectedTabs
.length
;
655 if (action
.panelFluentID
) {
656 document
.l10n
.setAttributes(panelNode
, action
.panelFluentID
, {
660 panelNode
.setAttribute("label", title
);
664 // Some actions (e.g. Save Page to Pocket) have a wrapper node with the
665 // actual controls inside that wrapper. The wrapper is semantically
666 // meaningless, so it doesn't get reflected in the accessibility tree.
667 // In these cases, we don't want to set aria-label because that will
668 // force the element to be exposed to accessibility.
669 if (urlbarNode
.nodeName
!= "hbox") {
670 urlbarNode
.setAttribute("aria-label", title
);
672 // tooltiptext falls back to the title, so update it too if necessary.
673 let tooltip
= action
.getTooltip(window
);
675 if (action
.urlbarFluentID
) {
676 document
.l10n
.setAttributes(urlbarNode
, action
.urlbarFluentID
, {
680 urlbarNode
.setAttribute("tooltiptext", title
);
686 _updateActionTooltip(
690 tooltip
= action
.getTooltip(window
)
694 tooltip
= action
.getTitle(window
);
697 urlbarNode
.setAttribute("tooltiptext", tooltip
);
702 _updateActionWantsSubview(
706 wantsSubview
= action
.getWantsSubview(window
)
711 let panelViewID
= this._panelViewNodeIDForActionID(action
.id
, false);
712 let panelViewNode
= document
.getElementById(panelViewID
);
713 panelNode
.classList
.toggle("subviewbutton-nav", wantsSubview
);
716 panelViewNode
.remove();
720 if (!panelViewNode
) {
721 panelViewNode
= this._makePanelViewNodeForAction(action
, false);
722 this.multiViewNode
.appendChild(panelViewNode
);
723 action
.onSubviewPlaced(panelViewNode
);
727 doCommandForAction(action
, event
, buttonNode
) {
728 // On mac, ctrl-click will send a context menu event from the widget, so we
729 // don't want to handle the click event when ctrl key is pressed.
732 event
.type
== "click" &&
733 (event
.button
!= 0 ||
734 (AppConstants
.platform
== "macosx" && event
.ctrlKey
))
738 if (event
&& event
.type
== "keypress") {
739 if (event
.key
!= " " && event
.key
!= "Enter") {
742 event
.stopPropagation();
744 // If we're in the panel, open a subview inside the panel:
745 // Note that we can't use this.panelNode.contains(buttonNode) here
746 // because of XBL boundaries breaking Element.contains.
748 action
.getWantsSubview(window
) &&
750 buttonNode
.closest("panel") == this.panelNode
752 let panelViewNodeID
= this._panelViewNodeIDForActionID(action
.id
, false);
753 let panelViewNode
= document
.getElementById(panelViewNodeID
);
754 action
.onSubviewShowing(panelViewNode
);
755 this.multiViewNode
.showSubView(panelViewNode
, buttonNode
);
758 // Otherwise, hide the main popup in case it was open:
759 PanelMultiView
.hidePopup(this.panelNode
);
761 let aaPanelNode
= this.activatedActionPanelNode
;
762 if (!aaPanelNode
|| aaPanelNode
.getAttribute("actionID") != action
.id
) {
763 action
.onCommand(event
, buttonNode
);
765 if (action
.getWantsSubview(window
) || action
.wantsIframe
) {
766 this.togglePanelForAction(action
, null, event
);
771 * Returns the action for a node.
773 * @param node (DOM node, required)
774 * A button DOM node, either one that's shown in the page action panel
776 * @return (PageAction.Action) If the node has a related action and the action
777 * is not a separator, then the action is returned. Otherwise null is
780 actionForNode(node
) {
784 let actionID
= this._actionIDForNodeID(node
.id
);
785 let action
= PageActions
.actionForID(actionID
);
787 // The given node may be an ancestor of a node corresponding to an action,
788 // like how #star-button is contained in #star-button-box, the latter
789 // being the bookmark action's node. Look up the ancestor chain.
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 acton was found.
796 actionID
= this._actionIDForNodeID(n
.id
);
797 action
= PageActions
.actionForID(actionID
);
800 return action
&& !action
.__isSeparator
? action
: null;
804 * The given action's top-level button in the main panel.
806 * @param actionID (string, required)
808 * @return (DOM node) The action's button in the main panel.
810 panelButtonNodeForActionID(actionID
) {
811 return document
.getElementById(this.panelButtonNodeIDForActionID(actionID
));
815 * The ID of the given action's top-level button in the main panel.
817 * @param actionID (string, required)
819 * @return (string) The ID of the action's button in the main panel.
821 panelButtonNodeIDForActionID(actionID
) {
822 return `pageAction-panel-${actionID}`;
826 * The given action's button in the urlbar.
828 * @param actionID (string, required)
830 * @return (DOM node) The action's urlbar button node.
832 urlbarButtonNodeForActionID(actionID
) {
833 return document
.getElementById(
834 this.urlbarButtonNodeIDForActionID(actionID
)
839 * The ID of the given action's button in the urlbar.
841 * @param actionID (string, required)
843 * @return (string) The ID of the action's urlbar button node.
845 urlbarButtonNodeIDForActionID(actionID
) {
846 let action
= PageActions
.actionForID(actionID
);
847 if (action
&& action
.urlbarIDOverride
) {
848 return action
.urlbarIDOverride
;
850 return `pageAction-urlbar-${actionID}`;
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`;
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
) {
865 let match
= nodeID
.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
869 // Check all the urlbar ID overrides.
870 for (let action
of PageActions
.actions
) {
871 if (action
.urlbarIDOverride
&& action
.urlbarIDOverride
== nodeID
) {
879 * Call this when the main page action button in the urlbar is activated.
881 * @param event (DOM event, required)
882 * The click or whatever event.
884 mainButtonClicked(event
) {
885 event
.stopPropagation();
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
)
899 // If the activated-action panel is open and anchored to the main button,
901 let panelNode
= this.activatedActionPanelNode
;
902 if (panelNode
&& panelNode
.anchorNode
.id
== this.mainButtonNode
.id
) {
903 PanelMultiView
.hidePopup(panelNode
);
907 if (this.panelNode
.state
== "open") {
908 PanelMultiView
.hidePopup(this.panelNode
);
909 } else if (this.panelNode
.state
== "closed") {
910 this.showPanel(event
);
915 * Show the page action panel
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)
921 showPanel(event
= null) {
922 this.panelNode
.hidden
= false;
923 this.mainButtonNode
.setAttribute("open", "true");
924 PanelMultiView
.openPopup(this.panelNode
, this.mainButtonNode
, {
925 position
: "bottomcenter topright",
927 }).catch(Cu
.reportError
);
931 * Call this on the context menu's popupshowing event.
933 * @param event (DOM event, required)
934 * The popupshowing event.
935 * @param popup (DOM node, required)
936 * The context menu popup DOM node.
938 async
onContextMenuShowing(event
, popup
) {
939 if (event
.target
!= popup
) {
943 let action
= this.actionForNode(popup
.triggerNode
);
946 // In Proton, only extension actions provide a context menu.
947 (UrlbarPrefs
.get("browser.proton.urlbar.enabled") && !action
.extensionID
)
949 this._contextAction
= null;
950 event
.preventDefault();
953 this._contextAction
= action
;
956 if (this._contextAction
._isMozillaAction
) {
957 state
= this._contextAction
.pinnedToUrlbar
961 state
= this._contextAction
.pinnedToUrlbar
963 : "extensionUnpinned";
965 popup
.setAttribute("state", state
);
967 let removeExtension
= popup
.querySelector(".removeExtensionItem");
968 let { extensionID
} = this._contextAction
;
969 let addon
= extensionID
&& (await AddonManager
.getAddonByID(extensionID
));
970 removeExtension
.hidden
= !addon
;
972 removeExtension
.disabled
= !(
973 addon
.permissions
& AddonManager
.PERM_CAN_UNINSTALL
979 * Call this from the menu item in the context menu that toggles pinning.
981 togglePinningForContextAction() {
982 if (!this._contextAction
) {
985 let action
= this._contextAction
;
986 this._contextAction
= null;
988 action
.pinnedToUrlbar
= !action
.pinnedToUrlbar
;
989 BrowserUsageTelemetry
.recordWidgetChange(
991 action
.pinnedToUrlbar
? "page-action-buttons" : null,
997 * Call this from the menu item in the context menu that opens about:addons.
999 openAboutAddonsForContextAction() {
1000 if (!this._contextAction
) {
1003 let action
= this._contextAction
;
1004 this._contextAction
= null;
1006 AMTelemetry
.recordActionEvent({
1007 object
: "pageAction",
1009 extra
: { addonId
: action
.extensionID
},
1012 let viewID
= "addons://detail/" + encodeURIComponent(action
.extensionID
);
1013 window
.BrowserOpenAddonsMgr(viewID
);
1017 * Call this from the menu item in the context menu that removes an add-on.
1019 removeExtensionForContextAction() {
1020 if (!this._contextAction
) {
1023 let action
= this._contextAction
;
1024 this._contextAction
= null;
1026 BrowserAddonUI
.removeAddon(action
.extensionID
, "pageAction");
1029 _contextAction
: null,
1032 * We use this to set an attribute on the DOM node. If the attribute exists,
1033 * then we get the panel node's attribute and set it on the DOM node. Otherwise,
1034 * we get the title string and update the attribute with that value. The point is to map
1035 * attributes on the node to strings on the main panel. Use this for DOM
1036 * nodes that don't correspond to actions, like buttons in subviews.
1038 * @param node (DOM node, required)
1039 * The node you're setting up.
1040 * @param attrName (string, required)
1041 * The name of the attribute *on the node you're setting up*.
1043 takeNodeAttributeFromPanel(node
, attrName
) {
1044 let panelAttrName
= node
.getAttribute(attrName
);
1045 if (!panelAttrName
&& attrName
== "title") {
1047 panelAttrName
= node
.getAttribute(attrName
);
1049 if (panelAttrName
) {
1050 let attrValue
= this.panelNode
.getAttribute(panelAttrName
);
1052 node
.setAttribute(attrName
, attrValue
);
1058 * Call this on tab switch or when the current <browser>'s location changes.
1060 onLocationChange() {
1061 for (let action
of PageActions
.actions
) {
1062 action
.onLocationChange(window
);
1068 * Shows the feedback popup for an action.
1070 * @param action (PageActions.Action, required)
1071 * The action associated with the feedback.
1072 * @param event (DOM event, optional)
1073 * The event that triggered the feedback.
1074 * @param messageId (string, optional)
1075 * Can be used to set a message id that is different from the action id.
1077 function showBrowserPageActionFeedback(action
, event
= null, messageId
= null) {
1078 let anchor
= BrowserPageActions
.panelAnchorNodeForAction(action
, event
);
1080 ConfirmationHint
.show(anchor
, messageId
|| action
.id
, {
1086 // built-in actions below //////////////////////////////////////////////////////
1089 BrowserPageActions
.bookmark
= {
1090 onShowingInPanel(buttonNode
) {
1091 if (buttonNode
.label
== "null") {
1092 BookmarkingUI
.updateBookmarkPageMenuItem();
1096 onCommand(event
, buttonNode
) {
1097 PanelMultiView
.hidePopup(BrowserPageActions
.panelNode
);
1098 BookmarkingUI
.onStarCommand(event
);
1103 BrowserPageActions
.pinTab
= {
1105 let action
= PageActions
.actionForID("pinTab");
1107 // This action doesn't exist in Proton.
1110 let { pinned
} = gBrowser
.selectedTab
;
1113 fluentID
= "page-action-unpin-tab";
1115 fluentID
= "page-action-pin-tab";
1118 let panelButton
= BrowserPageActions
.panelButtonNodeForActionID(action
.id
);
1120 document
.l10n
.setAttributes(panelButton
, fluentID
+ "-panel");
1121 panelButton
.toggleAttribute("pinned", pinned
);
1123 let urlbarButton
= BrowserPageActions
.urlbarButtonNodeForActionID(
1127 document
.l10n
.setAttributes(urlbarButton
, fluentID
+ "-urlbar");
1128 urlbarButton
.toggleAttribute("pinned", pinned
);
1132 onCommand(event
, buttonNode
) {
1133 if (gBrowser
.selectedTab
.pinned
) {
1134 gBrowser
.unpinTab(gBrowser
.selectedTab
);
1136 gBrowser
.pinTab(gBrowser
.selectedTab
);
1142 BrowserPageActions
.copyURL
= {
1143 onCommand(event
, buttonNode
) {
1144 PanelMultiView
.hidePopup(BrowserPageActions
.panelNode
);
1145 Cc
["@mozilla.org/widget/clipboardhelper;1"]
1146 .getService(Ci
.nsIClipboardHelper
)
1148 gURLBar
.makeURIReadable(gBrowser
.selectedBrowser
.currentURI
).displaySpec
1150 let action
= PageActions
.actionForID("copyURL");
1151 showBrowserPageActionFeedback(action
, event
);
1156 BrowserPageActions
.emailLink
= {
1157 onCommand(event
, buttonNode
) {
1158 PanelMultiView
.hidePopup(BrowserPageActions
.panelNode
);
1159 MailIntegration
.sendLinkForBrowser(gBrowser
.selectedBrowser
);
1164 BrowserPageActions
.sendToDevice
= {
1165 onBeforePlacedInWindow(browserWindow
) {
1166 this._updateTitle();
1167 gBrowser
.addEventListener("TabMultiSelect", event
=> {
1168 this._updateTitle();
1172 // The action's title in this window depends on the number of tabs that are
1175 let action
= PageActions
.actionForID("sendToDevice");
1176 let tabCount
= gBrowser
.selectedTabs
.length
;
1178 let panelButton
= BrowserPageActions
.panelButtonNodeForActionID(action
.id
);
1180 document
.l10n
.setAttributes(panelButton
, action
.panelFluentID
, {
1184 let urlbarButton
= BrowserPageActions
.urlbarButtonNodeForActionID(
1188 document
.l10n
.setAttributes(urlbarButton
, action
.urlbarFluentID
, {
1194 onSubviewPlaced(panelViewNode
) {
1195 let bodyNode
= panelViewNode
.querySelector(".panel-subview-body");
1196 let notReady
= document
.createXULElement("toolbarbutton");
1197 notReady
.classList
.add(
1199 "subviewbutton-iconic",
1200 "pageAction-sendToDevice-notReady"
1202 document
.l10n
.setAttributes(notReady
, "page-action-send-tab-not-ready");
1203 notReady
.setAttribute("disabled", "true");
1204 bodyNode
.appendChild(notReady
);
1205 for (let node
of bodyNode
.children
) {
1206 BrowserPageActions
.takeNodeAttributeFromPanel(node
, "title");
1207 BrowserPageActions
.takeNodeAttributeFromPanel(node
, "shortcut");
1211 onLocationChange() {
1212 let action
= PageActions
.actionForID("sendToDevice");
1214 // This action doesn't exist in Proton.
1217 let browser
= gBrowser
.selectedBrowser
;
1218 let url
= browser
.currentURI
;
1219 action
.setDisabled(!BrowserUtils
.isShareableURL(url
), window
);
1222 onShowingSubview(panelViewNode
) {
1223 gSync
.populateSendTabToDevicesView(panelViewNode
);
1227 // add search engine
1228 BrowserPageActions
.addSearchEngine
= {
1230 return PageActions
.actionForID("addSearchEngine");
1234 return gBrowser
.selectedBrowser
.engines
|| [];
1238 delete this.strings
;
1239 let uri
= "chrome://browser/locale/search.properties";
1240 return (this.strings
= Services
.strings
.createBundle(uri
));
1245 // This action doesn't exist in Proton.
1248 // As a slight optimization, if the action isn't in the urlbar, don't do
1249 // anything here except disable it. The action's panel nodes are updated
1250 // when the panel is shown.
1251 this.action
.setDisabled(!this.engines
.length
, window
);
1252 if (this.action
.shouldShowInUrlbar(window
)) {
1253 this._updateTitleAndIcon();
1257 _updateTitleAndIcon() {
1258 if (!this.engines
.length
) {
1261 let title
= this.strings
.GetStringFromName("searchAddFoundEngine2");
1262 this.action
.setTitle(title
, window
);
1263 this.action
.setIconURL(this.engines
[0].icon
, window
);
1266 onShowingInPanel() {
1267 this._updateTitleAndIcon();
1268 this.action
.setWantsSubview(this.engines
.length
> 1, window
);
1269 let button
= BrowserPageActions
.panelButtonNodeForActionID(this.action
.id
);
1270 button
.setAttribute("image", this.engines
[0].icon
);
1271 button
.setAttribute("uri", this.engines
[0].uri
);
1272 button
.setAttribute("crop", "center");
1275 onSubviewShowing(panelViewNode
) {
1276 let body
= panelViewNode
.querySelector(".panel-subview-body");
1277 while (body
.firstChild
) {
1278 body
.firstChild
.remove();
1280 for (let engine
of this.engines
) {
1281 let button
= document
.createXULElement("toolbarbutton");
1282 button
.classList
.add("subviewbutton", "subviewbutton-iconic");
1283 button
.setAttribute("label", engine
.title
);
1284 button
.setAttribute("image", engine
.icon
);
1285 button
.setAttribute("uri", engine
.uri
);
1286 button
.addEventListener("command", event
=> {
1287 let panelNode
= panelViewNode
.closest("panel");
1288 PanelMultiView
.hidePopup(panelNode
);
1289 this._installEngine(
1290 button
.getAttribute("uri"),
1291 button
.getAttribute("image")
1294 body
.appendChild(button
);
1298 onCommand(event
, buttonNode
) {
1299 if (!buttonNode
.closest("panel")) {
1300 // The urlbar button was clicked. It should have a subview if there are
1302 let manyEngines
= this.engines
.length
> 1;
1303 this.action
.setWantsSubview(manyEngines
, window
);
1308 // Either the panel button or urlbar button was clicked -- not a button in
1309 // the subview -- but in either case, there's only one search engine.
1310 // (Because this method isn't called when the panel button is clicked and it
1311 // shows a subview, and the many-engines case for the urlbar returned early
1313 let engine
= this.engines
[0];
1314 this._installEngine(engine
.uri
, engine
.icon
);
1317 _installEngine(uri
, image
) {
1318 SearchUIUtils
.addOpenSearchEngine(
1321 gBrowser
.selectedBrowser
.browsingContext
1325 showBrowserPageActionFeedback(this.action
);
1328 .catch(console
.error
);
1333 BrowserPageActions
.shareURL
= {
1334 onCommand(event
, buttonNode
) {
1335 let browser
= gBrowser
.selectedBrowser
;
1336 let currentURI
= gURLBar
.makeURIReadable(browser
.currentURI
).displaySpec
;
1337 this._windowsUIUtils
.shareUrl(currentURI
, browser
.contentTitle
);
1340 onShowingInPanel(buttonNode
) {
1341 this._cached
= false;
1344 onShowingSubview(panelViewNode
) {
1345 let bodyNode
= panelViewNode
.querySelector(".panel-subview-body");
1347 // We cache the providers + the UI if the user selects the share
1348 // panel multiple times while the panel is open.
1349 if (this._cached
&& bodyNode
.children
.length
) {
1353 let sharingService
= this._sharingService
;
1354 let url
= gBrowser
.selectedBrowser
.currentURI
;
1355 let currentURI
= gURLBar
.makeURIReadable(url
).displaySpec
;
1356 let shareProviders
= sharingService
.getSharingProviders(currentURI
);
1357 let fragment
= document
.createDocumentFragment();
1359 let onCommand
= event
=> {
1360 let shareName
= event
.target
.getAttribute("share-name");
1362 sharingService
.shareUrl(
1365 gBrowser
.selectedBrowser
.contentTitle
1367 } else if (event
.target
.classList
.contains("share-more-button")) {
1368 sharingService
.openSharingPreferences();
1370 PanelMultiView
.hidePopup(BrowserPageActions
.panelNode
);
1373 shareProviders
.forEach(function(share
) {
1374 let item
= document
.createXULElement("toolbarbutton");
1375 item
.setAttribute("label", share
.menuItemTitle
);
1376 item
.setAttribute("share-name", share
.name
);
1377 item
.setAttribute("image", share
.image
);
1378 item
.classList
.add("subviewbutton", "subviewbutton-iconic");
1379 item
.addEventListener("command", onCommand
);
1380 fragment
.appendChild(item
);
1383 let item
= document
.createXULElement("toolbarbutton");
1384 document
.l10n
.setAttributes(item
, "page-action-share-more-panel");
1387 "subviewbutton-iconic",
1390 item
.addEventListener("command", onCommand
);
1391 fragment
.appendChild(item
);
1393 while (bodyNode
.firstChild
) {
1394 bodyNode
.firstChild
.remove();
1396 bodyNode
.appendChild(fragment
);
1397 this._cached
= true;
1401 // Attach sharingService here so tests can override the implementation
1402 XPCOMUtils
.defineLazyServiceGetters(BrowserPageActions
.shareURL
, {
1404 "@mozilla.org/widget/macsharingservice;1",
1405 "nsIMacSharingService",
1407 _windowsUIUtils
: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],