--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -43,137 +43,227 @@ var BrowserPageActions = {
return this.mainViewBodyNode = this.mainViewNode.querySelector(".panel-subview-body");
},
/**
* Inits. Call to init.
*/
init() {
this.placeAllActions();
+ this._onPanelShowing = this._onPanelShowing.bind(this);
+ this.panelNode.addEventListener("popupshowing", this._onPanelShowing);
+ this.panelNode.addEventListener("popuphiding", () => {
+ this.mainButtonNode.removeAttribute("open");
+ });
},
+ _onPanelShowing() {
+ this.placeLazyActionsInPanel();
+ 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.
*/
placeAllActions() {
- // Place actions in the panel. Notify of onBeforePlacedInWindow too.
- for (let action of PageActions.actions) {
- action.onBeforePlacedInWindow(window);
+ let panelActions = PageActions.actionsInPanel(window);
+ for (let action of panelActions) {
this.placeActionInPanel(action);
}
-
- // Place actions in the urlbar. Do this in reverse order. The reason is
- // subtle. If there were no urlbar nodes already in markup (like the
- // bookmark star button), then doing this in forward order would be fine.
- // Forward order means that the insert-before relationship is always broken:
- // there's never a next-sibling node before which to insert a new node, so
- // node.insertBefore() is always passed null, and nodes are always appended.
- // That will break the position of nodes that should be inserted before
- // nodes that are in markup, which in turn can break other nodes.
- let actionsInUrlbar = PageActions.actionsInUrlbar(window);
- for (let i = actionsInUrlbar.length - 1; i >= 0; i--) {
- let action = actionsInUrlbar[i];
+ let urlbarActions = PageActions.actionsInUrlbar(window);
+ for (let action of urlbarActions) {
this.placeActionInUrlbar(action);
}
},
/**
* Adds or removes as necessary DOM nodes for the given action.
*
* @param action (PageActions.Action, required)
* The action to place.
*/
placeAction(action) {
- action.onBeforePlacedInWindow(window);
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.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) {
- let panelViewNode;
- [node, panelViewNode] = this._makePanelButtonNodeForAction(action);
- node.id = id;
- let insertBeforeID = PageActions.nextActionIDInPanel(action);
- let insertBeforeNode =
- insertBeforeID ? this.panelButtonNodeForActionID(insertBeforeID) :
- null;
- this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
- this.updateAction(action);
- this._updateActionDisabledInPanel(action);
- action.onPlacedInPanel(node);
+ return;
+ }
+ node.remove();
+ if (action.getWantsSubview(window)) {
+ let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewNodeID);
if (panelViewNode) {
- action.subview.onPlaced(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.createElement("toolbarseparator");
- return [node, null];
+ return node;
}
-
let buttonNode = document.createElement("toolbarbutton");
buttonNode.classList.add(
"subviewbutton",
"subviewbutton-iconic",
"pageAction-panel-button"
);
buttonNode.setAttribute("actionid", action.id);
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", event => {
this.doCommandForAction(action, event, buttonNode);
});
- return [buttonNode, panelViewNode];
+ return buttonNode;
},
_makePanelViewNodeForAction(action, forUrlbar) {
let panelViewNode = document.createElement("panelview");
panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
panelViewNode.classList.add("PanelUI-subView");
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");
- buttonNode.id =
- this._panelViewButtonNodeIDForActionID(action.id, button.id, forUrlbar);
- 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", event => {
- button.onCommand(event, buttonNode);
- });
- bodyNode.appendChild(buttonNode);
- }
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)
@@ -224,24 +314,20 @@ var BrowserPageActions = {
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");
panelNode.setAttribute("photon", "true");
- if (this._disablePanelAnimations) {
- panelNode.setAttribute("animate", "false");
- }
-
let panelViewNode = null;
let iframeNode = null;
- if (action.subview) {
+ if (action.getWantsSubview(window)) {
let multiViewNode = document.createElement("panelmultiview");
panelViewNode = this._makePanelViewNodeForAction(action, true);
multiViewNode.setAttribute("mainViewId", panelViewNode.id);
multiViewNode.appendChild(panelViewNode);
panelNode.appendChild(multiViewNode);
} else if (action.wantsIframe) {
iframeNode = document.createElement("iframe");
iframeNode.setAttribute("type", "content");
@@ -262,50 +348,36 @@ var BrowserPageActions = {
action.onIframeHiding(iframeNode, panelNode);
}, { once: true });
panelNode.addEventListener("popuphidden", () => {
action.onIframeHidden(iframeNode, panelNode);
}, { once: true });
}
if (panelViewNode) {
- action.subview.onPlaced(panelViewNode);
+ action.onSubviewPlaced(panelViewNode);
panelNode.addEventListener("popupshowing", () => {
- action.subview.onShowing(panelViewNode);
+ action.onSubviewShowing(panelViewNode);
}, { once: true });
}
return panelNode;
},
- // For tests.
- get _disablePanelAnimations() {
- return this.__disablePanelAnimations || false;
- },
- set _disablePanelAnimations(val) {
- this.__disablePanelAnimations = val;
- if (val) {
- this.panelNode.setAttribute("animate", "false");
- } else {
- this.panelNode.removeAttribute("animate");
- }
- },
-
/**
* 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, nonnull) The node to which the action should be
- * anchored.
+ * @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 = [
@@ -357,42 +429,51 @@ var BrowserPageActions = {
node.remove();
}
}
return;
}
let newlyPlaced = false;
if (action.__urlbarNodeInMarkup) {
- newlyPlaced = node && node.hidden;
+ 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) {
- let insertBeforeID = PageActions.nextActionIDInUrlbar(window, action);
- let insertBeforeNode =
- insertBeforeID ? this.urlbarButtonNodeForActionID(insertBeforeID) :
- null;
- this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
- this.updateAction(action);
- action.onPlacedInUrlbar(node);
+ if (!newlyPlaced) {
+ return;
+ }
- // urlbar buttons should always have tooltips, so if the node doesn't have
- // one, then as a last resort use the label of the corresponding panel
- // button. Why not set tooltiptext to action.title when the node is
- // created? Because the consumer may set a title dynamically.
- if (!node.hasAttribute("tooltiptext")) {
- let panelNode = this.panelButtonNodeForActionID(action.id);
- if (panelNode) {
- node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
- }
+ let insertBeforeNode = this._getNextNode(action, true);
+ this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
+ this.updateAction(action, null, {
+ urlbarNode: node,
+ });
+ action.onPlacedInUrlbar(node);
+
+ // urlbar buttons should always have tooltips, so if the node doesn't have
+ // one, then as a last resort use the label of the corresponding panel
+ // button. Why not set tooltiptext to action.title when the node is
+ // created? Because the consumer may set a title dynamically.
+ if (!node.hasAttribute("tooltiptext")) {
+ let panelNode = this.panelButtonNodeForActionID(action.id);
+ if (panelNode) {
+ node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
}
}
},
_makeUrlbarButtonNode(action) {
let buttonNode = document.createElement("image");
buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
buttonNode.setAttribute("actionid", action.id);
@@ -415,170 +496,182 @@ var BrowserPageActions = {
* The action to remove.
*/
removeAction(action) {
this._removeActionFromPanel(action);
this._removeActionFromUrlbar(action);
action.onRemovedFromWindow(window);
},
- _removeActionFromPanel(action) {
- let node = this.panelButtonNodeForActionID(action.id);
- if (node) {
- node.remove();
- }
- if (action.subview) {
- let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
- let panelViewNode = document.getElementById(panelViewNodeID);
- if (panelViewNode) {
- panelViewNode.remove();
- }
- }
- // If there are now no more non-built-in actions, remove the separator
- // between the built-ins and non-built-ins.
- if (!PageActions.nonBuiltInActions.length) {
- let separator = document.getElementById(
- this.panelButtonNodeIDForActionID(
- PageActions.ACTION_ID_BUILT_IN_SEPARATOR
- )
- );
- if (separator) {
- separator.remove();
- }
- }
- },
-
_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 value (optional)
- * If a property name is passed, this argument may contain its
- * current value, in order to prevent a further look-up.
+ * @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, value) {
+ 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, value);
+ this[this._updateMethods[propertyName]](action, panelNode, urlbarNode,
+ value);
} else {
- for (let name of ["iconURL", "title", "tooltip"]) {
- this[this._updateMethods[name]](action, value);
+ for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
+ this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
}
}
},
_updateMethods: {
disabled: "_updateActionDisabled",
iconURL: "_updateActionIconURL",
title: "_updateActionTitle",
tooltip: "_updateActionTooltip",
+ wantsSubview: "_updateActionWantsSubview",
},
- _updateActionDisabled(action, disabled) {
- this._updateActionDisabledInPanel(action, disabled);
+ _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, disabled = action.getDisabled(window)) {
- let panelButton = this.panelButtonNodeForActionID(action.id);
- if (panelButton) {
+ _updateActionDisabledInPanel(action, panelNode,
+ disabled = action.getDisabled(window)) {
+ if (panelNode) {
if (disabled) {
- panelButton.setAttribute("disabled", "true");
+ panelNode.setAttribute("disabled", "true");
} else {
- panelButton.removeAttribute("disabled");
+ panelNode.removeAttribute("disabled");
}
}
},
- _updateActionIconURL(action, properties = action.getIconProperties(window)) {
- let panelButton = this.panelButtonNodeForActionID(action.id);
- let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
-
+ _updateActionIconURL(action, panelNode, urlbarNode,
+ properties = action.getIconProperties(window)) {
for (let [prop, value] of Object.entries(properties)) {
- if (panelButton) {
- panelButton.style.setProperty(prop, value);
+ if (panelNode) {
+ panelNode.style.setProperty(prop, value);
}
- if (urlbarButton) {
- urlbarButton.style.setProperty(prop, value);
+ if (urlbarNode) {
+ urlbarNode.style.setProperty(prop, value);
}
}
},
- _updateActionTitle(action, title = action.getTitle(window)) {
+ _updateActionTitle(action, panelNode, urlbarNode,
+ title = action.getTitle(window)) {
if (!title) {
// `title` is a required action property, but the bookmark action's is an
// empty string since its actual title is set via
// BookmarkingUI.updateBookmarkPageMenuItem(). The purpose of this early
// return is to ignore that empty title.
return;
}
- let panelButton = this.panelButtonNodeForActionID(action.id);
- if (panelButton) {
- panelButton.setAttribute("label", title);
+ if (panelNode) {
+ panelNode.setAttribute("label", title);
}
-
- let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
- if (urlbarButton) {
- urlbarButton.setAttribute("aria-label", title);
-
- // tooltiptext falls back to the title, so update it, too.
- this._updateActionTooltip(action, undefined, title, urlbarButton);
+ if (urlbarNode) {
+ urlbarNode.setAttribute("aria-label", title);
+ // tooltiptext falls back to the title, so update it too if necessary.
+ let tooltip = action.getTooltip(window);
+ if (!tooltip && title) {
+ urlbarNode.setAttribute("tooltiptext", title);
+ }
}
},
- _updateActionTooltip(action, tooltip = action.getTooltip(window),
- title,
- node = this.urlbarButtonNodeForActionID(action.id)) {
- if (node) {
- if (!tooltip && title === undefined) {
- title = action.getTitle(window);
+ _updateActionTooltip(action, panelNode, urlbarNode,
+ tooltip = action.getTooltip(window)) {
+ if (urlbarNode) {
+ if (!tooltip) {
+ tooltip = action.getTitle(window);
+ }
+ if (tooltip) {
+ urlbarNode.setAttribute("tooltiptext", tooltip);
}
- node.setAttribute("tooltiptext", tooltip || title);
+ }
+ },
+
+ _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) {
if (event && event.type == "click" && event.button != 0) {
return;
}
PageActions.logTelemetry("used", action, buttonNode);
// 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.subview &&
+ if (action.getWantsSubview(window) &&
buttonNode &&
buttonNode.closest("panel") == this.panelNode) {
let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
let panelViewNode = document.getElementById(panelViewNodeID);
- action.subview.onShowing(panelViewNode);
+ action.onSubviewShowing(panelViewNode);
this.multiViewNode.showSubView(panelViewNode, buttonNode);
return;
}
// Otherwise, hide the main popup in case it was open:
PanelMultiView.hidePopup(this.panelNode);
- // Toggle the activated action's panel if necessary
- if (action.subview || action.wantsIframe) {
+ let aaPanelNode = this.activatedActionPanelNode;
+ if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) {
+ action.onCommand(event, buttonNode);
+ }
+ if (action.getWantsSubview(window) || action.wantsIframe) {
this.togglePanelForAction(action);
- return;
}
-
- // Otherwise, run the action.
- action.onCommand(event, buttonNode);
},
/**
* 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.
@@ -656,22 +749,16 @@ var BrowserPageActions = {
},
// 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 given button in the given action's panelview.
- _panelViewButtonNodeIDForActionID(actionID, buttonID, forUrlbar) {
- let placementID = forUrlbar ? "urlbar" : "panel";
- return `pageAction-${placementID}-${actionID}-${buttonID}`;
- },
-
// 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) {
@@ -718,25 +805,17 @@ var BrowserPageActions = {
/**
* 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) {
- for (let action of PageActions.actions) {
- let buttonNode = this.panelButtonNodeForActionID(action.id);
- action.onShowingInPanel(buttonNode);
- }
-
this.panelNode.hidden = false;
- this.panelNode.addEventListener("popuphiding", () => {
- this.mainButtonNode.removeAttribute("open");
- }, {once: true});
this.mainButtonNode.setAttribute("open", "true");
PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
position: "bottomcenter topright",
triggerEvent: event,
}).catch(Cu.reportError);
},
/**
@@ -961,16 +1040,25 @@ BrowserPageActions.emailLink = {
BrowserPageActions.sendToDevice = {
onPlacedInPanel(buttonNode) {
let action = PageActions.actionForID("sendToDevice");
BrowserPageActions.takeActionTitleFromPanel(action);
},
onSubviewPlaced(panelViewNode) {
let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+ let notReady = document.createElement("toolbarbutton");
+ notReady.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "pageAction-sendToDevice-notReady"
+ );
+ notReady.setAttribute("label", "sendToDevice-notReadyTitle");
+ notReady.setAttribute("disabled", "true");
+ bodyNode.appendChild(notReady);
for (let node of bodyNode.childNodes) {
BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
}
},
onLocationChange() {
let action = PageActions.actionForID("sendToDevice");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1381,18 +1381,18 @@ toolbarpaletteitem[place="palette"][hidd
.dragfeedback-tab {
-moz-appearance: none;
opacity: 0.65;
-moz-window-shadow: none;
}
/* Page action panel */
-#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > #pageAction-panel-sendToDevice-notReady,
-#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > #pageAction-urlbar-sendToDevice-notReady {
+#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady,
+#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady {
display: none;
}
/* Page action buttons */
.pageAction-panel-button > .toolbarbutton-icon {
list-style-image: var(--pageAction-image-16px, inherit);
}
.urlbar-page-action {
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -9,25 +9,16 @@ registerCleanupFunction(function() {
const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017
const mockRemoteClients = [
{ id: "0", name: "foo", type: "mobile", serverLastModified: lastModifiedFixture },
{ id: "1", name: "bar", type: "desktop", serverLastModified: lastModifiedFixture },
{ id: "2", name: "baz", type: "mobile", serverLastModified: lastModifiedFixture },
];
-add_task(async function init() {
- // Disable panel animations. They cause intermittent timeouts on Linux when
- // the test tries to synthesize clicks on items in newly opened panels.
- BrowserPageActions._disablePanelAnimations = true;
- registerCleanupFunction(() => {
- BrowserPageActions._disablePanelAnimations = false;
- });
-});
-
add_task(async function bookmark() {
// Open a unique page.
let url = "http://example.com/browser_page_action_menu";
await BrowserTestUtils.withNewTab(url, async () => {
// Open the panel.
await promisePageActionPanelOpen();
// The bookmark button should read "Bookmark This Page" and not be starred.
@@ -231,17 +222,17 @@ add_task(async function sendToDevice_syn
// Click Send to Device.
let viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
let expectedItems = [
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
{
attrs: {
label: "Account Not Verified",
},
disabled: true
@@ -304,25 +295,25 @@ add_task(async function sendToDevice_syn
let view = await viewPromise;
Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
function testSendTabToDeviceMenu(numCall) {
if (numCall == 1) {
// "Syncing devices" should be shown.
checkSendToDeviceItems([
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
disabled: true,
},
]);
} else if (numCall == 2) {
// The devices should be shown in the subview.
let expectedItems = [
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
];
for (let client of mockRemoteClients) {
expectedItems.push({
attrs: {
clientId: client.id,
@@ -368,17 +359,17 @@ add_task(async function sendToDevice_not
// Click Send to Device.
let viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
let expectedItems = [
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
{
attrs: {
label: "Not Connected to Sync",
},
disabled: true
@@ -429,17 +420,17 @@ add_task(async function sendToDevice_noD
// Click Send to Device.
let viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
let expectedItems = [
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
{
attrs: {
label: "No Devices Connected",
},
disabled: true
@@ -495,17 +486,17 @@ add_task(async function sendToDevice_dev
let viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
// The devices should be shown in the subview.
let expectedItems = [
{
- id: "pageAction-panel-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
];
for (let client of mockRemoteClients) {
expectedItems.push({
attrs: {
clientId: client.id,
@@ -565,17 +556,17 @@ add_task(async function sendToDevice_inU
// only after the associated button has been clicked.
await promisePanelShown(BrowserPageActions._activatedActionPanelID);
Assert.equal(urlbarButton.getAttribute("open"), "true",
"Button has open attribute");
// The devices should be shown in the subview.
let expectedItems = [
{
- id: "pageAction-urlbar-sendToDevice-notReady",
+ className: "pageAction-sendToDevice-notReady",
display: "none",
disabled: true,
},
];
for (let client of mockRemoteClients) {
expectedItems.push({
attrs: {
clientId: client.id,
@@ -767,16 +758,23 @@ function checkSendToDeviceItems(expected
let actual = body.childNodes[i];
if (!expected) {
Assert.equal(actual.localName, "toolbarseparator");
continue;
}
if ("id" in expected) {
Assert.equal(actual.id, expected.id);
}
+ if ("className" in expected) {
+ let expectedNames = expected.className.split(/\s+/);
+ for (let name of expectedNames) {
+ Assert.ok(actual.classList.contains(name),
+ `classList contains: ${name}`);
+ }
+ }
let display = "display" in expected ? expected.display : "-moz-box";
Assert.equal(getComputedStyle(actual).display, display);
let disabled = "disabled" in expected ? expected.disabled : false;
Assert.equal(actual.disabled, disabled);
if ("attrs" in expected) {
for (let name in expected.attrs) {
Assert.ok(actual.hasAttribute(name));
let attrVal = actual.getAttribute(name);
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -153,16 +153,17 @@ var UITour = {
allowAdd: true,
query: (aDocument) => {
// The pocket's urlbar page action button is pre-defined in the DOM.
// It would be hidden if toggled off from the urlbar.
let node = aDocument.getElementById("pocket-button-box");
if (node && !node.hidden) {
return node;
}
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-panel-pocket");
},
}],
["privateWindow", {query: "#appMenu-private-window-button"}],
["quit", {query: "#appMenu-quit-button"}],
["readerMode-urlBar", {query: "#reader-mode-button"}],
["search", {
infoPanelOffsetX: 18,
@@ -219,39 +220,44 @@ var UITour = {
["pageAction-bookmark", {
query: (aDocument) => {
// The bookmark's urlbar page action button is pre-defined in the DOM.
// It would be hidden if toggled off from the urlbar.
let node = aDocument.getElementById("star-button-box");
if (node && !node.hidden) {
return node;
}
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-panel-bookmark");
},
}],
["pageAction-copyURL", {
query: (aDocument) => {
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-urlbar-copyURL") ||
aDocument.getElementById("pageAction-panel-copyURL");
},
}],
["pageAction-emailLink", {
query: (aDocument) => {
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-urlbar-emailLink") ||
aDocument.getElementById("pageAction-panel-emailLink");
},
}],
["pageAction-sendToDevice", {
query: (aDocument) => {
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-urlbar-sendToDevice") ||
aDocument.getElementById("pageAction-panel-sendToDevice");
},
}],
["screenshots", {
query: (aDocument) => {
+ aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
return aDocument.getElementById("pageAction-urlbar-screenshots") ||
aDocument.getElementById("pageAction-panel-screenshots");
},
}]
]),
init() {
log.debug("Initializing UITour");
--- a/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
+++ b/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
@@ -3,22 +3,89 @@
const BUTTON_ID = "pageAction-panel-screenshots";
function checkElements(expectPresent, l) {
for (const id of l) {
is(!!document.getElementById(id), expectPresent, "element " + id + (expectPresent ? " is" : " is not") + " present");
}
}
+async function togglePageActionPanel() {
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelEvent("popuphidden");
+}
+
+function promiseOpenPageActionPanel() {
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return BrowserTestUtils.waitForCondition(() => {
+ // Wait for the main page action button to become visible. It's hidden for
+ // some URIs, so depending on when this is called, it may not yet be quite
+ // visible. It's up to the caller to make sure it will be visible.
+ info("Waiting for main page action button to have non-0 size");
+ let bounds = dwu.getBoundsWithoutFlushing(BrowserPageActions.mainButtonNode);
+ return bounds.width > 0 && bounds.height > 0;
+ }).then(() => {
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (BrowserPageActions.panelNode.state == "open") {
+ return Promise.resolve();
+ }
+ let shownPromise = promisePageActionPanelEvent("popupshown");
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ return shownPromise;
+ }).then(() => {
+ // Wait for items in the panel to become visible.
+ return promisePageActionViewChildrenVisible(BrowserPageActions.mainViewNode);
+ });
+}
+
+function promisePageActionPanelEvent(eventType) {
+ return new Promise(resolve => {
+ let panel = BrowserPageActions.panelNode;
+ if ((eventType == "popupshown" && panel.state == "open") ||
+ (eventType == "popuphidden" && panel.state == "closed")) {
+ executeSoon(resolve);
+ return;
+ }
+ panel.addEventListener(eventType, () => {
+ executeSoon(resolve);
+ }, { once: true });
+ });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+ info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return BrowserTestUtils.waitForCondition(() => {
+ let bodyNode = panelViewNode.firstChild;
+ for (let childNode of bodyNode.childNodes) {
+ let bounds = dwu.getBoundsWithoutFlushing(childNode);
+ if (bounds.width > 0 && bounds.height > 0) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
add_task(async function() {
await promiseScreenshotsEnabled();
registerCleanupFunction(async function() {
await promiseScreenshotsReset();
});
+ // Toggle the page action panel to get it to rebuild itself. An actionable
+ // page must be opened first.
+ let url = "http://example.com/browser_screenshots_ui_check";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await togglePageActionPanel();
- await BrowserTestUtils.waitForCondition(
- () => document.getElementById(BUTTON_ID),
- "Screenshots button should be present", 100, 100);
+ await BrowserTestUtils.waitForCondition(
+ () => document.getElementById(BUTTON_ID),
+ "Screenshots button should be present", 100, 100);
- checkElements(true, [BUTTON_ID]);
+ checkElements(true, [BUTTON_ID]);
+ });
});
--- a/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
@@ -1,28 +1,25 @@
const REPORTABLE_PAGE = "http://example.com/";
const REPORTABLE_PAGE2 = "https://example.com/";
const NONREPORTABLE_PAGE = "about:mozilla";
/* Test that the Report Site Issue button is enabled for http and https tabs,
on page load, or TabSelect, and disabled for everything else. */
add_task(async function test_button_state_disabled() {
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
- openPageActions();
- await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+ await openPageActions();
is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
- openPageActions();
- await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+ await openPageActions();
is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on tab load");
let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
- openPageActions();
- await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+ await openPageActions();
is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
BrowserTestUtils.removeTab(tab3);
});
/* Test that the button is enabled or disabled when we expected it to be, when
--- a/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
@@ -3,29 +3,29 @@
add_task(async function test_opened_page() {
requestLongerTimeout(2);
// ./head.js sets the value for PREF_WC_REPORTER_ENDPOINT
await SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENDPOINT, NEW_ISSUE_PAGE]]});
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ await openPageActions();
let webcompatButton = document.getElementById(WC_PAGE_ACTION_ID);
ok(webcompatButton, "Report Site Issue button exists.");
let screenshotPromise;
let newTabPromise = new Promise(resolve => {
gBrowser.tabContainer.addEventListener("TabOpen", event => {
let tab = event.target;
screenshotPromise = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser, "ScreenshotReceived", false, null, true);
resolve(tab);
}, { once: true });
});
- openPageActions();
webcompatButton.click();
let tab2 = await newTabPromise;
await screenshotPromise;
await ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, function(args) {
let doc = content.document;
let urlParam = doc.getElementById("url").innerText;
let preview = doc.getElementById("screenshot-preview");
--- a/browser/extensions/webcompat-reporter/test/browser/head.js
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -12,18 +12,66 @@ function isButtonDisabled() {
return document.getElementById(WC_PAGE_ACTION_ID).disabled;
}
function isURLButtonEnabled() {
return document.getElementById(WC_PAGE_ACTION_URLBAR_ID) !== null;
}
function openPageActions() {
- var event = new MouseEvent("click");
- BrowserPageActions.mainButtonClicked(event);
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return BrowserTestUtils.waitForCondition(() => {
+ // Wait for the main page action button to become visible. It's hidden for
+ // some URIs, so depending on when this is called, it may not yet be quite
+ // visible. It's up to the caller to make sure it will be visible.
+ info("Waiting for main page action button to have non-0 size");
+ let bounds = dwu.getBoundsWithoutFlushing(BrowserPageActions.mainButtonNode);
+ return bounds.width > 0 && bounds.height > 0;
+ }).then(() => {
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (BrowserPageActions.panelNode.state == "open") {
+ return Promise.resolve();
+ }
+ let shownPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ return shownPromise;
+ }).then(() => {
+ // Wait for items in the panel to become visible.
+ return promisePageActionViewChildrenVisible(BrowserPageActions.mainViewNode);
+ });
+}
+
+function promisePageActionPanelShown() {
+ return new Promise(resolve => {
+ if (BrowserPageActions.panelNode.state == "open") {
+ executeSoon(resolve);
+ return;
+ }
+ BrowserPageActions.panelNode.addEventListener("popupshown", () => {
+ executeSoon(resolve);
+ }, { once: true });
+ });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+ info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return BrowserTestUtils.waitForCondition(() => {
+ let bodyNode = panelViewNode.firstChild;
+ for (let childNode of bodyNode.childNodes) {
+ let bounds = dwu.getBoundsWithoutFlushing(childNode);
+ if (bounds.width > 0 && bounds.height > 0) {
+ return true;
+ }
+ }
+ return false;
+ });
}
function pinToURLBar() {
PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = true;
}
function unpinFromURLBar() {
PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = false;
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -2,37 +2,37 @@
* 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/. */
"use strict";
var EXPORTED_SYMBOLS = [
"PageActions",
// PageActions.Action
- // PageActions.Button
- // PageActions.Subview
// PageActions.ACTION_ID_BOOKMARK
// PageActions.ACTION_ID_BOOKMARK_SEPARATOR
// PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ // PageActions.ACTION_ID_TRANSIENT_SEPARATOR
];
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
ChromeUtils.defineModuleGetter(this, "BinarySearch",
"resource://gre/modules/BinarySearch.jsm");
const ACTION_ID_BOOKMARK = "bookmark";
const ACTION_ID_BOOKMARK_SEPARATOR = "bookmarkSeparator";
const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
+const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
// Escapes the given raw URL string, and returns an equivalent CSS url()
// value for it.
function escapeCSSURL(url) {
return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
@@ -79,54 +79,68 @@ var PageActions = {
"PageActions: purging unregistered actions from cache",
() => this._purgeUnregisteredPersistedActions(),
);
},
_deferredAddActionCalls: [],
/**
- * The list of Action objects, sorted in the order in which they should be
- * placed in the page action panel. If there are both built-in and non-built-
- * in actions, then the list will include the separator between the two. The
- * list is not live. (array of Action objects)
+ * A list of all Action objects, not in any particular order. Not live.
+ * (array of Action objects)
*/
get actions() {
- let actions = this.builtInActions;
- if (this.nonBuiltInActions.length) {
- // There are non-built-in actions, so include them too.
+ let lists = [
+ this._builtInActions,
+ this._nonBuiltInActions,
+ this._transientActions,
+ ];
+ return lists.reduce((memo, list) => memo.concat(list), []);
+ },
+
+ /**
+ * The list of Action objects that should appear in the panel for a given
+ * window, sorted in the order in which they appear. If there are both
+ * built-in and non-built-in actions, then the list will include the separator
+ * between the two. The list is not live. (array of Action objects)
+ *
+ * @param browserWindow (DOM window, required)
+ * This window's actions will be returned.
+ * @return (array of PageAction.Action objects) The actions currently in the
+ * given window's panel.
+ */
+ actionsInPanel(browserWindow) {
+ function filter(action) {
+ return action.shouldShowInPanel(browserWindow);
+ }
+ let actions = this._builtInActions.filter(filter);
+ let nonBuiltInActions = this._nonBuiltInActions.filter(filter);
+ if (nonBuiltInActions.length) {
if (actions.length) {
- // There are both built-in and non-built-in actions. Add a separator
- // between the two groups so that the returned array looks like:
- // [...built-ins, separator, ...non-built-ins]
actions.push(new Action({
id: ACTION_ID_BUILT_IN_SEPARATOR,
_isSeparator: true,
}));
}
- actions.push(...this.nonBuiltInActions);
+ actions.push(...nonBuiltInActions);
+ }
+ let transientActions = this._transientActions.filter(filter);
+ if (transientActions.length) {
+ if (actions.length) {
+ actions.push(new Action({
+ id: ACTION_ID_TRANSIENT_SEPARATOR,
+ _isSeparator: true,
+ }));
+ }
+ actions.push(...transientActions);
}
return actions;
},
/**
- * The list of built-in actions. Not live. (array of Action objects)
- */
- get builtInActions() {
- return this._builtInActions.slice();
- },
-
- /**
- * The list of non-built-in actions. Not live. (array of Action objects)
- */
- get nonBuiltInActions() {
- return this._nonBuiltInActions.slice();
- },
-
- /**
* The list of actions currently in the urlbar, sorted in the order in which
* they appear. Not live.
*
* @param browserWindow (DOM window, required)
* This window's actions will be returned.
* @return (array of PageAction.Action objects) The actions currently in the
* given window's urlbar.
*/
@@ -170,35 +184,20 @@ var PageActions = {
*/
addAction(action) {
if (this._deferredAddActionCalls) {
// init() hasn't been called yet. Defer all additions until it's called,
// at which time _deferredAddActionCalls will be deleted.
this._deferredAddActionCalls.push(() => this.addAction(action));
return action;
}
-
- let hadSep = this.actions.some(a => a.id == ACTION_ID_BUILT_IN_SEPARATOR);
-
this._registerAction(action);
-
- let sep = null;
- if (!hadSep) {
- sep = this.actions.find(a => a.id == ACTION_ID_BUILT_IN_SEPARATOR);
- }
-
for (let bpa of allBrowserPageActions()) {
- if (sep) {
- // There are now both built-in and non-built-in actions, so place the
- // separator between the two groups.
- bpa.placeAction(sep);
- }
bpa.placeAction(action);
}
-
return action;
},
_registerAction(action) {
if (this.actionForID(action.id)) {
throw new Error(`Action with ID '${action.id}' already added`);
}
this._actionsByID.set(action.id, action);
@@ -213,20 +212,23 @@ var PageActions = {
// bundled with the browser. Right now we simply assume that no other
// consumers will use _insertBeforeActionID.
let index =
!action.__insertBeforeActionID ? -1 :
this._builtInActions.findIndex(a => {
return a.id == action.__insertBeforeActionID;
});
if (index < 0) {
- // Append the action.
- index = this._builtInActions.length;
+ // Append the action (excluding transient actions).
+ index = this._builtInActions.filter(a => !a.__transient).length;
}
this._builtInActions.splice(index, 0, action);
+ } else if (action.__transient) {
+ // A transient action.
+ this._transientActions.push(action);
} else if (gBuiltInActions.find(a => a.id == action.id)) {
// A built-in action. These are always added on init before all other
// actions, one after the other, so just push onto the array.
this._builtInActions.push(action);
} else {
// A non-built-in action, like a non-bundled extension potentially.
// Keep this list sorted by title.
let index = BinarySearch.insertionIndexOf((a1, a2) => {
@@ -264,95 +266,38 @@ var PageActions = {
this._persistedActions.idsInUrlbar.splice(index, 1);
}
this._storePersistedActions();
},
// These keep track of currently registered actions.
_builtInActions: [],
_nonBuiltInActions: [],
+ _transientActions: [],
_actionsByID: new Map(),
/**
- * Returns the ID of the action before which the given action should be
- * inserted in the urlbar.
- *
- * @param action (Action object, required)
- * The action you're inserting.
- * @return The ID of the reference action, or null if your action should be
- * appended.
- */
- nextActionIDInUrlbar(browserWindow, action) {
- // Actions in the urlbar are always inserted before the bookmark action,
- // which always comes last if it's present.
- if (action.id == ACTION_ID_BOOKMARK) {
- return null;
- }
- let id = this._nextActionID(action, this.actionsInUrlbar(browserWindow));
- return id || ACTION_ID_BOOKMARK;
- },
-
- /**
- * Returns the ID of the action before which the given action should be
- * inserted in the panel.
- *
- * @param action (Action object, required)
- * The action you're inserting.
- * @return The ID of the reference action, or null if your action should be
- * appended.
- */
- nextActionIDInPanel(action) {
- return this._nextActionID(action, this.actions);
- },
-
- /**
- * The DOM nodes of actions should be ordered properly, both in the panel and
- * the urlbar. This method returns the ID of the action that comes after the
- * given action in the given array. You can use the returned ID to get a DOM
- * node ID to pass to node.insertBefore().
- *
- * Pass PageActions.actions to get the ID of the next action in the panel.
- * Pass PageActions.actionsInUrlbar to get the ID of the next action in the
- * urlbar.
- *
- * @param action
- * The action whose node you want to insert into your DOM.
- * @param actionArray
- * The relevant array of actions, either PageActions.actions or
- * actionsInUrlbar.
- * @return The ID of the action before which the given action should be
- * inserted. If the given action should be inserted last, returns
- * null.
- */
- _nextActionID(action, actionArray) {
- let index = actionArray.findIndex(a => a.id == action.id);
- if (index < 0) {
- return null;
- }
- let nextAction = actionArray[index + 1];
- if (!nextAction) {
- return null;
- }
- return nextAction.id;
- },
-
- /**
* Call this when an action is removed.
*
* @param action (Action object, required)
* The action that was removed.
*/
onActionRemoved(action) {
if (!this.actionForID(action.id)) {
// The action isn't registered (yet). Not an error.
return;
}
this._actionsByID.delete(action.id);
- for (let list of [this._nonBuiltInActions, this._builtInActions]) {
+ let lists = [
+ this._builtInActions,
+ this._nonBuiltInActions,
+ this._transientActions,
+ ];
+ for (let list of lists) {
let index = list.findIndex(a => a.id == action.id);
if (index >= 0) {
list.splice(index, 1);
break;
}
}
for (let bpa of allBrowserPageActions()) {
@@ -396,16 +341,17 @@ var PageActions = {
}
},
// For tests. See Bug 1413692.
_reset() {
PageActions._purgeUnregisteredPersistedActions();
PageActions._builtInActions = [];
PageActions._nonBuiltInActions = [];
+ PageActions._transientActions = [];
PageActions._actionsByID = new Map();
},
_storePersistedActions() {
let json = JSON.stringify(this._persistedActions);
Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
},
@@ -467,16 +413,17 @@ var PageActions = {
version: PERSISTED_ACTIONS_CURRENT_VERSION,
// action IDs that have ever been seen and not removed, order not important
ids: [],
// action IDs ordered by position in urlbar
idsInUrlbar: [],
},
};
+
/**
* A single page action.
*
* Each action can have both per-browser-window state and global state.
* Per-window state takes precedence over global state. This is reflected in
* the title, tooltip, disabled, and icon properties. Each of these properties
* has a getter method and setter method that takes a browser window. Pass null
* to get the action's global state. Pass a browser window to get the per-
@@ -551,32 +498,39 @@ var PageActions = {
* @param onRemovedFromWindow (function, optional)
* Called after the action is removed from a browser window:
* onRemovedFromWindow(browserWindow)
* * browserWindow: The browser window that the action was removed from.
* @param onShowingInPanel (function, optional)
* Called when a browser window's page action panel is showing:
* onShowingInPanel(buttonNode)
* * buttonNode: The action's node in the page action panel.
+ * @param onSubviewPlaced (function, optional)
+ * Called when the action's subview is added to its parent panel in a
+ * browser window:
+ * onSubviewPlaced(panelViewNode)
+ * * panelViewNode: The subview's panelview node.
+ * @param onSubviewShowing (function, optional)
+ * Called when the action's subview is showing in a browser window:
+ * onSubviewShowing(panelViewNode)
+ * * panelViewNode: The subview's panelview node.
* @param pinnedToUrlbar (bool, optional)
* Pass true to pin the action to the urlbar. An action is shown in the
* urlbar if it's pinned and not disabled. False by default.
- * @param subview (object, optional)
- * An options object suitable for passing to the Subview constructor, if
- * you'd like the action to have a subview. See the subview constructor
- * for info on this object's properties.
* @param tooltip (string, optional)
* The action's button tooltip text.
* @param urlbarIDOverride (string, optional)
* Usually the ID of the action's button in the urlbar will be generated
* automatically. Pass a string for this property to override that with
* your own ID.
* @param wantsIframe (bool, optional)
* Pass true to make an action that shows an iframe in a panel when
* clicked.
+ * @param wantsSubview (bool, optional)
+ * Pass true to make an action that shows a panel subview when clicked.
*/
function Action(options) {
setProperties(this, options, {
id: true,
title: !options._isSeparator,
anchorIDOverride: false,
disabled: false,
extensionID: false,
@@ -588,121 +542,127 @@ function Action(options) {
onIframeHiding: false,
onIframeHidden: false,
onIframeShowing: false,
onLocationChange: false,
onPlacedInPanel: false,
onPlacedInUrlbar: false,
onRemovedFromWindow: false,
onShowingInPanel: false,
+ onSubviewPlaced: false,
+ onSubviewShowing: false,
pinnedToUrlbar: false,
- subview: false,
tooltip: false,
urlbarIDOverride: false,
wantsIframe: false,
+ wantsSubview: false,
// private
// (string, optional)
// The ID of another action before which to insert this new action in the
// panel.
_insertBeforeActionID: false,
// (bool, optional)
// True if this isn't really an action but a separator to be shown in the
// page action panel.
_isSeparator: false,
// (bool, optional)
+ // Transient actions have a couple of special properties: (1) They stick to
+ // the bottom of the panel, and (2) they're hidden in the panel when they're
+ // disabled. Other than that they behave like other actions.
+ _transient: false,
+
+ // (bool, optional)
// True if the action's urlbar button is defined in markup. In that case, a
// node with the action's urlbar node ID should already exist in the DOM
// (either the auto-generated ID or urlbarIDOverride). That node will be
// shown when the action is added to the urlbar and hidden when the action
// is removed from the urlbar.
_urlbarNodeInMarkup: false,
});
- if (this._subview) {
- this._subview = new Subview(options.subview);
- }
/**
* A cache of the pre-computed CSS variable values for a given icon
* URLs object, as passed to _createIconProperties.
*/
this._iconProperties = new WeakMap();
/**
* The global values for the action properties.
*/
this._globalProps = {
disabled: this._disabled,
iconURL: this._iconURL,
iconProps: this._createIconProperties(this._iconURL),
title: this._title,
tooltip: this._tooltip,
+ wantsSubview: this._wantsSubview,
};
/**
* A mapping of window-specific action property objects, each of which
* derives from the _globalProps object.
*/
this._windowProps = new WeakMap();
}
Action.prototype = {
/**
- * The ID of the action's parent extension (string, nullable)
+ * The ID of the action's parent extension (string)
*/
get extensionID() {
return this._extensionID;
},
/**
- * The action's ID (string, nonnull)
+ * The action's ID (string)
*/
get id() {
return this._id;
},
/**
* Attribute name => value mapping to set on nodes created for this action
- * (object, nullable)
+ * (object)
*/
get nodeAttributes() {
return this._nodeAttributes;
},
/**
* True if the action is pinned to the urlbar. The action is shown in the
- * urlbar if it's pinned and not disabled. (bool, nonnull)
+ * urlbar if it's pinned and not disabled. (bool)
*/
get pinnedToUrlbar() {
return this._pinnedToUrlbar || false;
},
set pinnedToUrlbar(shown) {
if (this.pinnedToUrlbar != shown) {
this._pinnedToUrlbar = shown;
PageActions.onActionToggledPinnedToUrlbar(this);
}
return this.pinnedToUrlbar;
},
/**
- * The action's disabled state (bool, nonnull)
+ * The action's disabled state (bool)
*/
getDisabled(browserWindow = null) {
return !!this._getProperties(browserWindow).disabled;
},
setDisabled(value, browserWindow = null) {
return this._setProperty("disabled", !!value, browserWindow);
},
/**
* The action's icon URL string, or an object mapping sizes to URL strings
- * (string or object, nullable)
+ * (string or object)
*/
getIconURL(browserWindow = null) {
return this._getProperties(browserWindow).iconURL;
},
setIconURL(value, browserWindow = null) {
let props = this._getProperties(browserWindow, !!browserWindow);
props.iconURL = value;
props.iconProps = this._createIconProperties(value);
@@ -734,36 +694,46 @@ Action.prototype = {
return Object.freeze({
"--pageAction-image-16px": null,
"--pageAction-image-32px": urls ? escapeCSSURL(urls) : null,
});
},
/**
- * The action's title (string, nonnull)
+ * The action's title (string)
*/
getTitle(browserWindow = null) {
return this._getProperties(browserWindow).title;
},
setTitle(value, browserWindow = null) {
return this._setProperty("title", value, browserWindow);
},
/**
- * The action's tooltip (string, nullable)
+ * The action's tooltip (string)
*/
getTooltip(browserWindow = null) {
return this._getProperties(browserWindow).tooltip;
},
setTooltip(value, browserWindow = null) {
return this._setProperty("tooltip", value, browserWindow);
},
/**
+ * Whether the action wants a subview (bool)
+ */
+ getWantsSubview(browserWindow = null) {
+ return !!this._getProperties(browserWindow).wantsSubview;
+ },
+ setWantsSubview(value, browserWindow = null) {
+ return this._setProperty("wantsSubview", !!value, browserWindow);
+ },
+
+ /**
* Sets a property, optionally for a particular browser window.
*
* @param name (string, required)
* The (non-underscored) name of the property.
* @param value
* The value.
* @param browserWindow (DOM window, optional)
* If given, then the property will be set in this window's state, not
@@ -776,17 +746,17 @@ Action.prototype = {
this._updateProperty(name, value, browserWindow);
return value;
},
_updateProperty(name, value, browserWindow) {
// This may be called before the action has been added.
if (PageActions.actionForID(this.id)) {
for (let bpa of allBrowserPageActions(browserWindow)) {
- bpa.updateAction(this, name, value);
+ bpa.updateAction(this, name, { value });
}
}
},
/**
* Returns the properties object for the given window, if it exists,
* or the global properties object if no window-specific properties
* exist.
@@ -807,44 +777,36 @@ Action.prototype = {
props = Object.create(this._globalProps);
this._windowProps.set(window, props);
}
return props || this._globalProps;
},
/**
- * Override for the ID of the action's activated-action panel anchor (string,
- * nullable)
+ * Override for the ID of the action's activated-action panel anchor (string)
*/
get anchorIDOverride() {
return this._anchorIDOverride;
},
/**
- * Override for the ID of the action's urlbar node (string, nullable)
+ * Override for the ID of the action's urlbar node (string)
*/
get urlbarIDOverride() {
return this._urlbarIDOverride;
},
/**
- * True if the action is shown in an iframe (bool, nonnull)
+ * True if the action is shown in an iframe (bool)
*/
get wantsIframe() {
return this._wantsIframe || false;
},
- /**
- * A Subview object if the action wants a subview (Subview, nullable)
- */
- get subview() {
- return this._subview;
- },
-
get labelForHistogram() {
return this._labelForHistogram || this._id;
},
/**
* Returns the URL of the best icon to use given a preferred size. The best
* icon is the one with the smallest size that's equal to or bigger than the
* preferred size. Returns null if the action has no icon URL.
@@ -1026,28 +988,66 @@ Action.prototype = {
*/
onShowingInPanel(buttonNode) {
if (this._onShowingInPanel) {
this._onShowingInPanel(buttonNode);
}
},
/**
+ * Call this when a panelview node for the action's subview is added to the
+ * DOM.
+ *
+ * @param panelViewNode (DOM node, required)
+ * The subview's panelview node.
+ */
+ onSubviewPlaced(panelViewNode) {
+ if (this._onSubviewPlaced) {
+ this._onSubviewPlaced(panelViewNode);
+ }
+ },
+
+ /**
+ * Call this when a panelview node for the action's subview is showing.
+ *
+ * @param panelViewNode (DOM node, required)
+ * The subview's panelview node.
+ */
+ onSubviewShowing(panelViewNode) {
+ if (this._onSubviewShowing) {
+ this._onSubviewShowing(panelViewNode);
+ }
+ },
+
+ /**
* Removes the action's DOM nodes from all browser windows.
*
* PageActions will remember the action's urlbar placement, if any, after this
* method is called until app shutdown. If the action is not added again
* before shutdown, then PageActions will discard the placement, and the next
* time the action is added, its placement will be reset.
*/
remove() {
PageActions.onActionRemoved(this);
},
/**
+ * Returns whether the action should be shown in a given window's panel.
+ *
+ * @param browserWindow (DOM window, required)
+ * The window.
+ * @return True if the action should be shown and false otherwise. Actions
+ * are always shown in the panel unless they're both transient and
+ * disabled.
+ */
+ shouldShowInPanel(browserWindow) {
+ return !this.__transient || !this.getDisabled(browserWindow);
+ },
+
+ /**
* Returns whether the action should be shown in a given window's urlbar.
*
* @param browserWindow (DOM window, required)
* The window.
* @return True if the action should be shown and false otherwise. The action
* should be shown if it's both pinned and not disabled.
*/
shouldShowInUrlbar(browserWindow) {
@@ -1061,168 +1061,24 @@ Action.prototype = {
"webcompat-reporter-button",
].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
return builtInIDs.includes(this.id);
},
};
this.PageActions.Action = Action;
-
-/**
- * A Subview represents a PanelUI panelview that your actions can show.
- * `options` is a required object with the following properties.
- *
- * @param buttons (array, optional)
- * An array of buttons to show in the subview. Each item in the array
- * must be an options object suitable for passing to the Button
- * constructor. See the Button constructor for information on these
- * objects' properties.
- * @param onPlaced (function, optional)
- * Called when the subview is added to its parent panel in a browser
- * window:
- * onPlaced(panelViewNode)
- * * panelViewNode: The panelview node represented by this Subview.
- * @param onShowing (function, optional)
- * Called when the subview is showing in a browser window:
- * onShowing(panelViewNode)
- * * panelViewNode: The panelview node represented by this Subview.
- */
-function Subview(options) {
- setProperties(this, options, {
- buttons: false,
- onPlaced: false,
- onShowing: false,
- });
- this._buttons = (this._buttons || []).map(buttonOptions => {
- return new Button(buttonOptions);
- });
-}
-
-Subview.prototype = {
- /**
- * The subview's buttons (array of Button objects, nonnull)
- */
- get buttons() {
- return this._buttons;
- },
-
- /**
- * Call this when a DOM node for the subview is added to the DOM.
- *
- * @param panelViewNode (DOM node, required)
- * The subview's panelview node.
- */
- onPlaced(panelViewNode) {
- if (this._onPlaced) {
- this._onPlaced(panelViewNode);
- }
- },
-
- /**
- * Call this when a DOM node for the subview is showing.
- *
- * @param panelViewNode (DOM node, required)
- * The subview's panelview node.
- */
- onShowing(panelViewNode) {
- if (this._onShowing) {
- this._onShowing(panelViewNode);
- }
- }
-};
-
-this.PageActions.Subview = Subview;
-
-
-/**
- * A button that can be shown in a subview. `options` is a required object with
- * the following properties.
- *
- * @param id (string, required)
- * The button's ID. This will not become the ID of a DOM node by itself,
- * but it will be used to generate DOM node IDs. But in terms of spaces
- * and weird characters and such, do treat this like a DOM node ID.
- * @param title (string, required)
- * The button's title.
- * @param disabled (bool, required)
- * Pass true to disable the button.
- * @param onCommand (function, optional)
- * Called when the button is clicked:
- * onCommand(event, buttonNode)
- * * event: The triggering event.
- * * buttonNode: The node that was clicked.
- * @param shortcut (string, optional)
- * The button's shortcut text.
- */
-function Button(options) {
- setProperties(this, options, {
- id: true,
- title: true,
- disabled: false,
- onCommand: false,
- shortcut: false,
- });
-}
-
-Button.prototype = {
- /**
- * True if the button is disabled (bool, nonnull)
- */
- get disabled() {
- return this._disabled || false;
- },
-
- /**
- * The button's ID (string, nonnull)
- */
- get id() {
- return this._id;
- },
-
- /**
- * The button's shortcut (string, nullable)
- */
- get shortcut() {
- return this._shortcut;
- },
-
- /**
- * The button's title (string, nonnull)
- */
- get title() {
- return this._title;
- },
-
- /**
- * Call this when the user clicks the button.
- *
- * @param event (DOM event, required)
- * The triggering event.
- * @param buttonNode (DOM node, required)
- * The button's DOM node that was clicked.
- */
- onCommand(event, buttonNode) {
- if (this._onCommand) {
- this._onCommand(event, buttonNode);
- }
- }
-};
-
-this.PageActions.Button = Button;
-
+this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
+this.PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
// These are only necessary so that Pocket and the test can use them.
this.PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK;
this.PageActions.ACTION_ID_BOOKMARK_SEPARATOR = ACTION_ID_BOOKMARK_SEPARATOR;
this.PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS;
-// This is only necessary so that the test can access it.
-this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
-
// Sorted in the order in which they should appear in the page action panel.
// Does not include the page actions of extensions bundled with the browser.
// They're added by the relevant extension code.
// NOTE: If you add items to this list (or system add-on actions that we
// want to keep track of), make sure to also update Histograms.json for the
// new actions.
var gBuiltInActions = [
@@ -1270,47 +1126,39 @@ var gBuiltInActions = [
id: "emailLink",
title: "emailLink-title",
onPlacedInPanel(buttonNode) {
browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
},
onCommand(event, buttonNode) {
browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
},
- }
+ },
];
if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
gBuiltInActions.push(
// send to device
{
id: "sendToDevice",
title: "sendToDevice-title",
onPlacedInPanel(buttonNode) {
browserPageActions(buttonNode).sendToDevice.onPlacedInPanel(buttonNode);
},
onLocationChange(browserWindow) {
browserPageActions(browserWindow).sendToDevice.onLocationChange();
},
- subview: {
- buttons: [
- {
- id: "notReady",
- title: "sendToDevice-notReadyTitle",
- disabled: true,
- },
- ],
- onPlaced(panelViewNode) {
- browserPageActions(panelViewNode).sendToDevice
- .onSubviewPlaced(panelViewNode);
- },
- onShowing(panelViewNode) {
- browserPageActions(panelViewNode).sendToDevice
- .onShowingSubview(panelViewNode);
- },
+ wantsSubview: true,
+ onSubviewPlaced(panelViewNode) {
+ browserPageActions(panelViewNode).sendToDevice
+ .onSubviewPlaced(panelViewNode);
+ },
+ onSubviewShowing(panelViewNode) {
+ browserPageActions(panelViewNode).sendToDevice
+ .onShowingSubview(panelViewNode);
},
});
}
/**
* Gets a BrowserPageActions object in a browser window.
*
--- a/browser/modules/test/browser/browser_PageActions.js
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -37,17 +37,25 @@ add_task(async function simple() {
let onPlacedInPanelCallCount = 0;
let onPlacedInUrlbarCallCount = 0;
let onShowingInPanelCallCount = 0;
let onCommandExpectedButtonID;
let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id);
let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id);
+
+ // Open the panel so that actions are added to it, and then close it.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
let initialActions = PageActions.actions;
+ let initialActionsInPanel = PageActions.actionsInPanel(window);
+ let initialActionsInUrlbar = PageActions.actionsInUrlbar(window);
let action = PageActions.addAction(new PageActions.Action({
iconURL,
id,
nodeAttributes,
title,
tooltip,
onCommand(event, buttonNode) {
@@ -72,86 +80,139 @@ add_task(async function simple() {
Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
},
}));
Assert.equal(action.getIconURL(), iconURL, "iconURL");
Assert.equal(action.id, id, "id");
Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
Assert.equal(action.pinnedToUrlbar, false, "pinnedToUrlbar");
- Assert.equal(action.subview, null, "subview");
Assert.equal(action.getDisabled(), false, "disabled");
Assert.equal(action.getDisabled(window), false, "disabled in window");
Assert.equal(action.getTitle(), title, "title");
Assert.equal(action.getTitle(window), title, "title in window");
Assert.equal(action.getTooltip(), tooltip, "tooltip");
Assert.equal(action.getTooltip(window), tooltip, "tooltip in window");
+ Assert.equal(action.getWantsSubview(), false, "subview");
+ Assert.equal(action.getWantsSubview(window), false, "subview in window");
Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride");
Assert.equal(action.wantsIframe, false, "wantsIframe");
Assert.ok(!("__insertBeforeActionID" in action), "__insertBeforeActionID");
Assert.ok(!("__isSeparator" in action), "__isSeparator");
Assert.ok(!("__urlbarNodeInMarkup" in action), "__urlbarNodeInMarkup");
+ Assert.ok(!("__transient" in action), "__transient");
- Assert.equal(onPlacedInPanelCallCount, 1,
- "onPlacedInPanelCallCount should be inc'ed");
+ // The action shouldn't be placed in the panel until it opens for the first
+ // time.
+ Assert.equal(onPlacedInPanelCallCount, 0,
+ "onPlacedInPanelCallCount should remain 0");
Assert.equal(onPlacedInUrlbarCallCount, 0,
"onPlacedInUrlbarCallCount should remain 0");
Assert.equal(onShowingInPanelCallCount, 0,
"onShowingInPanelCallCount should remain 0");
- // The separator between the built-in and non-built-in actions should have
- // been created and included in PageActions.actions, which is why the new
- // count should be the initial count + 2, not + 1.
- Assert.equal(PageActions.actions.length, initialActions.length + 2,
- "PageActions.actions.length should be updated");
- Assert.deepEqual(PageActions.actions[PageActions.actions.length - 1], action,
- "Last page action should be action");
- Assert.equal(PageActions.actions[PageActions.actions.length - 2].id,
- PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
- "2nd-to-last page action should be separator");
+ // Open the panel so that actions are added to it, and then close it.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ Assert.equal(onPlacedInPanelCallCount, 1,
+ "onPlacedInPanelCallCount should be inc'ed");
+ Assert.equal(onShowingInPanelCallCount, 1,
+ "onShowingInPanelCallCount should be inc'ed");
+
+ // Build an array of the expected actions in the panel and compare it to the
+ // actual actions. Don't assume that there are or aren't already other non-
+ // built-in actions.
+ let sepIndex =
+ initialActionsInPanel
+ .findIndex(a => a.id == PageActions.ACTION_ID_BUILT_IN_SEPARATOR);
+ let initialSepIndex = sepIndex;
+ let indexInPanel;
+ if (sepIndex < 0) {
+ // No prior non-built-in actions.
+ indexInPanel = initialActionsInPanel.length;
+ } else {
+ // Prior non-built-in actions. Find the index where the action goes.
+ for (indexInPanel = sepIndex + 1;
+ indexInPanel < initialActionsInPanel.length;
+ indexInPanel++) {
+ let a = initialActionsInPanel[indexInPanel];
+ if (a.getTitle().localeCompare(action.getTitle()) < 1) {
+ break;
+ }
+ }
+ }
+ let expectedActionsInPanel = initialActionsInPanel.slice();
+ expectedActionsInPanel.splice(indexInPanel, 0, action);
+ // The separator between the built-ins and non-built-ins should be present
+ // if it's not already.
+ if (sepIndex < 0) {
+ expectedActionsInPanel.splice(indexInPanel, 0, new PageActions.Action({
+ id: PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ _isSeparator: true,
+ }));
+ sepIndex = indexInPanel;
+ indexInPanel++;
+ }
+ Assert.deepEqual(PageActions.actionsInPanel(window),
+ expectedActionsInPanel,
+ "Actions in panel after adding the action");
+
+ // The actions in the urlbar should be the same since the test action isn't
+ // shown there.
+ Assert.deepEqual(PageActions.actionsInUrlbar(window),
+ initialActionsInUrlbar,
+ "Actions in urlbar after adding the action");
+
+ // Check the list of all actions.
+ Assert.deepEqual(PageActions.actions,
+ initialActions.concat([action]),
+ "All actions after adding the action");
Assert.deepEqual(PageActions.actionForID(action.id), action,
"actionForID should be action");
Assert.ok(PageActions._persistedActions.ids.includes(action.id),
"PageActions should record action in its list of seen actions");
// The action's panel button should have been created.
- let panelButtonNode = document.getElementById(panelButtonID);
+ let panelButtonNode =
+ BrowserPageActions.mainViewBodyNode.childNodes[indexInPanel];
Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+ Assert.equal(panelButtonNode.id, panelButtonID, "panelButtonID");
Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(),
"label");
for (let name in action.nodeAttributes) {
Assert.ok(panelButtonNode.hasAttribute(name), "Has attribute: " + name);
Assert.equal(panelButtonNode.getAttribute(name),
action.nodeAttributes[name],
"Equal attribute: " + name);
}
- // The panel button should be the last node in the panel, and its previous
- // sibling should be the separator between the built-in actions and non-built-
- // in actions.
- Assert.equal(panelButtonNode.nextSibling, null, "nextSibling");
- Assert.notEqual(panelButtonNode.previousSibling, null, "previousSibling");
+ // The separator between the built-ins and non-built-ins should exist.
+ let sepNode =
+ BrowserPageActions.mainViewBodyNode.childNodes[sepIndex];
+ Assert.notEqual(sepNode, null, "sepNode");
Assert.equal(
- panelButtonNode.previousSibling.id,
+ sepNode.id,
BrowserPageActions.panelButtonNodeIDForActionID(
PageActions.ACTION_ID_BUILT_IN_SEPARATOR
),
- "previousSibling.id"
+ "sepNode.id"
);
// The action's urlbar button should not have been created.
let urlbarButtonNode = document.getElementById(urlbarButtonID);
Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
// Open the panel, click the action's button.
- await promisePageActionPanelOpen();
- Assert.equal(onShowingInPanelCallCount, 1,
+ await promiseOpenPageActionPanel();
+ Assert.equal(onShowingInPanelCallCount, 2,
"onShowingInPanelCallCount should be inc'ed");
onCommandExpectedButtonID = panelButtonID;
EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
await promisePageActionPanelHidden();
Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
// Show the action's button in the urlbar.
action.pinnedToUrlbar = true;
@@ -228,28 +289,37 @@ add_task(async function simple() {
// Remove the action.
action.remove();
panelButtonNode = document.getElementById(panelButtonID);
Assert.equal(panelButtonNode, null, "panelButtonNode");
urlbarButtonNode = document.getElementById(urlbarButtonID);
Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
- // The separator between the built-in actions and non-built-in actions should
- // be gone now, too.
let separatorNode = document.getElementById(
BrowserPageActions.panelButtonNodeIDForActionID(
PageActions.ACTION_ID_BUILT_IN_SEPARATOR
)
);
- Assert.equal(separatorNode, null, "No separator");
- Assert.ok(!BrowserPageActions.mainViewBodyNode
- .lastChild.localName.includes("separator"),
- "Last child should not be separator");
+ if (initialSepIndex < 0) {
+ // The separator between the built-in actions and non-built-in actions
+ // should be gone now, too.
+ Assert.equal(separatorNode, null, "No separator");
+ Assert.ok(!BrowserPageActions.mainViewBodyNode
+ .lastChild.localName.includes("separator"),
+ "Last child should not be separator");
+ } else {
+ // The separator should still be present.
+ Assert.notEqual(separatorNode, null, "Separator should still exist");
+ }
+ Assert.deepEqual(PageActions.actionsInPanel(window), initialActionsInPanel,
+ "Actions in panel should go back to initial");
+ Assert.deepEqual(PageActions.actionsInUrlbar(window), initialActionsInUrlbar,
+ "Actions in urlbar should go back to initial");
Assert.deepEqual(PageActions.actions, initialActions,
"Actions should go back to initial");
Assert.equal(PageActions.actionForID(action.id), null,
"actionForID should be null");
Assert.ok(PageActions._persistedActions.ids.includes(action.id),
"Action ID should remain in cache until purged");
PageActions._purgeUnregisteredPersistedActions();
@@ -257,188 +327,132 @@ add_task(async function simple() {
"Action ID should be removed from cache after being purged");
});
// Tests a non-built-in action with a subview.
add_task(async function withSubview() {
let id = "test-subview";
- let onActionCommandCallCount = 0;
let onActionPlacedInPanelCallCount = 0;
let onActionPlacedInUrlbarCallCount = 0;
let onSubviewPlacedCount = 0;
let onSubviewShowingCount = 0;
- let onButtonCommandCallCount = 0;
let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id);
let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id);
let panelViewIDPanel =
BrowserPageActions._panelViewNodeIDForActionID(id, false);
let panelViewIDUrlbar =
BrowserPageActions._panelViewNodeIDForActionID(id, true);
let onSubviewPlacedExpectedPanelViewID = panelViewIDPanel;
let onSubviewShowingExpectedPanelViewID;
- let onButtonCommandExpectedButtonID;
-
- let subview = {
- buttons: [0, 1, 2].map(index => {
- return {
- id: "test-subview-button-" + index,
- title: "Test subview Button " + index,
- };
- }),
- onPlaced(panelViewNode) {
- onSubviewPlacedCount++;
- Assert.ok(panelViewNode,
- "panelViewNode should be non-null: " + panelViewNode);
- Assert.equal(panelViewNode.id, onSubviewPlacedExpectedPanelViewID,
- "panelViewNode.id");
- },
- onShowing(panelViewNode) {
- onSubviewShowingCount++;
- Assert.ok(panelViewNode,
- "panelViewNode should be non-null: " + panelViewNode);
- Assert.equal(panelViewNode.id, onSubviewShowingExpectedPanelViewID,
- "panelViewNode.id");
- },
- };
- subview.buttons[0].onCommand = (event, buttonNode) => {
- onButtonCommandCallCount++;
- Assert.ok(event, "event should be non-null: " + event);
- Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
- Assert.equal(buttonNode.id, onButtonCommandExpectedButtonID,
- "buttonNode.id");
- for (let node = buttonNode.parentNode; node; node = node.parentNode) {
- if (node.localName == "panel") {
- node.hidePopup();
- break;
- }
- }
- };
let action = PageActions.addAction(new PageActions.Action({
iconURL: "chrome://browser/skin/mail.svg",
id,
pinnedToUrlbar: true,
- subview,
title: "Test subview",
- onCommand(event, buttonNode) {
- onActionCommandCallCount++;
- },
+ wantsSubview: true,
onPlacedInPanel(buttonNode) {
onActionPlacedInPanelCallCount++;
Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
},
onPlacedInUrlbar(buttonNode) {
onActionPlacedInUrlbarCallCount++;
Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
},
+ onSubviewPlaced(panelViewNode) {
+ onSubviewPlacedCount++;
+ Assert.ok(panelViewNode,
+ "panelViewNode should be non-null: " + panelViewNode);
+ Assert.equal(panelViewNode.id, onSubviewPlacedExpectedPanelViewID,
+ "panelViewNode.id");
+ },
+ onSubviewShowing(panelViewNode) {
+ onSubviewShowingCount++;
+ Assert.ok(panelViewNode,
+ "panelViewNode should be non-null: " + panelViewNode);
+ Assert.equal(panelViewNode.id, onSubviewShowingExpectedPanelViewID,
+ "panelViewNode.id");
+ },
}));
- let panelViewButtonIDPanel =
- BrowserPageActions._panelViewButtonNodeIDForActionID(
- id, subview.buttons[0].id, false
- );
- let panelViewButtonIDUrlbar =
- BrowserPageActions._panelViewButtonNodeIDForActionID(
- id, subview.buttons[0].id, true
- );
+ Assert.equal(action.id, id, "id");
+ Assert.equal(action.getWantsSubview(), true, "subview");
+ Assert.equal(action.getWantsSubview(window), true, "subview in window");
- Assert.equal(action.id, id, "id");
- Assert.notEqual(action.subview, null, "subview");
- Assert.notEqual(action.subview.buttons, null, "subview.buttons");
- Assert.equal(action.subview.buttons.length, subview.buttons.length,
- "subview.buttons.length");
- for (let i = 0; i < subview.buttons.length; i++) {
- Assert.equal(action.subview.buttons[i].id, subview.buttons[i].id,
- "subview button id for index: " + i);
- Assert.equal(action.subview.buttons[i].title, subview.buttons[i].title,
- "subview button title for index: " + i);
- }
+ // The action shouldn't be placed in the panel until it opens for the first
+ // time.
+ Assert.equal(onActionPlacedInPanelCallCount, 0,
+ "onActionPlacedInPanelCallCount should be 0");
+ Assert.equal(onSubviewPlacedCount, 0,
+ "onSubviewPlacedCount should be 0");
+
+ // But it should be placed in the urlbar.
+ Assert.equal(onActionPlacedInUrlbarCallCount, 1,
+ "onActionPlacedInUrlbarCallCount should be 0");
+
+ // Open the panel, which should place the action in it.
+ await promiseOpenPageActionPanel();
Assert.equal(onActionPlacedInPanelCallCount, 1,
"onActionPlacedInPanelCallCount should be inc'ed");
- Assert.equal(onActionPlacedInUrlbarCallCount, 1,
- "onActionPlacedInUrlbarCallCount should be inc'ed");
Assert.equal(onSubviewPlacedCount, 1,
"onSubviewPlacedCount should be inc'ed");
Assert.equal(onSubviewShowingCount, 0,
"onSubviewShowingCount should remain 0");
// The action's panel button and view (in the main page action panel) should
// have been created.
let panelButtonNode = document.getElementById(panelButtonID);
Assert.notEqual(panelButtonNode, null, "panelButtonNode");
- let panelViewButtonNodePanel =
- document.getElementById(panelViewButtonIDPanel);
- Assert.notEqual(panelViewButtonNodePanel, null, "panelViewButtonNodePanel");
// The action's urlbar button should have been created.
let urlbarButtonNode = document.getElementById(urlbarButtonID);
Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
// The button should have been inserted before the bookmark star.
Assert.notEqual(urlbarButtonNode.nextSibling, null, "Should be a next node");
Assert.equal(
urlbarButtonNode.nextSibling.id,
PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK).urlbarIDOverride,
"Next node should be the bookmark star"
);
- // Open the panel, click the action's button, click the subview's first
- // button.
- await promisePageActionPanelOpen();
+ // Click the action's button in the panel. The subview should be shown.
Assert.equal(onSubviewShowingCount, 0,
"onSubviewShowingCount should remain 0");
let subviewShownPromise = promisePageActionViewShown();
onSubviewShowingExpectedPanelViewID = panelViewIDPanel;
-
- // synthesizeMouse often cannot seem to click the right node when used on
- // buttons that show subviews and buttons inside subviews. That's why we're
- // using node.click() twice here: the first time to show the subview, the
- // second time to click a button in the subview.
-// EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
panelButtonNode.click();
await subviewShownPromise;
- Assert.equal(onActionCommandCallCount, 0,
- "onActionCommandCallCount should remain 0");
- Assert.equal(onSubviewShowingCount, 1,
- "onSubviewShowingCount should be inc'ed");
- onButtonCommandExpectedButtonID = panelViewButtonIDPanel;
-// EventUtils.synthesizeMouseAtCenter(panelViewButtonNodePanel, {});
- panelViewButtonNodePanel.click();
+
+ // Click the main button to hide the main panel.
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
- Assert.equal(onActionCommandCallCount, 0,
- "onActionCommandCallCount should remain 0");
- Assert.equal(onButtonCommandCallCount, 1,
- "onButtonCommandCallCount should be inc'ed");
// Click the action's urlbar button, which should open the activated-action
- // panel showing the subview, and click the subview's first button.
+ // panel showing the subview.
onSubviewPlacedExpectedPanelViewID = panelViewIDUrlbar;
onSubviewShowingExpectedPanelViewID = panelViewIDUrlbar;
EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
await promisePanelShown(BrowserPageActions._activatedActionPanelID);
Assert.equal(onSubviewPlacedCount, 2,
"onSubviewPlacedCount should be inc'ed");
Assert.equal(onSubviewShowingCount, 2,
"onSubviewShowingCount should be inc'ed");
- let panelViewButtonNodeUrlbar =
- document.getElementById(panelViewButtonIDUrlbar);
- Assert.notEqual(panelViewButtonNodeUrlbar, null, "panelViewButtonNodeUrlbar");
- onButtonCommandExpectedButtonID = panelViewButtonIDUrlbar;
- EventUtils.synthesizeMouseAtCenter(panelViewButtonNodeUrlbar, {});
+
+ // Click the urlbar button again. The activated-action panel should close.
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
assertActivatedPageActionPanelHidden();
- Assert.equal(onButtonCommandCallCount, 2,
- "onButtonCommandCallCount should be inc'ed");
// Remove the action.
action.remove();
panelButtonNode = document.getElementById(panelButtonID);
Assert.equal(panelButtonNode, null, "panelButtonNode");
urlbarButtonNode = document.getElementById(urlbarButtonID);
Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
let panelViewNodePanel = document.getElementById(panelViewIDPanel);
@@ -487,16 +501,20 @@ add_task(async function withIframe() {
Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
},
}));
Assert.equal(action.id, id, "id");
Assert.equal(action.wantsIframe, true, "wantsIframe");
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
Assert.equal(onPlacedInPanelCallCount, 1,
"onPlacedInPanelCallCount should be inc'ed");
Assert.equal(onPlacedInUrlbarCallCount, 1,
"onPlacedInUrlbarCallCount should be inc'ed");
Assert.equal(onIframeShowingCount, 0,
"onIframeShowingCount should remain 0");
Assert.equal(onCommandCallCount, 0,
"onCommandCallCount should remain 0");
@@ -513,56 +531,56 @@ add_task(async function withIframe() {
Assert.notEqual(urlbarButtonNode.nextSibling, null, "Should be a next node");
Assert.equal(
urlbarButtonNode.nextSibling.id,
PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK).urlbarIDOverride,
"Next node should be the bookmark star"
);
// Open the panel, click the action's button.
- await promisePageActionPanelOpen();
+ await promiseOpenPageActionPanel();
Assert.equal(onIframeShowingCount, 0, "onIframeShowingCount should remain 0");
EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
await promisePanelShown(BrowserPageActions._activatedActionPanelID);
- Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
Assert.equal(onIframeShowingCount, 1, "onIframeShowingCount should be inc'ed");
// The activated-action panel should have opened, anchored to the action's
// urlbar button.
let aaPanel =
document.getElementById(BrowserPageActions._activatedActionPanelID);
Assert.notEqual(aaPanel, null, "activated-action panel");
Assert.equal(aaPanel.anchorNode.id, urlbarButtonID, "aaPanel.anchorNode.id");
EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
assertActivatedPageActionPanelHidden();
// Click the action's urlbar button.
EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
await promisePanelShown(BrowserPageActions._activatedActionPanelID);
- Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed");
Assert.equal(onIframeShowingCount, 2, "onIframeShowingCount should be inc'ed");
// The activated-action panel should have opened, again anchored to the
// action's urlbar button.
aaPanel = document.getElementById(BrowserPageActions._activatedActionPanelID);
Assert.notEqual(aaPanel, null, "aaPanel");
Assert.equal(aaPanel.anchorNode.id, urlbarButtonID, "aaPanel.anchorNode.id");
EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
assertActivatedPageActionPanelHidden();
// Hide the action's button in the urlbar.
action.pinnedToUrlbar = false;
urlbarButtonNode = document.getElementById(urlbarButtonID);
Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
// Open the panel, click the action's button.
- await promisePageActionPanelOpen();
+ await promiseOpenPageActionPanel();
EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
await promisePanelShown(BrowserPageActions._activatedActionPanelID);
- Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onCommandCallCount, 3, "onCommandCallCount should be inc'ed");
Assert.equal(onIframeShowingCount, 3, "onIframeShowingCount should be inc'ed");
// The activated-action panel should have opened, this time anchored to the
// main page action button in the urlbar.
aaPanel = document.getElementById(BrowserPageActions._activatedActionPanelID);
Assert.notEqual(aaPanel, null, "aaPanel");
Assert.equal(aaPanel.anchorNode.id, BrowserPageActions.mainButtonNode.id,
"aaPanel.anchorNode.id");
@@ -578,49 +596,54 @@ add_task(async function withIframe() {
});
// Tests an action with the _insertBeforeActionID option set.
add_task(async function insertBeforeActionID() {
let id = "test-insertBeforeActionID";
let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id);
- let initialActions = PageActions.actions;
- let initialBuiltInActions = PageActions.builtInActions;
- let initialNonBuiltInActions = PageActions.nonBuiltInActions;
- let initialBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+ let initialActions = PageActions.actionsInPanel(window);
+ let initialBuiltInActions = PageActions._builtInActions.slice();
+ let initialNonBuiltInActions = PageActions._nonBuiltInActions.slice();
+ let initialBookmarkSeparatorIndex = initialActions.findIndex(a => {
return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
});
let action = PageActions.addAction(new PageActions.Action({
id,
title: "Test insertBeforeActionID",
_insertBeforeActionID: PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
}));
Assert.equal(action.id, id, "id");
Assert.ok("__insertBeforeActionID" in action, "__insertBeforeActionID");
Assert.equal(action.__insertBeforeActionID,
PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
"action.__insertBeforeActionID");
- Assert.equal(PageActions.actions.length,
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ let newActions = PageActions.actionsInPanel(window);
+ Assert.equal(newActions.length,
initialActions.length + 1,
"PageActions.actions.length should be updated");
- Assert.equal(PageActions.builtInActions.length,
+ Assert.equal(PageActions._builtInActions.length,
initialBuiltInActions.length + 1,
- "PageActions.builtInActions.length should be updated");
- Assert.equal(PageActions.nonBuiltInActions.length,
+ "PageActions._builtInActions.length should be updated");
+ Assert.equal(PageActions._nonBuiltInActions.length,
initialNonBuiltInActions.length,
- "PageActions.nonBuiltInActions.length should be updated");
+ "PageActions._nonBuiltInActions.length should remain the same");
- let actionIndex = PageActions.actions.findIndex(a => a.id == id);
+ let actionIndex = newActions.findIndex(a => a.id == id);
Assert.equal(initialBookmarkSeparatorIndex, actionIndex,
"initialBookmarkSeparatorIndex");
- let newBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+ let newBookmarkSeparatorIndex = newActions.findIndex(a => {
return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
});
Assert.equal(newBookmarkSeparatorIndex, initialBookmarkSeparatorIndex + 1,
"newBookmarkSeparatorIndex");
// The action's panel button should have been created.
let panelButtonNode = document.getElementById(panelButtonID);
Assert.notEqual(panelButtonNode, null, "panelButtonNode");
@@ -647,54 +670,59 @@ add_task(async function insertBeforeActi
null,
"Separator should be gone"
);
action.remove();
});
-// Tests that the ordering of multiple non-built-in actions is alphabetical.
+// Tests that the ordering in the panel of multiple non-built-in actions is
+// alphabetical.
add_task(async function multipleNonBuiltInOrdering() {
let idPrefix = "test-multipleNonBuiltInOrdering-";
let titlePrefix = "Test multipleNonBuiltInOrdering ";
- let initialActions = PageActions.actions;
- let initialBuiltInActions = PageActions.builtInActions;
- let initialNonBuiltInActions = PageActions.nonBuiltInActions;
+ let initialActions = PageActions.actionsInPanel(window);
+ let initialBuiltInActions = PageActions._builtInActions.slice();
+ let initialNonBuiltInActions = PageActions._nonBuiltInActions.slice();
// Create some actions in an out-of-order order.
let actions = [2, 1, 4, 3].map(index => {
return PageActions.addAction(new PageActions.Action({
id: idPrefix + index,
title: titlePrefix + index,
}));
});
// + 1 for the separator between built-in and non-built-in actions.
- Assert.equal(PageActions.actions.length,
+ Assert.equal(PageActions.actionsInPanel(window).length,
initialActions.length + actions.length + 1,
- "PageActions.actions.length should be updated");
+ "PageActions.actionsInPanel().length should be updated");
- Assert.equal(PageActions.builtInActions.length,
+ Assert.equal(PageActions._builtInActions.length,
initialBuiltInActions.length,
- "PageActions.builtInActions.length should be same");
- Assert.equal(PageActions.nonBuiltInActions.length,
+ "PageActions._builtInActions.length should be same");
+ Assert.equal(PageActions._nonBuiltInActions.length,
initialNonBuiltInActions.length + actions.length,
- "PageActions.nonBuiltInActions.length should be updated");
+ "PageActions._nonBuiltInActions.length should be updated");
// Look at the final actions.length actions in PageActions.actions, from first
// to last.
for (let i = 0; i < actions.length; i++) {
let expectedIndex = i + 1;
- let actualAction = PageActions.nonBuiltInActions[i];
+ let actualAction = PageActions._nonBuiltInActions[i];
Assert.equal(actualAction.id, idPrefix + expectedIndex,
"actualAction.id for index: " + i);
}
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
// Check the button nodes in the panel.
let expectedIndex = 1;
let buttonNode = document.getElementById(
BrowserPageActions.panelButtonNodeIDForActionID(idPrefix + expectedIndex)
);
Assert.notEqual(buttonNode, null, "buttonNode");
Assert.notEqual(buttonNode.previousSibling, null,
"buttonNode.previousSibling");
@@ -734,114 +762,136 @@ add_task(async function multipleNonBuilt
});
// Makes sure the panel is correctly updated when a non-built-in action is
// added before the built-in actions; and when all built-in actions are removed
// and added back.
add_task(async function nonBuiltFirst() {
let initialActions = PageActions.actions;
+ let initialActionsInPanel = PageActions.actionsInPanel(window);
// Remove all actions.
for (let action of initialActions) {
action.remove();
}
// Check the actions.
Assert.deepEqual(PageActions.actions.map(a => a.id), [],
"PageActions.actions should be empty");
- Assert.deepEqual(PageActions.builtInActions.map(a => a.id), [],
- "PageActions.builtInActions should be empty");
- Assert.deepEqual(PageActions.nonBuiltInActions.map(a => a.id), [],
- "PageActions.nonBuiltInActions should be empty");
+ Assert.deepEqual(PageActions._builtInActions.map(a => a.id), [],
+ "PageActions._builtInActions should be empty");
+ Assert.deepEqual(PageActions._nonBuiltInActions.map(a => a.id), [],
+ "PageActions._nonBuiltInActions should be empty");
// Check the panel.
Assert.equal(BrowserPageActions.mainViewBodyNode.childNodes.length, 0,
"All nodes should be gone");
// Add a non-built-in action.
let action = PageActions.addAction(new PageActions.Action({
id: "test-nonBuiltFirst",
title: "Test nonBuiltFirst",
}));
// Check the actions.
Assert.deepEqual(PageActions.actions.map(a => a.id), [action.id],
"Action should be in PageActions.actions");
- Assert.deepEqual(PageActions.builtInActions.map(a => a.id), [],
- "PageActions.builtInActions should be empty");
- Assert.deepEqual(PageActions.nonBuiltInActions.map(a => a.id), [action.id],
- "Action should be in PageActions.nonBuiltInActions");
+ Assert.deepEqual(PageActions._builtInActions.map(a => a.id), [],
+ "PageActions._builtInActions should be empty");
+ Assert.deepEqual(PageActions._nonBuiltInActions.map(a => a.id), [action.id],
+ "Action should be in PageActions._nonBuiltInActions");
// Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
Assert.deepEqual(
Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
[BrowserPageActions.panelButtonNodeIDForActionID(action.id)],
"Action should be in panel"
);
// Now add back all the actions.
for (let a of initialActions) {
PageActions.addAction(a);
}
// Check the actions.
Assert.deepEqual(
PageActions.actions.map(a => a.id),
initialActions.map(a => a.id).concat(
- [PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
[action.id]
),
"All actions should be in PageActions.actions"
);
Assert.deepEqual(
- PageActions.builtInActions.map(a => a.id),
+ PageActions._builtInActions.map(a => a.id),
initialActions.map(a => a.id),
- "PageActions.builtInActions should be initial actions"
+ "PageActions._builtInActions should be initial actions"
);
Assert.deepEqual(
- PageActions.nonBuiltInActions.map(a => a.id),
+ PageActions._nonBuiltInActions.map(a => a.id),
[action.id],
- "PageActions.nonBuiltInActions should contain action"
+ "PageActions._nonBuiltInActions should contain action"
);
// Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat(
+ [PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
+ [action.id]
+ ),
+ "All actions should be in PageActions.actionsInPanel()"
+ );
Assert.deepEqual(
Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
- initialActions.map(a => a.id).concat(
+ initialActionsInPanel.map(a => a.id).concat(
[PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
[action.id]
).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
"Panel should contain all actions"
);
// Remove the test action.
action.remove();
// Check the actions.
Assert.deepEqual(
PageActions.actions.map(a => a.id),
initialActions.map(a => a.id),
"Action should no longer be in PageActions.actions"
);
Assert.deepEqual(
- PageActions.builtInActions.map(a => a.id),
+ PageActions._builtInActions.map(a => a.id),
initialActions.map(a => a.id),
- "PageActions.builtInActions should be initial actions"
+ "PageActions._builtInActions should be initial actions"
);
Assert.deepEqual(
- PageActions.nonBuiltInActions.map(a => a.id),
+ PageActions._nonBuiltInActions.map(a => a.id),
[],
- "Action should no longer be in PageActions.nonBuiltInActions"
+ "Action should no longer be in PageActions._nonBuiltInActions"
);
// Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id),
+ "Action should no longer be in PageActions.actionsInPanel()"
+ );
Assert.deepEqual(
Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
- initialActions.map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
+ initialActionsInPanel.map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
"Action should no longer be in panel"
);
});
// Makes sure that urlbar nodes appear in the correct order in a new window.
add_task(async function urlbarOrderNewWindow() {
// Make some new actions.
@@ -1035,16 +1085,17 @@ add_task(async function perWindowState()
"Title: old window");
Assert.equal(action.getTitle(newWindow), newGlobalTitle,
"Title: new window");
// The action's panel button nodes should be updated in both windows.
let panelButtonID =
BrowserPageActions.panelButtonNodeIDForActionID(action.id);
for (let win of [window, newWindow]) {
+ win.BrowserPageActions.placeLazyActionsInPanel();
let panelButtonNode = win.document.getElementById(panelButtonID);
Assert.equal(panelButtonNode.getAttribute("label"), newGlobalTitle,
"Panel button label should be global title");
}
// Set a new title in the new window.
let newPerWinTitle = title + " new title in new window";
action.setTitle(newPerWinTitle, newWindow);
@@ -1228,17 +1279,17 @@ add_task(async function contextMenu() {
// Add a test action.
let action = PageActions.addAction(new PageActions.Action({
id: "test-contextMenu",
title: "Test contextMenu",
pinnedToUrlbar: true,
}));
// Open the panel and then open the context menu on the action's item.
- await promisePageActionPanelOpen();
+ await promiseOpenPageActionPanel();
let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
let contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
@@ -1344,17 +1395,17 @@ add_task(async function contextMenu() {
await contextMenuPromise;
// The action should be removed from the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be removed");
// Open the panel and then open the context menu on the action's item.
- await promisePageActionPanelOpen();
+ await promiseOpenPageActionPanel();
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "show" item and the "manage" item. Click
@@ -1411,21 +1462,226 @@ add_task(async function contextMenu() {
// urlbar tests that run after this one can break if the mouse is left over
// the area where the urlbar popup appears, which seems to happen due to the
// above synthesized mouse events. Move it over the urlbar.
EventUtils.synthesizeMouseAtCenter(gURLBar, { type: "mousemove" });
gURLBar.focus();
});
+// Tests transient actions.
+add_task(async function transient() {
+ let initialActionsInPanel = PageActions.actionsInPanel(window);
+
+ let onPlacedInPanelCount = 0;
+ let onBeforePlacedInWindowCount = 0;
+
+ let action = PageActions.addAction(new PageActions.Action({
+ id: "test-transient",
+ title: "Test transient",
+ _transient: true,
+ onPlacedInPanel(buttonNode) {
+ onPlacedInPanelCount++;
+ },
+ onBeforePlacedInWindow(win) {
+ onBeforePlacedInWindowCount++;
+ },
+ }));
+
+ Assert.equal(action.__transient, true, "__transient");
+
+ Assert.equal(onPlacedInPanelCount, 0,
+ "onPlacedInPanelCount should remain 0");
+ Assert.equal(onBeforePlacedInWindowCount, 0,
+ "onBeforePlacedInWindowCount should remain 0");
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]),
+ "PageActions.actionsInPanel() should be updated"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+ "Actions in panel should be correct"
+ );
+
+ Assert.equal(onPlacedInPanelCount, 1,
+ "onPlacedInPanelCount should be inc'ed");
+ Assert.equal(onBeforePlacedInWindowCount, 1,
+ "onBeforePlacedInWindowCount should be inc'ed");
+
+ // Disable the action. It should be removed from the panel.
+ action.setDisabled(true, window);
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id),
+ "PageActions.actionsInPanel() should revert to initial"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel
+ .map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
+ "Actions in panel should be correct"
+ );
+
+ // Enable the action. It should be added back to the panel.
+ action.setDisabled(false, window);
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]),
+ "PageActions.actionsInPanel() should be updated"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+ "Actions in panel should be correct"
+ );
+
+ Assert.equal(onPlacedInPanelCount, 2,
+ "onPlacedInPanelCount should be inc'ed");
+ Assert.equal(onBeforePlacedInWindowCount, 2,
+ "onBeforePlacedInWindowCount should be inc'ed");
+
+ // Add another non-built in but non-transient action.
+ let otherAction = PageActions.addAction(new PageActions.Action({
+ id: "test-transient2",
+ title: "Test transient 2",
+ }));
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]),
+ "PageActions.actionsInPanel() should be updated"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+ "Actions in panel should be correct"
+ );
+
+ Assert.equal(onPlacedInPanelCount, 2,
+ "onPlacedInPanelCount should remain the same");
+ Assert.equal(onBeforePlacedInWindowCount, 2,
+ "onBeforePlacedInWindowCount should remain the same");
+
+ // Disable the action again. It should be removed from the panel.
+ action.setDisabled(true, window);
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ ]),
+ "PageActions.actionsInPanel() should be updated"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+ "Actions in panel should be correct"
+ );
+
+ // Enable the action again. It should be added back to the panel.
+ action.setDisabled(false, window);
+
+ Assert.deepEqual(
+ PageActions.actionsInPanel(window).map(a => a.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]),
+ "PageActions.actionsInPanel() should be updated"
+ );
+
+ // Check the panel.
+ await promiseOpenPageActionPanel();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.deepEqual(
+ Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+ initialActionsInPanel.map(a => a.id).concat([
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ otherAction.id,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ action.id,
+ ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+ "Actions in panel should be correct"
+ );
+
+ Assert.equal(onPlacedInPanelCount, 3,
+ "onPlacedInPanelCount should be inc'ed");
+ Assert.equal(onBeforePlacedInWindowCount, 3,
+ "onBeforePlacedInWindowCount should be inc'ed");
+
+ // Done, clean up.
+ action.remove();
+ otherAction.remove();
+});
+
+
function assertActivatedPageActionPanelHidden() {
Assert.ok(!document.getElementById(BrowserPageActions._activatedActionPanelID));
}
-function promisePageActionPanelOpen() {
+function promiseOpenPageActionPanel() {
let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return BrowserTestUtils.waitForCondition(() => {
// Wait for the main page action button to become visible. It's hidden for
// some URIs, so depending on when this is called, it may not yet be quite
// visible. It's up to the caller to make sure it will be visible.
info("Waiting for main page action button to have non-0 size");
let bounds = dwu.getBoundsWithoutFlushing(BrowserPageActions.mainButtonNode);