The CSS for page action icons doesn't handle fallback when only one variable is defined, so for widgets that don't define their icons using CSS, we always need to provide both. MozReview-Commit-ID: 7UgMSVS3W6K
1258 lines
40 KiB
JavaScript
1258 lines
40 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"PageActions",
|
|
// PageActions.Action
|
|
// 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)}")`;
|
|
}
|
|
|
|
var PageActions = {
|
|
/**
|
|
* Inits. Call to init.
|
|
*/
|
|
init() {
|
|
let callbacks = this._deferredAddActionCalls;
|
|
delete this._deferredAddActionCalls;
|
|
|
|
this._loadPersistedActions();
|
|
|
|
// Register the built-in actions, which are defined below in this file.
|
|
for (let options of gBuiltInActions) {
|
|
if (!this.actionForID(options.id)) {
|
|
this._registerAction(new Action(options));
|
|
}
|
|
}
|
|
|
|
// Now place them all in each window. Instead of splitting the register and
|
|
// place steps, we could simply call addAction, which does both, but doing
|
|
// it this way means that all windows initially place their actions the same
|
|
// way -- placeAllActions -- regardless of whether they're open when this
|
|
// method is called or opened later.
|
|
for (let bpa of allBrowserPageActions()) {
|
|
bpa.placeAllActions();
|
|
}
|
|
|
|
// These callbacks are deferred until init happens and all built-in actions
|
|
// are added.
|
|
while (callbacks && callbacks.length) {
|
|
callbacks.shift()();
|
|
}
|
|
|
|
// Purge removed actions from persisted state on shutdown. The point is not
|
|
// to do it on Action.remove(). That way actions that are removed and
|
|
// re-added while the app is running will have their urlbar placement and
|
|
// other state remembered and restored. This happens for upgraded and
|
|
// downgraded extensions, for example.
|
|
AsyncShutdown.profileBeforeChange.addBlocker(
|
|
"PageActions: purging unregistered actions from cache",
|
|
() => this._purgeUnregisteredPersistedActions(),
|
|
);
|
|
},
|
|
|
|
_deferredAddActionCalls: [],
|
|
|
|
/**
|
|
* A list of all Action objects, not in any particular order. Not live.
|
|
* (array of Action objects)
|
|
*/
|
|
get actions() {
|
|
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) {
|
|
actions.push(new Action({
|
|
id: ACTION_ID_BUILT_IN_SEPARATOR,
|
|
_isSeparator: true,
|
|
}));
|
|
}
|
|
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 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.
|
|
*/
|
|
actionsInUrlbar(browserWindow) {
|
|
// Remember that IDs in idsInUrlbar may belong to actions that aren't
|
|
// currently registered.
|
|
return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
|
|
let action = this.actionForID(id);
|
|
if (action && action.shouldShowInUrlbar(browserWindow)) {
|
|
actions.push(action);
|
|
}
|
|
return actions;
|
|
}, []);
|
|
},
|
|
|
|
/**
|
|
* Gets an action.
|
|
*
|
|
* @param id (string, required)
|
|
* The ID of the action to get.
|
|
* @return The Action object, or null if none.
|
|
*/
|
|
actionForID(id) {
|
|
return this._actionsByID.get(id);
|
|
},
|
|
|
|
/**
|
|
* Registers an action.
|
|
*
|
|
* Actions are registered by their IDs. An error is thrown if an action with
|
|
* the given ID has already been added. Use actionForID() before calling this
|
|
* method if necessary.
|
|
*
|
|
* Be sure to call remove() on the action if the lifetime of the code that
|
|
* owns it is shorter than the browser's -- if it lives in an extension, for
|
|
* example.
|
|
*
|
|
* @param action (Action, required)
|
|
* The Action object to register.
|
|
* @return The given Action.
|
|
*/
|
|
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;
|
|
}
|
|
this._registerAction(action);
|
|
for (let bpa of allBrowserPageActions()) {
|
|
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);
|
|
|
|
// Insert the action into the appropriate list, either _builtInActions or
|
|
// _nonBuiltInActions.
|
|
|
|
// Keep in mind that _insertBeforeActionID may be present but null, which
|
|
// means the action should be appended to the built-ins.
|
|
if ("__insertBeforeActionID" in action) {
|
|
// A "semi-built-in" action, probably an action from an extension
|
|
// 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 (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) => {
|
|
return a1.getTitle().localeCompare(a2.getTitle());
|
|
}, this._nonBuiltInActions, action);
|
|
this._nonBuiltInActions.splice(index, 0, action);
|
|
}
|
|
|
|
if (this._persistedActions.ids.includes(action.id)) {
|
|
// The action has been seen before. Override its pinnedToUrlbar value
|
|
// with the persisted value. Set the private version of that property
|
|
// so that onActionToggledPinnedToUrlbar isn't called, which happens when
|
|
// the public version is set.
|
|
action._pinnedToUrlbar =
|
|
this._persistedActions.idsInUrlbar.includes(action.id);
|
|
} else {
|
|
// The action is new. Store it in the persisted actions.
|
|
this._persistedActions.ids.push(action.id);
|
|
this._updateIDsPinnedToUrlbarForAction(action);
|
|
}
|
|
},
|
|
|
|
_updateIDsPinnedToUrlbarForAction(action) {
|
|
let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
|
|
if (action.pinnedToUrlbar) {
|
|
if (index < 0) {
|
|
index = action.id == ACTION_ID_BOOKMARK ? -1 :
|
|
this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
|
|
if (index < 0) {
|
|
index = this._persistedActions.idsInUrlbar.length;
|
|
}
|
|
this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
|
|
}
|
|
} else if (index >= 0) {
|
|
this._persistedActions.idsInUrlbar.splice(index, 1);
|
|
}
|
|
this._storePersistedActions();
|
|
},
|
|
|
|
// These keep track of currently registered actions.
|
|
_builtInActions: [],
|
|
_nonBuiltInActions: [],
|
|
_transientActions: [],
|
|
_actionsByID: new Map(),
|
|
|
|
/**
|
|
* 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);
|
|
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()) {
|
|
bpa.removeAction(action);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when an action's pinnedToUrlbar property changes.
|
|
*
|
|
* @param action (Action object, required)
|
|
* The action whose pinnedToUrlbar property changed.
|
|
*/
|
|
onActionToggledPinnedToUrlbar(action) {
|
|
if (!this.actionForID(action.id)) {
|
|
// This may be called before the action has been added.
|
|
return;
|
|
}
|
|
this._updateIDsPinnedToUrlbarForAction(action);
|
|
for (let bpa of allBrowserPageActions()) {
|
|
bpa.placeActionInUrlbar(action);
|
|
}
|
|
},
|
|
|
|
logTelemetry(type, action, node = null) {
|
|
if (type == "used") {
|
|
type =
|
|
node && node.closest("#urlbar-container") ? "urlbar_used" :
|
|
"panel_used";
|
|
}
|
|
let histogramID = "FX_PAGE_ACTION_" + type.toUpperCase();
|
|
try {
|
|
let histogram = Services.telemetry.getHistogramById(histogramID);
|
|
if (action._isBuiltIn) {
|
|
histogram.add(action.labelForHistogram);
|
|
} else {
|
|
histogram.add("other");
|
|
}
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
},
|
|
|
|
// 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);
|
|
},
|
|
|
|
_loadPersistedActions() {
|
|
try {
|
|
let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
|
|
this._persistedActions = this._migratePersistedActions(JSON.parse(json));
|
|
} catch (ex) {}
|
|
},
|
|
|
|
_purgeUnregisteredPersistedActions() {
|
|
// Remove all action IDs from persisted state that do not correspond to
|
|
// currently registered actions.
|
|
for (let name of ["ids", "idsInUrlbar"]) {
|
|
this._persistedActions[name] = this._persistedActions[name].filter(id => {
|
|
return this.actionForID(id);
|
|
});
|
|
}
|
|
this._storePersistedActions();
|
|
},
|
|
|
|
_migratePersistedActions(actions) {
|
|
// Start with actions.version and migrate one version at a time, all the way
|
|
// up to the current version.
|
|
for (let version = actions.version || 0;
|
|
version < PERSISTED_ACTIONS_CURRENT_VERSION;
|
|
version++) {
|
|
let methodName = `_migratePersistedActionsTo${version + 1}`;
|
|
actions = this[methodName](actions);
|
|
actions.version = version + 1;
|
|
}
|
|
return actions;
|
|
},
|
|
|
|
_migratePersistedActionsTo1(actions) {
|
|
// The `ids` object is a mapping: action ID => true. Convert it to an array
|
|
// to save space in the prefs.
|
|
let ids = [];
|
|
for (let id in actions.ids) {
|
|
ids.push(id);
|
|
}
|
|
// Move the bookmark ID to the end of idsInUrlbar. The bookmark action
|
|
// should always remain at the end of the urlbar, if present.
|
|
let bookmarkIndex = actions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
|
|
if (bookmarkIndex >= 0) {
|
|
actions.idsInUrlbar.splice(bookmarkIndex, 1);
|
|
actions.idsInUrlbar.push(ACTION_ID_BOOKMARK);
|
|
}
|
|
return {
|
|
ids,
|
|
idsInUrlbar: actions.idsInUrlbar,
|
|
};
|
|
},
|
|
|
|
// This keeps track of all actions, even those that are not currently
|
|
// registered because they have been removed, so long as
|
|
// _purgeUnregisteredPersistedActions has not been called.
|
|
_persistedActions: {
|
|
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-
|
|
* window state. However, if you pass a window and the action has no state for
|
|
* that window, then the global state will be returned.
|
|
*
|
|
* `options` is a required object with the following properties. Regarding the
|
|
* properties discussed in the previous paragraph, the values in `options` set
|
|
* global state.
|
|
*
|
|
* @param id (string, required)
|
|
* The action's ID. Treat this like the ID of a DOM node.
|
|
* @param title (string, required)
|
|
* The action's title.
|
|
* @param anchorIDOverride (string, optional)
|
|
* Pass a string to override the node to which the action's activated-
|
|
* action panel is anchored.
|
|
* @param disabled (bool, optional)
|
|
* Pass true to cause the action to be disabled initially in all browser
|
|
* windows. False by default.
|
|
* @param extensionID (string, optional)
|
|
* If the action lives in an extension, pass its ID.
|
|
* @param iconURL (string or object, optional)
|
|
* The URL string of the action's icon. Usually you want to specify an
|
|
* icon in CSS, but this option is useful if that would be a pain for
|
|
* some reason. You can also pass an object that maps pixel sizes to
|
|
* URLs, like { 16: url16, 32: url32 }. The best size for the user's
|
|
* screen will be used.
|
|
* @param nodeAttributes (object, optional)
|
|
* An object of name-value pairs. Each pair will be added as an
|
|
* attribute to DOM nodes created for this action.
|
|
* @param onBeforePlacedInWindow (function, optional)
|
|
* Called before the action is placed in the window:
|
|
* onBeforePlacedInWindow(window)
|
|
* * window: The window that the action will be placed in.
|
|
* @param onCommand (function, optional)
|
|
* Called when the action is clicked, but only if it has neither a
|
|
* subview nor an iframe:
|
|
* onCommand(event, buttonNode)
|
|
* * event: The triggering event.
|
|
* * buttonNode: The button node that was clicked.
|
|
* @param onIframeHiding (function, optional)
|
|
* Called when the action's iframe is hiding:
|
|
* onIframeHiding(iframeNode, parentPanelNode)
|
|
* * iframeNode: The iframe.
|
|
* * parentPanelNode: The panel node in which the iframe is shown.
|
|
* @param onIframeHidden (function, optional)
|
|
* Called when the action's iframe is hidden:
|
|
* onIframeHidden(iframeNode, parentPanelNode)
|
|
* * iframeNode: The iframe.
|
|
* * parentPanelNode: The panel node in which the iframe is shown.
|
|
* @param onIframeShowing (function, optional)
|
|
* Called when the action's iframe is showing to the user:
|
|
* onIframeShowing(iframeNode, parentPanelNode)
|
|
* * iframeNode: The iframe.
|
|
* * parentPanelNode: The panel node in which the iframe is shown.
|
|
* @param onLocationChange (function, optional)
|
|
* Called after tab switch or when the current <browser>'s location
|
|
* changes:
|
|
* onLocationChange(browserWindow)
|
|
* * browserWindow: The browser window containing the tab switch or
|
|
* changed <browser>.
|
|
* @param onPlacedInPanel (function, optional)
|
|
* Called when the action is added to the page action panel in a browser
|
|
* window:
|
|
* onPlacedInPanel(buttonNode)
|
|
* * buttonNode: The action's node in the page action panel.
|
|
* @param onPlacedInUrlbar (function, optional)
|
|
* Called when the action is added to the urlbar in a browser window:
|
|
* onPlacedInUrlbar(buttonNode)
|
|
* * buttonNode: The action's node in the urlbar.
|
|
* @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 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,
|
|
iconURL: false,
|
|
labelForHistogram: false,
|
|
nodeAttributes: false,
|
|
onBeforePlacedInWindow: false,
|
|
onCommand: false,
|
|
onIframeHiding: false,
|
|
onIframeHidden: false,
|
|
onIframeShowing: false,
|
|
onLocationChange: false,
|
|
onPlacedInPanel: false,
|
|
onPlacedInUrlbar: false,
|
|
onRemovedFromWindow: false,
|
|
onShowingInPanel: false,
|
|
onSubviewPlaced: false,
|
|
onSubviewShowing: false,
|
|
pinnedToUrlbar: 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,
|
|
});
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
get extensionID() {
|
|
return this._extensionID;
|
|
},
|
|
|
|
/**
|
|
* The action's ID (string)
|
|
*/
|
|
get id() {
|
|
return this._id;
|
|
},
|
|
|
|
/**
|
|
* Attribute name => value mapping to set on nodes created for this action
|
|
* (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)
|
|
*/
|
|
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)
|
|
*/
|
|
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)
|
|
*/
|
|
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);
|
|
|
|
this._updateProperty("iconURL", props.iconProps, browserWindow);
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* The set of CSS variables which define the action's icons in various
|
|
* sizes. This is generated automatically from the iconURL property.
|
|
*/
|
|
getIconProperties(browserWindow = null) {
|
|
return this._getProperties(browserWindow).iconProps;
|
|
},
|
|
|
|
_createIconProperties(urls) {
|
|
if (urls && typeof urls == "object") {
|
|
let props = this._iconProperties.get(urls);
|
|
if (!props) {
|
|
props = Object.freeze({
|
|
"--pageAction-image-16px": escapeCSSURL(this._iconURLForSize(urls, 16)),
|
|
"--pageAction-image-32px": escapeCSSURL(this._iconURLForSize(urls, 32)),
|
|
});
|
|
this._iconProperties.set(urls, props);
|
|
}
|
|
return props;
|
|
}
|
|
|
|
let cssURL = urls ? escapeCSSURL(urls) : null;
|
|
return Object.freeze({
|
|
"--pageAction-image-16px": cssURL,
|
|
"--pageAction-image-32px": cssURL,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
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
|
|
* globally.
|
|
*/
|
|
_setProperty(name, value, browserWindow) {
|
|
let props = this._getProperties(browserWindow, !!browserWindow);
|
|
props[name] = value;
|
|
|
|
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 });
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the properties object for the given window, if it exists,
|
|
* or the global properties object if no window-specific properties
|
|
* exist.
|
|
*
|
|
* @param {Window?} window
|
|
* The window for which to return the properties object, or
|
|
* null to return the global properties object.
|
|
* @param {bool} [forceWindowSpecific = false]
|
|
* If true, always returns a window-specific properties object.
|
|
* If a properties object does not exist for the given window,
|
|
* one is created and cached.
|
|
* @returns {object}
|
|
*/
|
|
_getProperties(window, forceWindowSpecific = false) {
|
|
let props = window && this._windowProps.get(window);
|
|
|
|
if (!props && forceWindowSpecific) {
|
|
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)
|
|
*/
|
|
get anchorIDOverride() {
|
|
return this._anchorIDOverride;
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
get wantsIframe() {
|
|
return this._wantsIframe || false;
|
|
},
|
|
|
|
get labelForHistogram() {
|
|
return this._labelForHistogram || this._id;
|
|
},
|
|
|
|
/**
|
|
* Selects the best matching icon from the given URLs object for the
|
|
* given preferred size.
|
|
*
|
|
* @param {object} urls
|
|
* An object containing square icons of various sizes. The name
|
|
* of each property is its width, and the value is its image URL.
|
|
* @param {integer} peferredSize
|
|
* The preferred icon width. The most appropriate icon in the
|
|
* urls object will be chosen to match that size. An exact
|
|
* match will be preferred, followed by an icon exactly double
|
|
* the size, followed by the smallest icon larger than the
|
|
* preferred size, followed by the largest available icon.
|
|
* @returns {string}
|
|
* The chosen icon URL.
|
|
*/
|
|
_iconURLForSize(urls, preferredSize) {
|
|
// This case is copied from ExtensionParent.jsm so that our image logic is
|
|
// the same, so that WebExtensions page action tests that deal with icons
|
|
// pass.
|
|
let bestSize = null;
|
|
if (urls[preferredSize]) {
|
|
bestSize = preferredSize;
|
|
} else if (urls[2 * preferredSize]) {
|
|
bestSize = 2 * preferredSize;
|
|
} else {
|
|
let sizes = Object.keys(urls)
|
|
.map(key => parseInt(key, 10))
|
|
.sort((a, b) => a - b);
|
|
bestSize = sizes.find(candidate => candidate > preferredSize) || sizes.pop();
|
|
}
|
|
return urls[bestSize];
|
|
},
|
|
|
|
/**
|
|
* Performs the command for an action. If the action has an onCommand
|
|
* handler, then it's called. If the action has a subview or iframe, then a
|
|
* panel is opened, displaying the subview or iframe.
|
|
*
|
|
* @param browserWindow (DOM window, required)
|
|
* The browser window in which to perform the action.
|
|
*/
|
|
doCommand(browserWindow) {
|
|
browserPageActions(browserWindow).doCommandForAction(this);
|
|
},
|
|
|
|
/**
|
|
* Call this when before placing the action in the window.
|
|
*
|
|
* @param browserWindow (DOM window, required)
|
|
* The browser window the action will be placed in.
|
|
*/
|
|
onBeforePlacedInWindow(browserWindow) {
|
|
if (this._onBeforePlacedInWindow) {
|
|
this._onBeforePlacedInWindow(browserWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the user activates the action.
|
|
*
|
|
* @param event (DOM event, required)
|
|
* The triggering event.
|
|
* @param buttonNode (DOM node, required)
|
|
* The action's panel or urlbar button node that was clicked.
|
|
*/
|
|
onCommand(event, buttonNode) {
|
|
if (this._onCommand) {
|
|
this._onCommand(event, buttonNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the action's iframe is hiding.
|
|
*
|
|
* @param iframeNode (DOM node, required)
|
|
* The iframe that's hiding.
|
|
* @param parentPanelNode (DOM node, required)
|
|
* The panel in which the iframe is hiding.
|
|
*/
|
|
onIframeHiding(iframeNode, parentPanelNode) {
|
|
if (this._onIframeHiding) {
|
|
this._onIframeHiding(iframeNode, parentPanelNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the action's iframe is hidden.
|
|
*
|
|
* @param iframeNode (DOM node, required)
|
|
* The iframe that's being hidden.
|
|
* @param parentPanelNode (DOM node, required)
|
|
* The panel in which the iframe is hidden.
|
|
*/
|
|
onIframeHidden(iframeNode, parentPanelNode) {
|
|
if (this._onIframeHidden) {
|
|
this._onIframeHidden(iframeNode, parentPanelNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the action's iframe is showing.
|
|
*
|
|
* @param iframeNode (DOM node, required)
|
|
* The iframe that's being shown.
|
|
* @param parentPanelNode (DOM node, required)
|
|
* The panel in which the iframe is shown.
|
|
*/
|
|
onIframeShowing(iframeNode, parentPanelNode) {
|
|
if (this._onIframeShowing) {
|
|
this._onIframeShowing(iframeNode, parentPanelNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this on tab switch or when the current <browser>'s location changes.
|
|
*
|
|
* @param browserWindow (DOM window, required)
|
|
* The browser window containing the tab switch or changed <browser>.
|
|
*/
|
|
onLocationChange(browserWindow) {
|
|
if (this._onLocationChange) {
|
|
this._onLocationChange(browserWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when a DOM node for the action is added to the page action panel.
|
|
*
|
|
* @param buttonNode (DOM node, required)
|
|
* The action's panel button node.
|
|
*/
|
|
onPlacedInPanel(buttonNode) {
|
|
if (this._onPlacedInPanel) {
|
|
this._onPlacedInPanel(buttonNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when a DOM node for the action is added to the urlbar.
|
|
*
|
|
* @param buttonNode (DOM node, required)
|
|
* The action's urlbar button node.
|
|
*/
|
|
onPlacedInUrlbar(buttonNode) {
|
|
if (this._onPlacedInUrlbar) {
|
|
this._onPlacedInUrlbar(buttonNode);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the DOM nodes for the action are removed from a browser
|
|
* window.
|
|
*
|
|
* @param browserWindow (DOM window, required)
|
|
* The browser window the action was removed from.
|
|
*/
|
|
onRemovedFromWindow(browserWindow) {
|
|
if (this._onRemovedFromWindow) {
|
|
this._onRemovedFromWindow(browserWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call this when the action's button is shown in the page action panel.
|
|
*
|
|
* @param buttonNode (DOM node, required)
|
|
* The action's panel button node.
|
|
*/
|
|
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) {
|
|
return this.pinnedToUrlbar && !this.getDisabled(browserWindow);
|
|
},
|
|
|
|
get _isBuiltIn() {
|
|
let builtInIDs = [
|
|
"pocket",
|
|
"screenshots",
|
|
"webcompat-reporter-button",
|
|
].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
|
|
return builtInIDs.includes(this.id);
|
|
},
|
|
};
|
|
|
|
this.PageActions.Action = Action;
|
|
|
|
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;
|
|
|
|
|
|
// 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 = [
|
|
|
|
// bookmark
|
|
{
|
|
id: ACTION_ID_BOOKMARK,
|
|
urlbarIDOverride: "star-button-box",
|
|
_urlbarNodeInMarkup: true,
|
|
// The title is set in browser-pageActions.js by calling
|
|
// BookmarkingUI.updateBookmarkPageMenuItem().
|
|
title: "",
|
|
pinnedToUrlbar: true,
|
|
nodeAttributes: {
|
|
observes: "bookmarkThisPageBroadcaster",
|
|
},
|
|
onShowingInPanel(buttonNode) {
|
|
browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
|
|
},
|
|
onCommand(event, buttonNode) {
|
|
browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode);
|
|
},
|
|
},
|
|
|
|
// separator
|
|
{
|
|
id: ACTION_ID_BOOKMARK_SEPARATOR,
|
|
_isSeparator: true,
|
|
},
|
|
|
|
// copy URL
|
|
{
|
|
id: "copyURL",
|
|
title: "copyURL-title",
|
|
onPlacedInPanel(buttonNode) {
|
|
browserPageActions(buttonNode).copyURL.onPlacedInPanel(buttonNode);
|
|
},
|
|
onCommand(event, buttonNode) {
|
|
browserPageActions(buttonNode).copyURL.onCommand(event, buttonNode);
|
|
},
|
|
},
|
|
|
|
// email link
|
|
{
|
|
id: "emailLink",
|
|
title: "emailLink-title",
|
|
onPlacedInPanel(buttonNode) {
|
|
browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
|
|
},
|
|
onCommand(event, buttonNode) {
|
|
browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
|
|
},
|
|
},
|
|
|
|
// add search engine
|
|
{
|
|
id: "addSearchEngine",
|
|
// The title is set in browser-pageActions.js.
|
|
title: "",
|
|
_transient: true,
|
|
onShowingInPanel(buttonNode) {
|
|
browserPageActions(buttonNode).addSearchEngine.onShowingInPanel();
|
|
},
|
|
onCommand(event, buttonNode) {
|
|
browserPageActions(buttonNode).addSearchEngine
|
|
.onCommand(event, buttonNode);
|
|
},
|
|
onSubviewShowing(panelViewNode) {
|
|
browserPageActions(panelViewNode).addSearchEngine
|
|
.onSubviewShowing(panelViewNode);
|
|
},
|
|
},
|
|
];
|
|
|
|
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();
|
|
},
|
|
wantsSubview: true,
|
|
onSubviewPlaced(panelViewNode) {
|
|
browserPageActions(panelViewNode).sendToDevice
|
|
.onSubviewPlaced(panelViewNode);
|
|
},
|
|
onSubviewShowing(panelViewNode) {
|
|
browserPageActions(panelViewNode).sendToDevice
|
|
.onShowingSubview(panelViewNode);
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets a BrowserPageActions object in a browser window.
|
|
*
|
|
* @param obj
|
|
* Either a DOM node or a browser window.
|
|
* @return The BrowserPageActions object in the browser window related to the
|
|
* given object.
|
|
*/
|
|
function browserPageActions(obj) {
|
|
if (obj.BrowserPageActions) {
|
|
return obj.BrowserPageActions;
|
|
}
|
|
return obj.ownerGlobal.BrowserPageActions;
|
|
}
|
|
|
|
/**
|
|
* A generator function for all open browser windows.
|
|
*
|
|
* @param browserWindow (DOM window, optional)
|
|
* If given, then only this window will be yielded. That may sound
|
|
* pointless, but it can make callers nicer to write since they don't
|
|
* need two separate cases, one where a window is given and another where
|
|
* it isn't.
|
|
*/
|
|
function* allBrowserWindows(browserWindow = null) {
|
|
if (browserWindow) {
|
|
yield browserWindow;
|
|
return;
|
|
}
|
|
let windows = Services.wm.getEnumerator("navigator:browser");
|
|
while (windows.hasMoreElements()) {
|
|
yield windows.getNext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A generator function for BrowserPageActions objects in all open windows.
|
|
*
|
|
* @param browserWindow (DOM window, optional)
|
|
* If given, then the BrowserPageActions for only this window will be
|
|
* yielded.
|
|
*/
|
|
function* allBrowserPageActions(browserWindow = null) {
|
|
for (let win of allBrowserWindows(browserWindow)) {
|
|
yield browserPageActions(win);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A simple function that sets properties on a given object while doing basic
|
|
* required-properties checking. If a required property isn't specified in the
|
|
* given options object, or if the options object has properties that aren't in
|
|
* the given schema, then an error is thrown.
|
|
*
|
|
* @param obj
|
|
* The object to set properties on.
|
|
* @param options
|
|
* An options object supplied by the consumer.
|
|
* @param schema
|
|
* An object a property for each required and optional property. The
|
|
* keys are property names; the value of a key is a bool that is true if
|
|
* the property is required.
|
|
*/
|
|
function setProperties(obj, options, schema) {
|
|
for (let name in schema) {
|
|
let required = schema[name];
|
|
if (required && !(name in options)) {
|
|
throw new Error(`'${name}' must be specified`);
|
|
}
|
|
let nameInObj = "_" + name;
|
|
if (name[0] == "_") {
|
|
// The property is "private". If it's defined in the options, then define
|
|
// it on obj exactly as it's defined on options.
|
|
if (name in options) {
|
|
obj[nameInObj] = options[name];
|
|
}
|
|
} else {
|
|
// The property is "public". Make sure the property is defined on obj.
|
|
obj[nameInObj] = options[name] || null;
|
|
}
|
|
}
|
|
for (let name in options) {
|
|
if (!(name in schema)) {
|
|
throw new Error(`Unrecognized option '${name}'`);
|
|
}
|
|
}
|
|
}
|