summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-pageActions.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-pageActions.js')
-rw-r--r--browser/base/content/browser-pageActions.js1390
1 files changed, 1390 insertions, 0 deletions
diff --git a/browser/base/content/browser-pageActions.js b/browser/base/content/browser-pageActions.js
new file mode 100644
index 0000000000..8c896ba3b9
--- /dev/null
+++ b/browser/base/content/browser-pageActions.js
@@ -0,0 +1,1390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SearchUIUtils",
+ "resource:///modules/SearchUIUtils.jsm"
+);
+
+var BrowserPageActions = {
+ _panelNode: null,
+ /**
+ * The main page action button in the urlbar (DOM node)
+ */
+ get mainButtonNode() {
+ delete this.mainButtonNode;
+ return (this.mainButtonNode = document.getElementById("pageActionButton"));
+ },
+
+ /**
+ * The main page action panel DOM node (DOM node)
+ */
+ get panelNode() {
+ // Lazy load the page action panel the first time we need to display it
+ if (!this._panelNode) {
+ this.initializePanel();
+ }
+ delete this.panelNode;
+ return (this.panelNode = this._panelNode);
+ },
+
+ /**
+ * The panelmultiview node in the main page action panel (DOM node)
+ */
+ get multiViewNode() {
+ delete this.multiViewNode;
+ return (this.multiViewNode = document.getElementById(
+ "pageActionPanelMultiView"
+ ));
+ },
+
+ /**
+ * The main panelview node in the main page action panel (DOM node)
+ */
+ get mainViewNode() {
+ delete this.mainViewNode;
+ return (this.mainViewNode = document.getElementById(
+ "pageActionPanelMainView"
+ ));
+ },
+
+ /**
+ * The vbox body node in the main panelview node (DOM node)
+ */
+ get mainViewBodyNode() {
+ delete this.mainViewBodyNode;
+ return (this.mainViewBodyNode = this.mainViewNode.querySelector(
+ ".panel-subview-body"
+ ));
+ },
+
+ /**
+ * Inits. Call to init.
+ */
+ init() {
+ this.placeAllActionsInUrlbar();
+ this._onPanelShowing = this._onPanelShowing.bind(this);
+ },
+
+ _onPanelShowing() {
+ this.initializePanel();
+ for (let action of PageActions.actionsInPanel(window)) {
+ let buttonNode = this.panelButtonNodeForActionID(action.id);
+ action.onShowingInPanel(buttonNode);
+ }
+ },
+
+ placeLazyActionsInPanel() {
+ let actions = this._actionsToLazilyPlaceInPanel;
+ this._actionsToLazilyPlaceInPanel = [];
+ for (let action of actions) {
+ this._placeActionInPanelNow(action);
+ }
+ },
+
+ // Actions placed in the panel aren't actually placed until the panel is
+ // subsequently opened.
+ _actionsToLazilyPlaceInPanel: [],
+
+ /**
+ * Places all registered actions in the urlbar.
+ */
+ placeAllActionsInUrlbar() {
+ let urlbarActions = PageActions.actionsInUrlbar(window);
+ for (let action of urlbarActions) {
+ this.placeActionInUrlbar(action);
+ }
+ },
+
+ /**
+ * Initializes the panel if necessary.
+ */
+ initializePanel() {
+ // Lazy load the page action panel the first time we need to display it
+ if (!this._panelNode) {
+ let template = document.getElementById("pageActionPanelTemplate");
+ template.replaceWith(template.content);
+ this._panelNode = document.getElementById("pageActionPanel");
+ this._panelNode.addEventListener("popupshowing", this._onPanelShowing);
+ this._panelNode.addEventListener("popuphiding", () => {
+ this.mainButtonNode.removeAttribute("open");
+ });
+ }
+
+ for (let action of PageActions.actionsInPanel(window)) {
+ this.placeActionInPanel(action);
+ }
+ this.placeLazyActionsInPanel();
+ },
+
+ /**
+ * Adds or removes as necessary DOM nodes for the given action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeAction(action) {
+ this.placeActionInPanel(action);
+ this.placeActionInUrlbar(action);
+ },
+
+ /**
+ * Adds or removes as necessary DOM nodes for the action in the panel.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeActionInPanel(action) {
+ if (this._panelNode && this.panelNode.state != "closed") {
+ this._placeActionInPanelNow(action);
+ } else {
+ // Lazily place the action in the panel the next time it opens.
+ this._actionsToLazilyPlaceInPanel.push(action);
+ }
+ },
+
+ _placeActionInPanelNow(action) {
+ if (action.shouldShowInPanel(window)) {
+ this._addActionToPanel(action);
+ } else {
+ this._removeActionFromPanel(action);
+ }
+ },
+
+ _addActionToPanel(action) {
+ let id = this.panelButtonNodeIDForActionID(action.id);
+ let node = document.getElementById(id);
+ if (node) {
+ return;
+ }
+ this._maybeNotifyBeforePlacedInWindow(action);
+ node = this._makePanelButtonNodeForAction(action);
+ node.id = id;
+ let insertBeforeNode = this._getNextNode(action, false);
+ this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
+ this.updateAction(action, null, {
+ panelNode: node,
+ });
+ this._updateActionDisabledInPanel(action, node);
+ action.onPlacedInPanel(node);
+ this._addOrRemoveSeparatorsInPanel();
+ },
+
+ _removeActionFromPanel(action) {
+ let lazyIndex = this._actionsToLazilyPlaceInPanel.findIndex(
+ a => a.id == action.id
+ );
+ if (lazyIndex >= 0) {
+ this._actionsToLazilyPlaceInPanel.splice(lazyIndex, 1);
+ }
+ let node = this.panelButtonNodeForActionID(action.id);
+ if (!node) {
+ return;
+ }
+ node.remove();
+ if (action.getWantsSubview(window)) {
+ let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewNodeID);
+ if (panelViewNode) {
+ panelViewNode.remove();
+ }
+ }
+ this._addOrRemoveSeparatorsInPanel();
+ },
+
+ _addOrRemoveSeparatorsInPanel() {
+ let actions = PageActions.actionsInPanel(window);
+ let ids = [
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ ];
+ for (let id of ids) {
+ let sep = actions.find(a => a.id == id);
+ if (sep) {
+ this._addActionToPanel(sep);
+ } else {
+ let node = this.panelButtonNodeForActionID(id);
+ if (node) {
+ node.remove();
+ }
+ }
+ }
+ },
+
+ /**
+ * Returns the node before which an action's node should be inserted.
+ *
+ * @param action (PageActions.Action, required)
+ * The action that will be inserted.
+ * @param forUrlbar (bool, required)
+ * True if you're inserting into the urlbar, false if you're inserting
+ * into the panel.
+ * @return (DOM node, maybe null) The DOM node before which to insert the
+ * given action. Null if the action should be inserted at the end.
+ */
+ _getNextNode(action, forUrlbar) {
+ let actions = forUrlbar
+ ? PageActions.actionsInUrlbar(window)
+ : PageActions.actionsInPanel(window);
+ let index = actions.findIndex(a => a.id == action.id);
+ if (index < 0) {
+ return null;
+ }
+ for (let i = index + 1; i < actions.length; i++) {
+ let node = forUrlbar
+ ? this.urlbarButtonNodeForActionID(actions[i].id)
+ : this.panelButtonNodeForActionID(actions[i].id);
+ if (node) {
+ return node;
+ }
+ }
+ return null;
+ },
+
+ _maybeNotifyBeforePlacedInWindow(action) {
+ if (!this._isActionPlacedInWindow(action)) {
+ action.onBeforePlacedInWindow(window);
+ }
+ },
+
+ _isActionPlacedInWindow(action) {
+ if (this.panelButtonNodeForActionID(action.id)) {
+ return true;
+ }
+ let urlbarNode = this.urlbarButtonNodeForActionID(action.id);
+ return urlbarNode && !urlbarNode.hidden;
+ },
+
+ _makePanelButtonNodeForAction(action) {
+ if (action.__isSeparator) {
+ let node = document.createXULElement("toolbarseparator");
+ return node;
+ }
+ let buttonNode = document.createXULElement("toolbarbutton");
+ buttonNode.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "pageAction-panel-button"
+ );
+ if (action.isBadged) {
+ buttonNode.setAttribute("badged", "true");
+ }
+ buttonNode.setAttribute("actionid", action.id);
+ buttonNode.addEventListener("command", event => {
+ this.doCommandForAction(action, event, buttonNode);
+ });
+ return buttonNode;
+ },
+
+ _makePanelViewNodeForAction(action, forUrlbar) {
+ let panelViewNode = document.createXULElement("panelview");
+ panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
+ panelViewNode.classList.add("PanelUI-subView");
+ let bodyNode = document.createXULElement("vbox");
+ bodyNode.id = panelViewNode.id + "-body";
+ bodyNode.classList.add("panel-subview-body");
+ panelViewNode.appendChild(bodyNode);
+ return panelViewNode;
+ },
+
+ /**
+ * Shows or hides a panel for an action. You can supply your own panel;
+ * otherwise one is created.
+ *
+ * @param action (PageActions.Action, required)
+ * The action for which to toggle the panel. If the action is in the
+ * urlbar, then the panel will be anchored to it. Otherwise, a
+ * suitable anchor will be used.
+ * @param panelNode (DOM node, optional)
+ * The panel to use. This method takes a hands-off approach with
+ * regard to your panel in terms of attributes, styling, etc.
+ * @param event (DOM event, optional)
+ * The event which triggered this panel.
+ */
+ togglePanelForAction(action, panelNode = null, event = null) {
+ let aaPanelNode = this.activatedActionPanelNode;
+ if (panelNode) {
+ // Note that this particular code path will not prevent the panel from
+ // opening later if PanelMultiView.showPopup was called but the panel has
+ // not been opened yet.
+ if (panelNode.state != "closed") {
+ PanelMultiView.hidePopup(panelNode);
+ return;
+ }
+ if (aaPanelNode) {
+ PanelMultiView.hidePopup(aaPanelNode);
+ }
+ } else if (aaPanelNode) {
+ PanelMultiView.hidePopup(aaPanelNode);
+ return;
+ } else {
+ panelNode = this._makeActivatedActionPanelForAction(action);
+ }
+
+ // Hide the main panel before showing the action's panel.
+ PanelMultiView.hidePopup(this.panelNode);
+
+ let anchorNode = this.panelAnchorNodeForAction(action);
+ anchorNode.setAttribute("open", "true");
+ panelNode.addEventListener(
+ "popuphiding",
+ () => {
+ anchorNode.removeAttribute("open");
+ },
+ { once: true }
+ );
+
+ PanelMultiView.openPopup(panelNode, anchorNode, {
+ position: "bottomcenter topright",
+ triggerEvent: event,
+ }).catch(Cu.reportError);
+ },
+
+ _makeActivatedActionPanelForAction(action) {
+ let panelNode = document.createXULElement("panel");
+ panelNode.id = this._activatedActionPanelID;
+ panelNode.classList.add("cui-widget-panel", "panel-no-padding");
+ panelNode.setAttribute("actionID", action.id);
+ panelNode.setAttribute("role", "group");
+ panelNode.setAttribute("type", "arrow");
+ panelNode.setAttribute("flip", "slide");
+ panelNode.setAttribute("noautofocus", "true");
+ panelNode.setAttribute("tabspecific", "true");
+
+ let panelViewNode = null;
+ let iframeNode = null;
+
+ if (action.getWantsSubview(window)) {
+ let multiViewNode = document.createXULElement("panelmultiview");
+ panelViewNode = this._makePanelViewNodeForAction(action, true);
+ multiViewNode.setAttribute("mainViewId", panelViewNode.id);
+ multiViewNode.appendChild(panelViewNode);
+ panelNode.appendChild(multiViewNode);
+ } else if (action.wantsIframe) {
+ iframeNode = document.createXULElement("iframe");
+ iframeNode.setAttribute("type", "content");
+ panelNode.appendChild(iframeNode);
+ }
+
+ let popupSet = document.getElementById("mainPopupSet");
+ popupSet.appendChild(panelNode);
+ panelNode.addEventListener(
+ "popuphidden",
+ () => {
+ PanelMultiView.removePopup(panelNode);
+ },
+ { once: true }
+ );
+
+ if (iframeNode) {
+ panelNode.addEventListener(
+ "popupshowing",
+ () => {
+ action.onIframeShowing(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popupshown",
+ () => {
+ iframeNode.focus();
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popuphiding",
+ () => {
+ action.onIframeHiding(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popuphidden",
+ () => {
+ action.onIframeHidden(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ }
+
+ if (panelViewNode) {
+ action.onSubviewPlaced(panelViewNode);
+ panelNode.addEventListener(
+ "popupshowing",
+ () => {
+ action.onSubviewShowing(panelViewNode);
+ },
+ { once: true }
+ );
+ }
+
+ return panelNode;
+ },
+
+ /**
+ * Returns the node in the urlbar to which popups for the given action should
+ * be anchored. If the action is null, a sensible anchor is returned.
+ *
+ * @param action (PageActions.Action, optional)
+ * The action you want to anchor.
+ * @param event (DOM event, optional)
+ * This is used to display the feedback panel on the right node when
+ * the command can be invoked from both the main panel and another
+ * location, such as an activated action panel or a button.
+ * @return (DOM node) The node to which the action should be anchored.
+ */
+ panelAnchorNodeForAction(action, event) {
+ if (event && event.target.closest("panel") == this.panelNode) {
+ return this.mainButtonNode;
+ }
+
+ // Try each of the following nodes in order, using the first that's visible.
+ let potentialAnchorNodeIDs = [
+ action && action.anchorIDOverride,
+ action && this.urlbarButtonNodeIDForActionID(action.id),
+ this.mainButtonNode.id,
+ "identity-icon",
+ "urlbar-search-button",
+ ];
+ for (let id of potentialAnchorNodeIDs) {
+ if (id) {
+ let node = document.getElementById(id);
+ if (node && !node.hidden) {
+ let bounds = window.windowUtils.getBoundsWithoutFlushing(node);
+ if (bounds.height > 0 && bounds.width > 0) {
+ return node;
+ }
+ }
+ }
+ }
+ let id = action ? action.id : "<no action>";
+ throw new Error(`PageActions: No anchor node for ${id}`);
+ },
+
+ get activatedActionPanelNode() {
+ return document.getElementById(this._activatedActionPanelID);
+ },
+
+ get _activatedActionPanelID() {
+ return "pageActionActivatedActionPanel";
+ },
+
+ /**
+ * Adds or removes as necessary a DOM node for the given action in the urlbar.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeActionInUrlbar(action) {
+ let id = this.urlbarButtonNodeIDForActionID(action.id);
+ let node = document.getElementById(id);
+
+ if (!action.shouldShowInUrlbar(window)) {
+ if (node) {
+ if (action.__urlbarNodeInMarkup) {
+ node.hidden = true;
+ } else {
+ node.remove();
+ }
+ }
+ return;
+ }
+
+ let newlyPlaced = false;
+ if (action.__urlbarNodeInMarkup) {
+ this._maybeNotifyBeforePlacedInWindow(action);
+ // Allow the consumer to add the node in response to the
+ // onBeforePlacedInWindow notification.
+ node = document.getElementById(id);
+ if (!node) {
+ return;
+ }
+ newlyPlaced = node.hidden;
+ node.hidden = false;
+ } else if (!node) {
+ newlyPlaced = true;
+ this._maybeNotifyBeforePlacedInWindow(action);
+ node = this._makeUrlbarButtonNode(action);
+ node.id = id;
+ }
+
+ if (!newlyPlaced) {
+ return;
+ }
+
+ let insertBeforeNode = this._getNextNode(action, true);
+ this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
+ this.updateAction(action, null, {
+ urlbarNode: node,
+ });
+ action.onPlacedInUrlbar(node);
+ },
+
+ _makeUrlbarButtonNode(action) {
+ let buttonNode = document.createXULElement("image");
+ buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
+ buttonNode.setAttribute("actionid", action.id);
+ buttonNode.setAttribute("role", "button");
+ let commandHandler = event => {
+ this.doCommandForAction(action, event, buttonNode);
+ };
+ buttonNode.addEventListener("click", commandHandler);
+ buttonNode.addEventListener("keypress", commandHandler);
+ return buttonNode;
+ },
+
+ /**
+ * Removes all the DOM nodes of the given action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to remove.
+ */
+ removeAction(action) {
+ this._removeActionFromPanel(action);
+ this._removeActionFromUrlbar(action);
+ action.onRemovedFromWindow(window);
+ },
+
+ _removeActionFromUrlbar(action) {
+ let node = this.urlbarButtonNodeForActionID(action.id);
+ if (node) {
+ node.remove();
+ }
+ },
+
+ /**
+ * Updates the DOM nodes of an action to reflect either a changed property or
+ * all properties.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to update.
+ * @param propertyName (string, optional)
+ * The name of the property to update. If not given, then DOM nodes
+ * will be updated to reflect the current values of all properties.
+ * @param opts (object, optional)
+ * - panelNode: The action's node in the panel to update.
+ * - urlbarNode: The action's node in the urlbar to update.
+ * - value: If a property name is passed, this argument may contain
+ * its current value, in order to prevent a further look-up.
+ */
+ updateAction(action, propertyName = null, opts = {}) {
+ let anyNodeGiven = "panelNode" in opts || "urlbarNode" in opts;
+ let panelNode = anyNodeGiven
+ ? opts.panelNode || null
+ : this.panelButtonNodeForActionID(action.id);
+ let urlbarNode = anyNodeGiven
+ ? opts.urlbarNode || null
+ : this.urlbarButtonNodeForActionID(action.id);
+ let value = opts.value || undefined;
+ if (propertyName) {
+ this[this._updateMethods[propertyName]](
+ action,
+ panelNode,
+ urlbarNode,
+ value
+ );
+ } else {
+ for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
+ this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
+ }
+ }
+ },
+
+ _updateMethods: {
+ disabled: "_updateActionDisabled",
+ iconURL: "_updateActionIconURL",
+ title: "_updateActionLabeling",
+ tooltip: "_updateActionTooltip",
+ wantsSubview: "_updateActionWantsSubview",
+ },
+
+ _updateActionDisabled(
+ action,
+ panelNode,
+ urlbarNode,
+ disabled = action.getDisabled(window)
+ ) {
+ if (action.__transient) {
+ this.placeActionInPanel(action);
+ } else {
+ this._updateActionDisabledInPanel(action, panelNode, disabled);
+ }
+ this.placeActionInUrlbar(action);
+ },
+
+ _updateActionDisabledInPanel(
+ action,
+ panelNode,
+ disabled = action.getDisabled(window)
+ ) {
+ if (panelNode) {
+ if (disabled) {
+ panelNode.setAttribute("disabled", "true");
+ } else {
+ panelNode.removeAttribute("disabled");
+ }
+ }
+ },
+
+ _updateActionIconURL(
+ action,
+ panelNode,
+ urlbarNode,
+ properties = action.getIconProperties(window)
+ ) {
+ for (let [prop, value] of Object.entries(properties)) {
+ if (panelNode) {
+ panelNode.style.setProperty(prop, value);
+ }
+ if (urlbarNode) {
+ urlbarNode.style.setProperty(prop, value);
+ }
+ }
+ },
+
+ _updateActionLabeling(
+ action,
+ panelNode,
+ urlbarNode,
+ title = action.getTitle(window)
+ ) {
+ let tabCount = gBrowser.selectedTabs.length;
+ if (panelNode) {
+ if (action.panelFluentID) {
+ document.l10n.setAttributes(panelNode, action.panelFluentID, {
+ tabCount,
+ });
+ } else {
+ panelNode.setAttribute("label", title);
+ }
+ }
+ if (urlbarNode) {
+ // Some actions (e.g. Save Page to Pocket) have a wrapper node with the
+ // actual controls inside that wrapper. The wrapper is semantically
+ // meaningless, so it doesn't get reflected in the accessibility tree.
+ // In these cases, we don't want to set aria-label because that will
+ // force the element to be exposed to accessibility.
+ if (urlbarNode.nodeName != "hbox") {
+ urlbarNode.setAttribute("aria-label", title);
+ }
+ // tooltiptext falls back to the title, so update it too if necessary.
+ let tooltip = action.getTooltip(window);
+ if (!tooltip) {
+ if (action.urlbarFluentID) {
+ document.l10n.setAttributes(urlbarNode, action.urlbarFluentID, {
+ tabCount,
+ });
+ } else {
+ urlbarNode.setAttribute("tooltiptext", title);
+ }
+ }
+ }
+ },
+
+ _updateActionTooltip(
+ action,
+ panelNode,
+ urlbarNode,
+ tooltip = action.getTooltip(window)
+ ) {
+ if (urlbarNode) {
+ if (!tooltip) {
+ tooltip = action.getTitle(window);
+ }
+ if (tooltip) {
+ urlbarNode.setAttribute("tooltiptext", tooltip);
+ }
+ }
+ },
+
+ _updateActionWantsSubview(
+ action,
+ panelNode,
+ urlbarNode,
+ wantsSubview = action.getWantsSubview(window)
+ ) {
+ if (!panelNode) {
+ return;
+ }
+ let panelViewID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewID);
+ panelNode.classList.toggle("subviewbutton-nav", wantsSubview);
+ if (!wantsSubview) {
+ if (panelViewNode) {
+ panelViewNode.remove();
+ }
+ return;
+ }
+ if (!panelViewNode) {
+ panelViewNode = this._makePanelViewNodeForAction(action, false);
+ this.multiViewNode.appendChild(panelViewNode);
+ action.onSubviewPlaced(panelViewNode);
+ }
+ },
+
+ doCommandForAction(action, event, buttonNode) {
+ // On mac, ctrl-click will send a context menu event from the widget, so we
+ // don't want to handle the click event when ctrl key is pressed.
+ if (
+ event &&
+ event.type == "click" &&
+ (event.button != 0 ||
+ (AppConstants.platform == "macosx" && event.ctrlKey))
+ ) {
+ return;
+ }
+ if (event && event.type == "keypress") {
+ if (event.key != " " && event.key != "Enter") {
+ return;
+ }
+ event.stopPropagation();
+ }
+ // If we're in the panel, open a subview inside the panel:
+ // Note that we can't use this.panelNode.contains(buttonNode) here
+ // because of XBL boundaries breaking Element.contains.
+ if (
+ action.getWantsSubview(window) &&
+ buttonNode &&
+ buttonNode.closest("panel") == this.panelNode
+ ) {
+ let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewNodeID);
+ action.onSubviewShowing(panelViewNode);
+ this.multiViewNode.showSubView(panelViewNode, buttonNode);
+ return;
+ }
+ // Otherwise, hide the main popup in case it was open:
+ PanelMultiView.hidePopup(this.panelNode);
+
+ let aaPanelNode = this.activatedActionPanelNode;
+ if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) {
+ action.onCommand(event, buttonNode);
+ }
+ if (action.getWantsSubview(window) || action.wantsIframe) {
+ this.togglePanelForAction(action, null, event);
+ }
+ },
+
+ /**
+ * Returns the action for a node.
+ *
+ * @param node (DOM node, required)
+ * A button DOM node, either one that's shown in the page action panel
+ * or the urlbar.
+ * @return (PageAction.Action) If the node has a related action and the action
+ * is not a separator, then the action is returned. Otherwise null is
+ * returned.
+ */
+ actionForNode(node) {
+ if (!node) {
+ return null;
+ }
+ let actionID = this._actionIDForNodeID(node.id);
+ let action = PageActions.actionForID(actionID);
+ if (!action) {
+ // The given node may be an ancestor of a node corresponding to an action,
+ // like how #star-button is contained in #star-button-box, the latter
+ // being the bookmark action's node. Look up the ancestor chain.
+ for (let n = node.parentNode; n && !action; n = n.parentNode) {
+ if (n.id == "page-action-buttons" || n.localName == "panelview") {
+ // We reached the page-action-buttons or panelview container.
+ // Stop looking; no acton was found.
+ break;
+ }
+ actionID = this._actionIDForNodeID(n.id);
+ action = PageActions.actionForID(actionID);
+ }
+ }
+ return action && !action.__isSeparator ? action : null;
+ },
+
+ /**
+ * The given action's top-level button in the main panel.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (DOM node) The action's button in the main panel.
+ */
+ panelButtonNodeForActionID(actionID) {
+ return document.getElementById(this.panelButtonNodeIDForActionID(actionID));
+ },
+
+ /**
+ * The ID of the given action's top-level button in the main panel.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (string) The ID of the action's button in the main panel.
+ */
+ panelButtonNodeIDForActionID(actionID) {
+ return `pageAction-panel-${actionID}`;
+ },
+
+ /**
+ * The given action's button in the urlbar.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (DOM node) The action's urlbar button node.
+ */
+ urlbarButtonNodeForActionID(actionID) {
+ return document.getElementById(
+ this.urlbarButtonNodeIDForActionID(actionID)
+ );
+ },
+
+ /**
+ * The ID of the given action's button in the urlbar.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (string) The ID of the action's urlbar button node.
+ */
+ urlbarButtonNodeIDForActionID(actionID) {
+ let action = PageActions.actionForID(actionID);
+ if (action && action.urlbarIDOverride) {
+ return action.urlbarIDOverride;
+ }
+ return `pageAction-urlbar-${actionID}`;
+ },
+
+ // The ID of the given action's panelview.
+ _panelViewNodeIDForActionID(actionID, forUrlbar) {
+ let placementID = forUrlbar ? "urlbar" : "panel";
+ return `pageAction-${placementID}-${actionID}-subview`;
+ },
+
+ // The ID of the action corresponding to the given top-level button in the
+ // panel or button in the urlbar.
+ _actionIDForNodeID(nodeID) {
+ if (!nodeID) {
+ return null;
+ }
+ let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
+ if (match) {
+ return match[1];
+ }
+ // Check all the urlbar ID overrides.
+ for (let action of PageActions.actions) {
+ if (action.urlbarIDOverride && action.urlbarIDOverride == nodeID) {
+ return action.id;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Call this when the main page action button in the urlbar is activated.
+ *
+ * @param event (DOM event, required)
+ * The click or whatever event.
+ */
+ mainButtonClicked(event) {
+ event.stopPropagation();
+ if (
+ // On mac, ctrl-click will send a context menu event from the widget, so
+ // we don't want to bring up the panel when ctrl key is pressed.
+ (event.type == "mousedown" &&
+ (event.button != 0 ||
+ (AppConstants.platform == "macosx" && event.ctrlKey))) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return;
+ }
+
+ // If the activated-action panel is open and anchored to the main button,
+ // close it.
+ let panelNode = this.activatedActionPanelNode;
+ if (panelNode && panelNode.anchorNode.id == this.mainButtonNode.id) {
+ PanelMultiView.hidePopup(panelNode);
+ return;
+ }
+
+ if (this.panelNode.state == "open") {
+ PanelMultiView.hidePopup(this.panelNode);
+ } else if (this.panelNode.state == "closed") {
+ this.showPanel(event);
+ }
+ },
+
+ /**
+ * Show the page action panel
+ *
+ * @param event (DOM event, optional)
+ * The event that triggers showing the panel. (such as a mouse click,
+ * if the user clicked something to open the panel)
+ */
+ showPanel(event = null) {
+ this.panelNode.hidden = false;
+ this.mainButtonNode.setAttribute("open", "true");
+ PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
+ position: "bottomcenter topright",
+ triggerEvent: event,
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Call this on the context menu's popupshowing event.
+ *
+ * @param event (DOM event, required)
+ * The popupshowing event.
+ * @param popup (DOM node, required)
+ * The context menu popup DOM node.
+ */
+ async onContextMenuShowing(event, popup) {
+ if (event.target != popup) {
+ return;
+ }
+
+ this._contextAction = this.actionForNode(popup.triggerNode);
+ if (!this._contextAction) {
+ event.preventDefault();
+ return;
+ }
+
+ let state;
+ if (this._contextAction._isMozillaAction) {
+ state = this._contextAction.pinnedToUrlbar
+ ? "builtInPinned"
+ : "builtInUnpinned";
+ } else {
+ state = this._contextAction.pinnedToUrlbar
+ ? "extensionPinned"
+ : "extensionUnpinned";
+ }
+ popup.setAttribute("state", state);
+
+ let removeExtension = popup.querySelector(".removeExtensionItem");
+ let { extensionID } = this._contextAction;
+ let addon = extensionID && (await AddonManager.getAddonByID(extensionID));
+ removeExtension.hidden = !addon;
+ if (addon) {
+ removeExtension.disabled = !(
+ addon.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ }
+ },
+
+ /**
+ * Call this from the menu item in the context menu that toggles pinning.
+ */
+ togglePinningForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ action.pinnedToUrlbar = !action.pinnedToUrlbar;
+ BrowserUsageTelemetry.recordWidgetChange(
+ action.id,
+ action.pinnedToUrlbar ? "page-action-buttons" : null,
+ "pageaction-context"
+ );
+ },
+
+ /**
+ * Call this from the menu item in the context menu that opens about:addons.
+ */
+ openAboutAddonsForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ AMTelemetry.recordActionEvent({
+ object: "pageAction",
+ action: "manage",
+ extra: { addonId: action.extensionID },
+ });
+
+ let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
+ window.BrowserOpenAddonsMgr(viewID);
+ },
+
+ /**
+ * Call this from the menu item in the context menu that removes an add-on.
+ */
+ removeExtensionForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ BrowserAddonUI.removeAddon(action.extensionID, "pageAction");
+ },
+
+ _contextAction: null,
+
+ /**
+ * We use this to set an attribute on the DOM node. If the attribute exists,
+ * then we get the panel node's attribute and set it on the DOM node. Otherwise,
+ * we get the title string and update the attribute with that value. The point is to map
+ * attributes on the node to strings on the main panel. Use this for DOM
+ * nodes that don't correspond to actions, like buttons in subviews.
+ *
+ * @param node (DOM node, required)
+ * The node you're setting up.
+ * @param attrName (string, required)
+ * The name of the attribute *on the node you're setting up*.
+ */
+ takeNodeAttributeFromPanel(node, attrName) {
+ let panelAttrName = node.getAttribute(attrName);
+ if (!panelAttrName && attrName == "title") {
+ attrName = "label";
+ panelAttrName = node.getAttribute(attrName);
+ }
+ if (panelAttrName) {
+ let attrValue = this.panelNode.getAttribute(panelAttrName);
+ if (attrValue) {
+ node.setAttribute(attrName, attrValue);
+ }
+ }
+ },
+
+ /**
+ * Call this on tab switch or when the current <browser>'s location changes.
+ */
+ onLocationChange() {
+ for (let action of PageActions.actions) {
+ action.onLocationChange(window);
+ }
+ },
+};
+
+/**
+ * Shows the feedback popup for an action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action associated with the feedback.
+ * @param event (DOM event, optional)
+ * The event that triggered the feedback.
+ * @param messageId (string, optional)
+ * Can be used to set a message id that is different from the action id.
+ */
+function showBrowserPageActionFeedback(action, event = null, messageId = null) {
+ let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
+
+ ConfirmationHint.show(anchor, messageId || action.id, {
+ event,
+ hideArrow: true,
+ });
+}
+
+// built-in actions below //////////////////////////////////////////////////////
+
+// bookmark
+BrowserPageActions.bookmark = {
+ onShowingInPanel(buttonNode) {
+ if (buttonNode.label == "null") {
+ BookmarkingUI.updateBookmarkPageMenuItem();
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ BookmarkingUI.onStarCommand(event);
+ },
+};
+
+// pin tab
+BrowserPageActions.pinTab = {
+ updateState() {
+ let action = PageActions.actionForID("pinTab");
+ let { pinned } = gBrowser.selectedTab;
+ let fluentID;
+ if (pinned) {
+ fluentID = "page-action-unpin-tab";
+ } else {
+ fluentID = "page-action-pin-tab";
+ }
+
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
+ if (panelButton) {
+ document.l10n.setAttributes(panelButton, fluentID + "-panel");
+ panelButton.toggleAttribute("pinned", pinned);
+ }
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ action.id
+ );
+ if (urlbarButton) {
+ document.l10n.setAttributes(urlbarButton, fluentID + "-urlbar");
+ urlbarButton.toggleAttribute("pinned", pinned);
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ if (gBrowser.selectedTab.pinned) {
+ gBrowser.unpinTab(gBrowser.selectedTab);
+ } else {
+ gBrowser.pinTab(gBrowser.selectedTab);
+ }
+ },
+};
+
+// copy URL
+BrowserPageActions.copyURL = {
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(
+ gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec
+ );
+ let action = PageActions.actionForID("copyURL");
+ showBrowserPageActionFeedback(action, event);
+ },
+};
+
+// email link
+BrowserPageActions.emailLink = {
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+ },
+};
+
+// send to device
+BrowserPageActions.sendToDevice = {
+ onBeforePlacedInWindow(browserWindow) {
+ this._updateTitle();
+ gBrowser.addEventListener("TabMultiSelect", event => {
+ this._updateTitle();
+ });
+ },
+
+ // The action's title in this window depends on the number of tabs that are
+ // selected.
+ _updateTitle() {
+ let action = PageActions.actionForID("sendToDevice");
+ let tabCount = gBrowser.selectedTabs.length;
+
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
+ if (panelButton) {
+ document.l10n.setAttributes(panelButton, action.panelFluentID, {
+ tabCount,
+ });
+ }
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ action.id
+ );
+ if (urlbarButton) {
+ document.l10n.setAttributes(urlbarButton, action.urlbarFluentID, {
+ tabCount,
+ });
+ }
+ },
+
+ onSubviewPlaced(panelViewNode) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+ let notReady = document.createXULElement("toolbarbutton");
+ notReady.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "pageAction-sendToDevice-notReady"
+ );
+ document.l10n.setAttributes(notReady, "page-action-send-tab-not-ready");
+ notReady.setAttribute("disabled", "true");
+ bodyNode.appendChild(notReady);
+ for (let node of bodyNode.children) {
+ BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
+ BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
+ }
+ },
+
+ onLocationChange() {
+ let action = PageActions.actionForID("sendToDevice");
+ let browser = gBrowser.selectedBrowser;
+ let url = browser.currentURI.spec;
+ action.setDisabled(!gSync.isSendableURI(url), window);
+ },
+
+ onShowingSubview(panelViewNode) {
+ gSync.populateSendTabToDevicesView(panelViewNode);
+ },
+};
+
+// add search engine
+BrowserPageActions.addSearchEngine = {
+ get action() {
+ return PageActions.actionForID("addSearchEngine");
+ },
+
+ get engines() {
+ return gBrowser.selectedBrowser.engines || [];
+ },
+
+ get strings() {
+ delete this.strings;
+ let uri = "chrome://browser/locale/search.properties";
+ return (this.strings = Services.strings.createBundle(uri));
+ },
+
+ updateEngines() {
+ // As a slight optimization, if the action isn't in the urlbar, don't do
+ // anything here except disable it. The action's panel nodes are updated
+ // when the panel is shown.
+ this.action.setDisabled(!this.engines.length, window);
+ if (this.action.shouldShowInUrlbar(window)) {
+ this._updateTitleAndIcon();
+ }
+ },
+
+ _updateTitleAndIcon() {
+ if (!this.engines.length) {
+ return;
+ }
+ let title = this.strings.GetStringFromName("searchAddFoundEngine2");
+ this.action.setTitle(title, window);
+ this.action.setIconURL(this.engines[0].icon, window);
+ },
+
+ onShowingInPanel() {
+ this._updateTitleAndIcon();
+ this.action.setWantsSubview(this.engines.length > 1, window);
+ let button = BrowserPageActions.panelButtonNodeForActionID(this.action.id);
+ button.setAttribute("image", this.engines[0].icon);
+ button.setAttribute("uri", this.engines[0].uri);
+ button.setAttribute("crop", "center");
+ },
+
+ onSubviewShowing(panelViewNode) {
+ let body = panelViewNode.querySelector(".panel-subview-body");
+ while (body.firstChild) {
+ body.firstChild.remove();
+ }
+ for (let engine of this.engines) {
+ let button = document.createXULElement("toolbarbutton");
+ button.classList.add("subviewbutton", "subviewbutton-iconic");
+ button.setAttribute("label", engine.title);
+ button.setAttribute("image", engine.icon);
+ button.setAttribute("uri", engine.uri);
+ button.addEventListener("command", event => {
+ let panelNode = panelViewNode.closest("panel");
+ PanelMultiView.hidePopup(panelNode);
+ this._installEngine(
+ button.getAttribute("uri"),
+ button.getAttribute("image")
+ );
+ });
+ body.appendChild(button);
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ if (!buttonNode.closest("panel")) {
+ // The urlbar button was clicked. It should have a subview if there are
+ // many engines.
+ let manyEngines = this.engines.length > 1;
+ this.action.setWantsSubview(manyEngines, window);
+ if (manyEngines) {
+ return;
+ }
+ }
+ // Either the panel button or urlbar button was clicked -- not a button in
+ // the subview -- but in either case, there's only one search engine.
+ // (Because this method isn't called when the panel button is clicked and it
+ // shows a subview, and the many-engines case for the urlbar returned early
+ // above.)
+ let engine = this.engines[0];
+ this._installEngine(engine.uri, engine.icon);
+ },
+
+ _installEngine(uri, image) {
+ SearchUIUtils.addOpenSearchEngine(
+ uri,
+ image,
+ gBrowser.selectedBrowser.browsingContext
+ )
+ .then(result => {
+ if (result) {
+ showBrowserPageActionFeedback(this.action);
+ }
+ })
+ .catch(console.error);
+ },
+};
+
+// share URL
+BrowserPageActions.shareURL = {
+ onCommand(event, buttonNode) {
+ let browser = gBrowser.selectedBrowser;
+ let currentURI = gURLBar.makeURIReadable(browser.currentURI).displaySpec;
+ this._windowsUIUtils.shareUrl(currentURI, browser.contentTitle);
+ },
+
+ onShowingInPanel(buttonNode) {
+ this._cached = false;
+ },
+
+ onShowingSubview(panelViewNode) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+
+ // We cache the providers + the UI if the user selects the share
+ // panel multiple times while the panel is open.
+ if (this._cached && bodyNode.children.length) {
+ return;
+ }
+
+ let sharingService = this._sharingService;
+ let url = gBrowser.selectedBrowser.currentURI;
+ let currentURI = gURLBar.makeURIReadable(url).displaySpec;
+ let shareProviders = sharingService.getSharingProviders(currentURI);
+ let fragment = document.createDocumentFragment();
+
+ let onCommand = event => {
+ let shareName = event.target.getAttribute("share-name");
+ if (shareName) {
+ sharingService.shareUrl(
+ shareName,
+ currentURI,
+ gBrowser.selectedBrowser.contentTitle
+ );
+ } else if (event.target.classList.contains("share-more-button")) {
+ sharingService.openSharingPreferences();
+ }
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ };
+
+ shareProviders.forEach(function(share) {
+ let item = document.createXULElement("toolbarbutton");
+ item.setAttribute("label", share.menuItemTitle);
+ item.setAttribute("share-name", share.name);
+ item.setAttribute("image", share.image);
+ item.classList.add("subviewbutton", "subviewbutton-iconic");
+ item.addEventListener("command", onCommand);
+ fragment.appendChild(item);
+ });
+
+ let item = document.createXULElement("toolbarbutton");
+ document.l10n.setAttributes(item, "page-action-share-more-panel");
+ item.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "share-more-button"
+ );
+ item.addEventListener("command", onCommand);
+ fragment.appendChild(item);
+
+ while (bodyNode.firstChild) {
+ bodyNode.firstChild.remove();
+ }
+ bodyNode.appendChild(fragment);
+ this._cached = true;
+ },
+};
+
+// Attach sharingService here so tests can override the implementation
+XPCOMUtils.defineLazyServiceGetters(BrowserPageActions.shareURL, {
+ _sharingService: [
+ "@mozilla.org/widget/macsharingservice;1",
+ "nsIMacSharingService",
+ ],
+ _windowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
+});