Bug 1700364 - Page action buttons should not be remove-able in Proton. r=adw
[gecko.git] / browser / base / content / browser-pageActions.js
blob073404a8db2be48ca0f356c39098d1cbb2736111
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(
6 this,
7 "SearchUIUtils",
8 "resource:///modules/SearchUIUtils.jsm"
9 );
11 var BrowserPageActions = {
12 _panelNode: null,
13 /**
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"));
21 /**
22 * The main page action panel DOM node (DOM node)
24 get panelNode() {
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);
33 /**
34 * The panelmultiview node in the main page action panel (DOM node)
36 get multiViewNode() {
37 delete this.multiViewNode;
38 return (this.multiViewNode = document.getElementById(
39 "pageActionPanelMultiView"
40 ));
43 /**
44 * The main panelview node in the main page action panel (DOM node)
46 get mainViewNode() {
47 delete this.mainViewNode;
48 return (this.mainViewNode = document.getElementById(
49 "pageActionPanelMainView"
50 ));
53 /**
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(
59 ".panel-subview-body"
60 ));
63 /**
64 * Inits. Call to init.
66 init() {
67 this.placeAllActionsInUrlbar();
68 this._onPanelShowing = this._onPanelShowing.bind(this);
71 _onPanelShowing() {
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: [],
91 /**
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.
104 initializePanel() {
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);
142 } else {
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);
151 } else {
152 this._removeActionFromPanel(action);
156 _addActionToPanel(action) {
157 let id = this.panelButtonNodeIDForActionID(action.id);
158 let node = document.getElementById(id);
159 if (node) {
160 return;
162 this._maybeNotifyBeforePlacedInWindow(action);
163 node = this._makePanelButtonNodeForAction(action);
164 node.id = id;
165 let insertBeforeNode = this._getNextNode(action, false);
166 this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
167 this.updateAction(action, null, {
168 panelNode: node,
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);
183 if (!node) {
184 return;
186 node.remove();
187 if (action.getWantsSubview(window)) {
188 let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
189 let panelViewNode = document.getElementById(panelViewNodeID);
190 if (panelViewNode) {
191 panelViewNode.remove();
194 this._addOrRemoveSeparatorsInPanel();
197 _addOrRemoveSeparatorsInPanel() {
198 let actions = PageActions.actionsInPanel(window);
199 let ids = [
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);
205 if (sep) {
206 this._addActionToPanel(sep);
207 } else {
208 let node = this.panelButtonNodeForActionID(id);
209 if (node) {
210 node.remove();
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
223 * into the panel.
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);
232 if (index < 0) {
233 return null;
235 for (let i = index + 1; i < actions.length; i++) {
236 let node = forUrlbar
237 ? this.urlbarButtonNodeForActionID(actions[i].id)
238 : this.panelButtonNodeForActionID(actions[i].id);
239 if (node) {
240 return node;
243 return null;
246 _maybeNotifyBeforePlacedInWindow(action) {
247 if (!this._isActionPlacedInWindow(action)) {
248 action.onBeforePlacedInWindow(window);
252 _isActionPlacedInWindow(action) {
253 if (this.panelButtonNodeForActionID(action.id)) {
254 return true;
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");
263 return node;
265 let buttonNode = document.createXULElement("toolbarbutton");
266 buttonNode.classList.add(
267 "subviewbutton",
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);
278 return 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;
308 if (panelNode) {
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);
314 return;
316 if (aaPanelNode) {
317 PanelMultiView.hidePopup(aaPanelNode);
319 } else if (aaPanelNode) {
320 PanelMultiView.hidePopup(aaPanelNode);
321 return;
322 } else {
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(
332 "popuphiding",
333 () => {
334 anchorNode.removeAttribute("open");
336 { once: true }
339 PanelMultiView.openPopup(panelNode, anchorNode, {
340 position: "bottomcenter topright",
341 triggerEvent: event,
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(
374 "popuphidden",
375 () => {
376 PanelMultiView.removePopup(panelNode);
378 { once: true }
381 if (iframeNode) {
382 panelNode.addEventListener(
383 "popupshowing",
384 () => {
385 action.onIframeShowing(iframeNode, panelNode);
387 { once: true }
389 panelNode.addEventListener(
390 "popupshown",
391 () => {
392 iframeNode.focus();
394 { once: true }
396 panelNode.addEventListener(
397 "popuphiding",
398 () => {
399 action.onIframeHiding(iframeNode, panelNode);
401 { once: true }
403 panelNode.addEventListener(
404 "popuphidden",
405 () => {
406 action.onIframeHidden(iframeNode, panelNode);
408 { once: true }
412 if (panelViewNode) {
413 action.onSubviewPlaced(panelViewNode);
414 panelNode.addEventListener(
415 "popupshowing",
416 () => {
417 action.onSubviewShowing(panelViewNode);
419 { once: true }
423 return panelNode;
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,
448 "identity-icon",
449 "urlbar-search-button",
451 for (let id of potentialAnchorNodeIDs) {
452 if (id) {
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) {
457 return node;
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)) {
485 if (node) {
486 if (action.__urlbarNodeInMarkup) {
487 node.hidden = true;
488 } else {
489 node.remove();
492 return;
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);
501 if (!node) {
502 return;
504 newlyPlaced = node.hidden;
505 node.hidden = false;
506 } else if (!node) {
507 newlyPlaced = true;
508 this._maybeNotifyBeforePlacedInWindow(action);
509 node = this._makeUrlbarButtonNode(action);
510 node.id = id;
513 if (!newlyPlaced) {
514 return;
517 let insertBeforeNode = this._getNextNode(action, true);
518 this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
519 this.updateAction(action, null, {
520 urlbarNode: node,
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);
535 return buttonNode;
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);
552 if (node) {
553 node.remove();
558 * Updates the DOM nodes of an action to reflect either a changed property or
559 * all properties.
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;
581 if (propertyName) {
582 this[this._updateMethods[propertyName]](
583 action,
584 panelNode,
585 urlbarNode,
586 value
588 } else {
589 for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
590 this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
595 _updateMethods: {
596 disabled: "_updateActionDisabled",
597 iconURL: "_updateActionIconURL",
598 title: "_updateActionLabeling",
599 tooltip: "_updateActionTooltip",
600 wantsSubview: "_updateActionWantsSubview",
603 _updateActionDisabled(
604 action,
605 panelNode,
606 urlbarNode,
607 disabled = action.getDisabled(window)
609 if (action.__transient) {
610 this.placeActionInPanel(action);
611 } else {
612 this._updateActionDisabledInPanel(action, panelNode, disabled);
614 this.placeActionInUrlbar(action);
617 _updateActionDisabledInPanel(
618 action,
619 panelNode,
620 disabled = action.getDisabled(window)
622 if (panelNode) {
623 if (disabled) {
624 panelNode.setAttribute("disabled", "true");
625 } else {
626 panelNode.removeAttribute("disabled");
631 _updateActionIconURL(
632 action,
633 panelNode,
634 urlbarNode,
635 properties = action.getIconProperties(window)
637 for (let [prop, value] of Object.entries(properties)) {
638 if (panelNode) {
639 panelNode.style.setProperty(prop, value);
641 if (urlbarNode) {
642 urlbarNode.style.setProperty(prop, value);
647 _updateActionLabeling(
648 action,
649 panelNode,
650 urlbarNode,
651 title = action.getTitle(window)
653 let tabCount = gBrowser.selectedTabs.length;
654 if (panelNode) {
655 if (action.panelFluentID) {
656 document.l10n.setAttributes(panelNode, action.panelFluentID, {
657 tabCount,
659 } else {
660 panelNode.setAttribute("label", title);
663 if (urlbarNode) {
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);
674 if (!tooltip) {
675 if (action.urlbarFluentID) {
676 document.l10n.setAttributes(urlbarNode, action.urlbarFluentID, {
677 tabCount,
679 } else {
680 urlbarNode.setAttribute("tooltiptext", title);
686 _updateActionTooltip(
687 action,
688 panelNode,
689 urlbarNode,
690 tooltip = action.getTooltip(window)
692 if (urlbarNode) {
693 if (!tooltip) {
694 tooltip = action.getTitle(window);
696 if (tooltip) {
697 urlbarNode.setAttribute("tooltiptext", tooltip);
702 _updateActionWantsSubview(
703 action,
704 panelNode,
705 urlbarNode,
706 wantsSubview = action.getWantsSubview(window)
708 if (!panelNode) {
709 return;
711 let panelViewID = this._panelViewNodeIDForActionID(action.id, false);
712 let panelViewNode = document.getElementById(panelViewID);
713 panelNode.classList.toggle("subviewbutton-nav", wantsSubview);
714 if (!wantsSubview) {
715 if (panelViewNode) {
716 panelViewNode.remove();
718 return;
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.
730 if (
731 event &&
732 event.type == "click" &&
733 (event.button != 0 ||
734 (AppConstants.platform == "macosx" && event.ctrlKey))
736 return;
738 if (event && event.type == "keypress") {
739 if (event.key != " " && event.key != "Enter") {
740 return;
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.
747 if (
748 action.getWantsSubview(window) &&
749 buttonNode &&
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);
756 return;
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
775 * or the urlbar.
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
778 * returned.
780 actionForNode(node) {
781 if (!node) {
782 return null;
784 let actionID = this._actionIDForNodeID(node.id);
785 let action = PageActions.actionForID(actionID);
786 if (!action) {
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.
794 break;
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)
807 * The action ID.
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)
818 * The action ID.
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)
829 * The action ID.
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)
842 * The action ID.
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) {
862 if (!nodeID) {
863 return null;
865 let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
866 if (match) {
867 return match[1];
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;
875 return null;
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();
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)
896 return;
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;
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",
926 triggerEvent: event,
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) {
940 return;
943 let action = this.actionForNode(popup.triggerNode);
944 if (
945 !action ||
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();
951 return;
953 this._contextAction = action;
955 let state;
956 if (this._contextAction._isMozillaAction) {
957 state = this._contextAction.pinnedToUrlbar
958 ? "builtInPinned"
959 : "builtInUnpinned";
960 } else {
961 state = this._contextAction.pinnedToUrlbar
962 ? "extensionPinned"
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;
971 if (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) {
983 return;
985 let action = this._contextAction;
986 this._contextAction = null;
988 action.pinnedToUrlbar = !action.pinnedToUrlbar;
989 BrowserUsageTelemetry.recordWidgetChange(
990 action.id,
991 action.pinnedToUrlbar ? "page-action-buttons" : null,
992 "pageaction-context"
997 * Call this from the menu item in the context menu that opens about:addons.
999 openAboutAddonsForContextAction() {
1000 if (!this._contextAction) {
1001 return;
1003 let action = this._contextAction;
1004 this._contextAction = null;
1006 AMTelemetry.recordActionEvent({
1007 object: "pageAction",
1008 action: "manage",
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) {
1021 return;
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") {
1046 attrName = "label";
1047 panelAttrName = node.getAttribute(attrName);
1049 if (panelAttrName) {
1050 let attrValue = this.panelNode.getAttribute(panelAttrName);
1051 if (attrValue) {
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, {
1081 event,
1082 hideArrow: true,
1086 // built-in actions below //////////////////////////////////////////////////////
1088 // bookmark
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);
1102 // pin tab
1103 BrowserPageActions.pinTab = {
1104 updateState() {
1105 let action = PageActions.actionForID("pinTab");
1106 if (!action) {
1107 // This action doesn't exist in Proton.
1108 return;
1110 let { pinned } = gBrowser.selectedTab;
1111 let fluentID;
1112 if (pinned) {
1113 fluentID = "page-action-unpin-tab";
1114 } else {
1115 fluentID = "page-action-pin-tab";
1118 let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
1119 if (panelButton) {
1120 document.l10n.setAttributes(panelButton, fluentID + "-panel");
1121 panelButton.toggleAttribute("pinned", pinned);
1123 let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
1124 action.id
1126 if (urlbarButton) {
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);
1135 } else {
1136 gBrowser.pinTab(gBrowser.selectedTab);
1141 // copy URL
1142 BrowserPageActions.copyURL = {
1143 onCommand(event, buttonNode) {
1144 PanelMultiView.hidePopup(BrowserPageActions.panelNode);
1145 Cc["@mozilla.org/widget/clipboardhelper;1"]
1146 .getService(Ci.nsIClipboardHelper)
1147 .copyString(
1148 gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec
1150 let action = PageActions.actionForID("copyURL");
1151 showBrowserPageActionFeedback(action, event);
1155 // email link
1156 BrowserPageActions.emailLink = {
1157 onCommand(event, buttonNode) {
1158 PanelMultiView.hidePopup(BrowserPageActions.panelNode);
1159 MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
1163 // send to device
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
1173 // selected.
1174 _updateTitle() {
1175 let action = PageActions.actionForID("sendToDevice");
1176 let tabCount = gBrowser.selectedTabs.length;
1178 let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
1179 if (panelButton) {
1180 document.l10n.setAttributes(panelButton, action.panelFluentID, {
1181 tabCount,
1184 let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
1185 action.id
1187 if (urlbarButton) {
1188 document.l10n.setAttributes(urlbarButton, action.urlbarFluentID, {
1189 tabCount,
1194 onSubviewPlaced(panelViewNode) {
1195 let bodyNode = panelViewNode.querySelector(".panel-subview-body");
1196 let notReady = document.createXULElement("toolbarbutton");
1197 notReady.classList.add(
1198 "subviewbutton",
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");
1213 if (!action) {
1214 // This action doesn't exist in Proton.
1215 return;
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 = {
1229 get action() {
1230 return PageActions.actionForID("addSearchEngine");
1233 get engines() {
1234 return gBrowser.selectedBrowser.engines || [];
1237 get strings() {
1238 delete this.strings;
1239 let uri = "chrome://browser/locale/search.properties";
1240 return (this.strings = Services.strings.createBundle(uri));
1243 updateEngines() {
1244 if (!this.action) {
1245 // This action doesn't exist in Proton.
1246 return;
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) {
1259 return;
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
1301 // many engines.
1302 let manyEngines = this.engines.length > 1;
1303 this.action.setWantsSubview(manyEngines, window);
1304 if (manyEngines) {
1305 return;
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
1312 // above.)
1313 let engine = this.engines[0];
1314 this._installEngine(engine.uri, engine.icon);
1317 _installEngine(uri, image) {
1318 SearchUIUtils.addOpenSearchEngine(
1319 uri,
1320 image,
1321 gBrowser.selectedBrowser.browsingContext
1323 .then(result => {
1324 if (result) {
1325 showBrowserPageActionFeedback(this.action);
1328 .catch(console.error);
1332 // share URL
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) {
1350 return;
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");
1361 if (shareName) {
1362 sharingService.shareUrl(
1363 shareName,
1364 currentURI,
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");
1385 item.classList.add(
1386 "subviewbutton",
1387 "subviewbutton-iconic",
1388 "share-more-button"
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, {
1403 _sharingService: [
1404 "@mozilla.org/widget/macsharingservice;1",
1405 "nsIMacSharingService",
1407 _windowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],