Bug 1374477 - Add browser-pageActions.js for Photon page actions. r?mikedeboer draft
authorDrew Willcoxon <adw@mozilla.com>
Fri, 21 Jul 2017 12:11:06 -0700
changeset 613226 ca64e47ae9ed1bce20b56b17da9cee8b863f4c2a
parent 613225 d0d139e709301434ff35d36ccd5aece21d31b133
child 613227 9beb9db87203a4d309919b0caa3e9b7b1a65d61d
push id69757
push userdwillcoxon@mozilla.com
push dateFri, 21 Jul 2017 19:11:24 +0000
reviewersmikedeboer
bugs1374477
milestone56.0a1
Bug 1374477 - Add browser-pageActions.js for Photon page actions. r?mikedeboer MozReview-Commit-ID: DUl7WlSnk4k
browser/base/content/browser-pageActions.js
browser/base/content/browser.js
browser/base/content/global-scripts.inc
browser/base/jar.mn
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-pageActions.js
@@ -0,0 +1,687 @@
+/* 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/. */
+
+var BrowserPageActions = {
+  /**
+   * 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() {
+    delete this.panelNode;
+    return this.panelNode = document.getElementById("pageActionPanel");
+  },
+
+  /**
+   * The photonmultiview 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() {
+    for (let action of PageActions.actions) {
+      this.placeAction(action);
+    }
+  },
+
+  /**
+   * Adds or removes as necessary DOM nodes for the given action.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   * @param  urlbarIndex (int, required)
+   *         If the action is shown in the urlbar, then this is the index within
+   *         the actions in the urlbar at which the action should be placed.
+   *         Ignored otherwise.
+   */
+  placeAction(action, urlbarIndex) {
+    if (action.isSeparator) {
+      this._placePanelSeparator();
+      return;
+    }
+    this.placeActionInPanel(action);
+    this.placeActionInUrlbar(action, urlbarIndex);
+  },
+
+  /**
+   * Adds or removes as necessary DOM nodes for the action in the panel.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   */
+  placeActionInPanel(action) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (!node) {
+      let panelViewNode;
+      [node, panelViewNode] = this._makePanelButtonNodeForAction(action);
+      node.id = id;
+      let insertAfterNode = null;
+      if (action.insertAfter) {
+        let insertAfterNodeID = this._panelButtonNodeIDForActionID(action.insertAfter);
+        insertAfterNode = document.getElementById(insertAfterNodeID);
+      }
+      let insertBeforeNode =
+        insertAfterNode ? insertAfterNode.nextSibling : null;
+      this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
+      action.onPlacedInPanel(node);
+      if (panelViewNode) {
+        action.subview.onPlaced(panelViewNode);
+      }
+    }
+    return node;
+  },
+
+  _makePanelButtonNodeForAction(action) {
+    let buttonNode = document.createElement("toolbarbutton");
+    buttonNode.classList.add(
+      "subviewbutton",
+      "subviewbutton-iconic",
+      "pageAction-panel-button"
+    );
+    buttonNode.setAttribute("label", action.title);
+    if (action.iconURL) {
+      buttonNode.style.listStyleImage = "url('" + action.iconURL + "')";
+    }
+    if (action.nodeAttributes) {
+      for (let name in action.nodeAttributes) {
+        buttonNode.setAttribute(name, action.nodeAttributes[name]);
+      }
+    }
+    let panelViewNode = null;
+    if (action.subview) {
+      buttonNode.classList.add("subviewbutton-nav");
+      panelViewNode = this._makePanelViewNodeForAction(action, false);
+      this.multiViewNode.appendChild(panelViewNode);
+    }
+    buttonNode.addEventListener("command", () => {
+      if (panelViewNode) {
+        action.subview.onShowing(panelViewNode);
+        this.multiViewNode.showSubView(panelViewNode, buttonNode);
+        return;
+      }
+      if (action.wantsIframe) {
+        this._toggleTempPanelForAction(action);
+        return;
+      }
+      this.panelNode.hidePopup();
+      action.onCommand(buttonNode);
+    });
+    return [buttonNode, panelViewNode];
+  },
+
+  _makePanelViewNodeForAction(action, forUrlbar) {
+    let panelViewNode = document.createElement("panelview");
+    let placementID = forUrlbar ? "urlbar" : "panel";
+    panelViewNode.id = `pageAction-${placementID}-${action.id}-subview`;
+    panelViewNode.classList.add("PanelUI-subView");
+    panelViewNode.setAttribute("title", action.subview.title);
+    let bodyNode = document.createElement("vbox");
+    bodyNode.id = panelViewNode.id + "-body";
+    bodyNode.classList.add("panel-subview-body");
+    panelViewNode.appendChild(bodyNode);
+    for (let button of action.subview.buttons) {
+      let buttonNode = document.createElement("toolbarbutton");
+      let buttonNodeID =
+        forUrlbar ? this._urlbarButtonNodeIDForActionID(action.id) :
+        this._panelButtonNodeIDForActionID(action.id);
+      buttonNodeID += "-" + button.id;
+      buttonNode.id = buttonNodeID;
+      buttonNode.classList.add("subviewbutton", "subviewbutton-iconic");
+      buttonNode.setAttribute("label", button.title);
+      if (button.shortcut) {
+        buttonNode.setAttribute("shortcut", button.shortcut);
+      }
+      if (button.disabled) {
+        buttonNode.setAttribute("disabled", "true");
+      }
+      buttonNode.addEventListener("command", () => {
+        button.onCommand(buttonNode);
+      });
+      bodyNode.appendChild(buttonNode);
+    }
+    return panelViewNode;
+  },
+
+  _toggleTempPanelForAction(action) {
+    let panelNodeID = "pageActionTempPanel";
+    let panelNode = document.getElementById(panelNodeID);
+    if (panelNode) {
+      panelNode.hidePopup();
+      return;
+    }
+
+    panelNode = document.createElement("panel");
+    panelNode.id = panelNodeID;
+    panelNode.classList.add("cui-widget-panel");
+    panelNode.setAttribute("role", "group");
+    panelNode.setAttribute("type", "arrow");
+    panelNode.setAttribute("flip", "slide");
+    panelNode.setAttribute("noautofocus", "true");
+
+    let panelViewNode = null;
+    let iframeNode = null;
+
+    if (action.subview) {
+      let multiViewNode = document.createElement("photonpanelmultiview");
+      panelViewNode = this._makePanelViewNodeForAction(action, true);
+      multiViewNode.appendChild(panelViewNode);
+      panelNode.appendChild(multiViewNode);
+    } else if (action.wantsIframe) {
+      iframeNode = document.createElement("iframe");
+      iframeNode.setAttribute("type", "content");
+      panelNode.appendChild(iframeNode);
+    }
+
+    let popupSet = document.getElementById("mainPopupSet");
+    popupSet.appendChild(panelNode);
+    panelNode.addEventListener("popuphidden", () => {
+      panelNode.remove();
+    }, { once: true });
+
+    if (panelViewNode) {
+      action.subview.onPlaced(panelViewNode);
+      action.subview.onShowing(panelViewNode);
+    }
+
+    this.panelNode.hidePopup();
+
+    let urlbarNodeID = this._urlbarButtonNodeIDForActionID(action.id);
+    let anchorNode =
+      document.getElementById(urlbarNodeID) || this.mainButtonNode;
+    panelNode.openPopup(anchorNode, "bottomcenter topright");
+
+    if (iframeNode) {
+      action.onIframeShown(iframeNode, panelNode);
+    }
+  },
+
+  /**
+   * Adds or removes as necessary a DOM node for the given action in the urlbar.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   * @param  urlbarIndex (int, required)
+   *         If the action is shown in the urlbar, then this is the index within
+   *         the actions in the urlbar at which the action should be placed.
+   *         Ignored otherwise.
+   */
+  placeActionInUrlbar(action, urlbarIndex) {
+    let id = this._urlbarButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (!action.shownInUrlbar) {
+      if (node) {
+        node.remove();
+      }
+      return null;
+    }
+    if (!node) {
+      node = this._makeUrlbarButtonNode(action);
+      node.id = id;
+      let parentNode = this.mainButtonNode.parentNode;
+      let insertBeforeNode = null;
+      let index = 0;
+      for (let childNode of parentNode.childNodes) {
+        if (childNode == this.mainButtonNode) {
+          continue;
+        }
+        if (index == urlbarIndex) {
+          insertBeforeNode = childNode;
+          break;
+        }
+        index++;
+      }
+      parentNode.insertBefore(node, insertBeforeNode);
+      action.onPlacedInUrlbar(node);
+    }
+    return node;
+  },
+
+  _makeUrlbarButtonNode(action) {
+    let buttonNode = document.createElement("image");
+    buttonNode.classList.add("urlbar-icon");
+    if (action.tooltip) {
+      buttonNode.setAttribute("tooltiptext", action.tooltip);
+    }
+    if (action.iconURL) {
+      buttonNode.style.listStyleImage = "url('" + action.iconURL + "')";
+    }
+    if (action.nodeAttributes) {
+      for (let name in action.nodeAttributes) {
+        buttonNode.setAttribute(name, action.nodeAttributes[name]);
+      }
+    }
+    buttonNode.addEventListener("click", event => {
+      if (event.button != 0) {
+        return;
+      }
+      if (action.subview || action.wantsIframe) {
+        this._toggleTempPanelForAction(action);
+        return;
+      }
+      action.onCommand(buttonNode);
+    });
+    return buttonNode;
+  },
+
+  _placePanelSeparator() {
+    let node = document.createElement("toolbarseparator");
+    this.mainViewBodyNode.appendChild(node);
+  },
+
+  /**
+   * 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);
+  },
+
+  _removeActionFromPanel(action) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.remove();
+    }
+    if (action.subview) {
+      let panelViewNodeID = this._panelViewNodeIDFromActionID(action.id);
+      let panelViewNode = document.getElementById(panelViewNodeID);
+      if (panelViewNode) {
+        panelViewNode.remove();
+      }
+    }
+  },
+
+  _removeActionFromUrlbar(action) {
+    let id = this._urlbarButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.remove();
+    }
+  },
+
+  /**
+   * Updates the DOM nodes of an action to reflect its changed title.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to update.
+   */
+  updateActionTitle(action) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.setAttribute("label", action.title);
+    }
+  },
+
+  /**
+   * 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) The node's related action, or null if none.
+   */
+  actionForNode(node) {
+    if (!node) {
+      return null;
+    }
+    let actionID = this._actionIDForNodeID(node.id);
+    return PageActions.actionForID(actionID);
+  },
+
+  _panelButtonNodeIDForActionID(actionID) {
+    return "pageAction-panel-" + actionID;
+  },
+
+  _urlbarButtonNodeIDForActionID(actionID) {
+    let action = PageActions.actionForID(actionID);
+    if (action && action.urlbarIDOverride) {
+      return action.urlbarIDOverride;
+    }
+    return "pageAction-urlbar-" + actionID;
+  },
+
+  _actionIDForNodeID(nodeID) {
+    if (!nodeID) {
+      return null;
+    }
+    let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
+    return match ? match[1] : 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 ((event.type == "click" && event.button != 0) ||
+        (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
+         event.keyCode != KeyEvent.DOM_VK_RETURN)) {
+      return;
+    }
+
+    for (let action of PageActions.actions) {
+      let buttonNodeID = this._panelButtonNodeIDForActionID(action.id);
+      let buttonNode = document.getElementById(buttonNodeID);
+      action.onShowingInPanel(buttonNode);
+    }
+
+    this.panelNode.hidden = false;
+    this.panelNode.openPopup(this.mainButtonNode, "bottomcenter topright");
+  },
+
+  /**
+   * Call this on the contextmenu event.  Note that this is called before
+   * onContextMenuShowing.
+   *
+   * @param  event (DOM event, required)
+   *         The contextmenu event.
+   */
+  onContextMenu(event) {
+    let node = event.originalTarget;
+    this._contextAction = this.actionForNode(node);
+  },
+
+  /**
+   * 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.
+   */
+  onContextMenuShowing(event, popup) {
+    if (event.target != popup) {
+      return;
+    }
+    // Right now there's only one item in the context menu, to toggle the
+    // context action's shown-in-urlbar state.  Update it now.
+    let toggleItem = popup.firstChild;
+    let toggleItemLabel = null;
+    if (this._contextAction) {
+      toggleItem.disabled = false;
+      if (this._contextAction.shownInUrlbar) {
+        toggleItemLabel = toggleItem.getAttribute("remove-label");
+      }
+    }
+    if (!toggleItemLabel) {
+      toggleItemLabel = toggleItem.getAttribute("add-label");
+    }
+    toggleItem.label = toggleItemLabel;
+  },
+
+  /**
+   * Call this from the context menu's toggle menu item.
+   */
+  toggleShownInUrlbarForContextAction() {
+    if (!this._contextAction) {
+      return;
+    }
+    this._contextAction.shownInUrlbar = !this._contextAction.shownInUrlbar;
+  },
+
+  _contextAction: null,
+
+  /**
+   * A bunch of strings (labels for actions and the like) are defined in DTD,
+   * but actions are created in JS.  So what we do is add a bunch of attributes
+   * to the page action panel's definition in the markup, whose values hold
+   * these DTD strings.  Then when each built-in action is set up, we get the
+   * related strings from the panel node and set up the action's node with them.
+   *
+   * The convention is to set for example the "title" property in an action's JS
+   * definition to the name of the attribute on the panel node that holds the
+   * actual title string.  Then call this function, passing the action's related
+   * DOM node and the name of the attribute that you are setting on the DOM
+   * node -- "label" or "title" in this example (either will do).
+   *
+   * @param  node (DOM node, required)
+   *         The node of an action 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);
+      }
+    }
+  },
+};
+
+
+// built-in actions below //////////////////////////////////////////////////////
+
+// bookmark
+BrowserPageActions.bookmark = {
+  /**
+   * The onShowingInPanel handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onShowingInPanel.
+   */
+  onShowingInPanel(buttonNode) {
+    // Update the button label via the bookmark observer.
+    BookmarkingUI.updateBookmarkPageMenuItem();
+  },
+
+  /**
+   * The onCommand handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onCommand.
+   */
+  onCommand(buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    let commandNode = document.getElementById("Browser:AddBookmarkAs");
+    commandNode.doCommand();
+  },
+};
+
+// copy URL
+BrowserPageActions.copyURL = {
+  /**
+   * The onPlacedInPanel handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onPlacedInPanel.
+   */
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  /**
+   * The onCommand handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onCommand.
+   */
+  onCommand(buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    Cc["@mozilla.org/widget/clipboardhelper;1"]
+      .getService(Ci.nsIClipboardHelper)
+      .copyString(gBrowser.selectedBrowser.currentURI.spec);
+  },
+};
+
+// email link
+BrowserPageActions.emailLink = {
+  /**
+   * The onPlacedInPanel handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onPlacedInPanel.
+   */
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  /**
+   * The onCommand handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onCommand.
+   */
+  onCommand(buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+  },
+};
+
+// send to device
+BrowserPageActions.sendToDevice = {
+  /**
+   * The onPlacedInPanel handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onPlacedInPanel.
+   */
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  /**
+   * The onSubviewPlaced handler for the action.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The panelViewNode arg passed to the action's onSubviewPlaced.
+   */
+  onSubviewPlaced(panelViewNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(panelViewNode, "title");
+    let bodyNode = panelViewNode.firstChild;
+    for (let node of bodyNode.childNodes) {
+      BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
+      BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
+    }
+  },
+
+  /**
+   * The onShowingInPanel handler for the action.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The buttonNode arg passed to the action's onShowingInPanel.
+   */
+  onShowingInPanel(buttonNode) {
+    let browser = gBrowser.selectedBrowser;
+    let url = browser.currentURI.spec;
+    if (gSync.isSendableURI(url)) {
+      buttonNode.removeAttribute("disabled");
+    } else {
+      buttonNode.setAttribute("disabled", "true");
+    }
+  },
+
+  /**
+   * The onShowingSubview handler for the action.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The panelViewNode arg passed to the action's onShowingSubview.
+   */
+  onShowingSubview(panelViewNode) {
+    let browser = gBrowser.selectedBrowser;
+    let url = browser.currentURI.spec;
+    let title = browser.contentTitle;
+
+    let bodyNode = panelViewNode.firstChild;
+
+    // This is on top because it also clears the device list between state
+    // changes.
+    gSync.populateSendTabToDevicesMenu(bodyNode, url, title, (clientId, name, clientType) => {
+      if (!name) {
+        return document.createElement("toolbarseparator");
+      }
+      let item = document.createElement("toolbarbutton");
+      item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
+      if (clientId) {
+        item.classList.add("subviewbutton-iconic");
+      }
+      item.setAttribute("tooltiptext", name);
+      return item;
+    });
+
+    if (!gSync.isSignedIn) {
+      // Could be unconfigured or unverified
+      bodyNode.setAttribute("state", "notsignedin");
+      return;
+    }
+
+    // In the first ~10 sec after startup, Sync may not be loaded and the list
+    // of devices will be empty.
+    if (!gSync.syncReady) {
+      bodyNode.setAttribute("state", "notready");
+      // Force a background Sync
+      Services.tm.dispatchToMainThread(() => {
+        Weave.Service.sync([]);  // [] = clients engine only
+        if (!window.closed && gSync.syncReady) {
+          this.onShowingSubview(panelViewNode);
+        }
+      });
+      return;
+    }
+    if (!gSync.remoteClients.length) {
+      bodyNode.setAttribute("state", "nodevice");
+      return;
+    }
+
+    bodyNode.setAttribute("state", "signedin");
+  },
+
+  /**
+   * Call when the "Firefox Account Required" button is clicked.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The button node.
+   */
+  fxaButtonClicked(buttonNode) {
+    let panelNode = buttonNode.parentNode;
+    while (panelNode && panelNode.localName != "panel") {
+      panelNode = panelNode.parentNode;
+    }
+    if (panelNode) {
+      panelNode.hidePopup();
+    }
+    gSync.openPrefs();
+  },
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -34,17 +34,17 @@ XPCOMUtils.defineLazyGetter(this, "exten
           PluralForm:false, Preferences:false, PrivateBrowsingUtils:false,
           ProcessHangMonitor:false, PromiseUtils:false, ReaderMode:false,
           ReaderParent:false, RecentWindow:false, SessionStore:false,
           ShortcutUtils:false, SimpleServiceDiscovery:false, SitePermissions:false,
           Social:false, TabCrashHandler:false, Task:false, TelemetryStopwatch:false,
           Translation:false, UITour:false, UpdateUtils:false, Weave:false,
           WebNavigationFrames: false, fxAccounts:false, gDevTools:false,
           gDevToolsBrowser:false, webrtcUI:false, FullZoomUI:false,
-          Marionette:false,
+          Marionette:false, PageActions:false
  */
 
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
  */
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
@@ -60,16 +60,17 @@ XPCOMUtils.defineLazyGetter(this, "exten
   ["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["FullZoomUI", "resource:///modules/FullZoomUI.jsm"],
   ["GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["Log", "resource://gre/modules/Log.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
+  ["PageActions", "resource:///modules/PageActions.jsm"],
   ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
   ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
   ["Preferences", "resource://gre/modules/Preferences.jsm"],
   ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"],
   ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"],
   ["PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"],
   ["ReaderMode", "resource://gre/modules/ReaderMode.jsm"],
   ["ReaderParent", "resource:///modules/ReaderParent.jsm"],
@@ -1408,16 +1409,18 @@ var gBrowserInit = {
     gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
       gIdentityHandler.refreshForInsecureLoginForms();
     });
 
     gBrowser.addEventListener("PermissionStateChange", function() {
       gIdentityHandler.refreshIdentityBlock();
     });
 
+    BrowserPageActions.init();
+
     let uriToLoad = this._getUriToLoad();
     if (uriToLoad && uriToLoad != "about:blank") {
       if (uriToLoad instanceof Ci.nsIArray) {
         let count = uriToLoad.length;
         let specs = [];
         for (let i = 0; i < count; i++) {
           let urisstring = uriToLoad.queryElementAt(i, Ci.nsISupportsString);
           specs.push(urisstring.data);
@@ -7903,128 +7906,16 @@ var gIdentityHandler = {
     container.appendChild(nameLabel);
     container.appendChild(stateLabel);
     container.appendChild(button);
 
     return container;
   }
 };
 
-var gPageActionButton = {
-  get button() {
-    delete this.button;
-    return this.button = document.getElementById("urlbar-page-action-button");
-  },
-
-  get panel() {
-    delete this.panel;
-    return this.panel = document.getElementById("page-action-panel");
-  },
-
-  get sendToDeviceBody() {
-    delete this.sendToDeviceBody;
-    return this.sendToDeviceBody = document.getElementById("page-action-sendToDeviceView-body");
-  },
-
-  onEvent(event) {
-    event.stopPropagation();
-
-    if ((event.type == "click" && event.button != 0) ||
-        (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
-         event.keyCode != KeyEvent.DOM_VK_RETURN)) {
-      return; // Left click, space or enter only
-    }
-
-    this._preparePanelToBeShown();
-    this.panel.hidden = false;
-    this.panel.openPopup(this.button, "bottomcenter topright");
-  },
-
-  _preparePanelToBeShown() {
-    // Update the bookmark item's label.
-    BookmarkingUI.updateBookmarkPageMenuItem();
-
-    // Update the send-to-device item's disabled state.
-    let browser = gBrowser.selectedBrowser;
-    let url = browser.currentURI.spec;
-    let sendToDeviceItem =
-      document.getElementById("page-action-send-to-device-button");
-    sendToDeviceItem.disabled = !gSync.isSendableURI(url);
-  },
-
-  copyURL() {
-    this.panel.hidePopup();
-    Cc["@mozilla.org/widget/clipboardhelper;1"]
-      .getService(Ci.nsIClipboardHelper)
-      .copyString(gBrowser.selectedBrowser.currentURI.spec);
-  },
-
-  emailLink() {
-    this.panel.hidePopup();
-    MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
-  },
-
-  showSendToDeviceView(subviewButton) {
-    this.setupSendToDeviceView();
-    PanelUI.showSubView("page-action-sendToDeviceView", subviewButton);
-  },
-
-  setupSendToDeviceView() {
-    let browser = gBrowser.selectedBrowser;
-    let url = browser.currentURI.spec;
-    let title = browser.contentTitle;
-    let body = this.sendToDeviceBody;
-
-    // This is on top because it also clears the device list between state changes.
-    gSync.populateSendTabToDevicesMenu(body, url, title, (clientId, name, clientType) => {
-      if (!name) {
-        return document.createElement("toolbarseparator");
-      }
-      let item = document.createElement("toolbarbutton");
-      item.classList.add("page-action-sendToDevice-device", "subviewbutton");
-      if (clientId) {
-        item.classList.add("subviewbutton-iconic");
-      }
-      item.setAttribute("tooltiptext", name);
-      return item;
-    });
-
-    if (!gSync.isSignedIn) {
-      // Could be unconfigured or unverified
-      body.setAttribute("state", "notsignedin");
-      return;
-    }
-
-    // In the first ~10 sec after startup, Sync may not be loaded and the list
-    // of devices will be empty.
-    if (!gSync.syncReady) {
-      body.setAttribute("state", "notready");
-      // Force a background Sync
-      Services.tm.dispatchToMainThread(() => {
-        Weave.Service.sync([]);  // [] = clients engine only
-        if (!window.closed && gSync.syncReady) {
-          this.setupSendToDeviceView();
-        }
-      });
-      return;
-    }
-    if (!gSync.remoteClients.length) {
-      body.setAttribute("state", "nodevice");
-      return;
-    }
-
-    body.setAttribute("state", "signedin");
-  },
-
-  fxaButtonClicked() {
-    this.panel.hidePopup();
-    gSync.openPrefs();
-  },
-};
-
 /**
  * Fired on the "marionette-remote-control" system notification,
  * indicating if the browser session is under remote control.
  */
 const gRemoteControl = {
   observe(subject, topic, data) {
     gRemoteControl.updateVisualCue(data);
   },
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -19,16 +19,17 @@
 <script type="application/javascript" src="chrome://browser/content/browser-ctrlTab.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-customization.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-compacttheme.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-fullScreenAndPointerLock.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-fullZoom.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-pageActions.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-refreshblocker.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -69,16 +69,17 @@ browser.jar:
         content/browser/browser-customization.js      (content/browser-customization.js)
         content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
         content/browser/browser-compacttheme.js       (content/browser-compacttheme.js)
         content/browser/browser-feeds.js              (content/browser-feeds.js)
         content/browser/browser-fullScreenAndPointerLock.js  (content/browser-fullScreenAndPointerLock.js)
         content/browser/browser-fullZoom.js           (content/browser-fullZoom.js)
         content/browser/browser-gestureSupport.js     (content/browser-gestureSupport.js)
         content/browser/browser-media.js              (content/browser-media.js)
+        content/browser/browser-pageActions.js        (content/browser-pageActions.js)
         content/browser/browser-places.js             (content/browser-places.js)
         content/browser/browser-plugins.js            (content/browser-plugins.js)
         content/browser/browser-refreshblocker.js     (content/browser-refreshblocker.js)
         content/browser/browser-safebrowsing.js       (content/browser-safebrowsing.js)
         content/browser/browser-sidebar.js            (content/browser-sidebar.js)
         content/browser/browser-social.js             (content/browser-social.js)
         content/browser/browser-sync.js               (content/browser-sync.js)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)