1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
11 BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
12 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
13 setTimeout: "resource://gre/modules/Timer.sys.mjs",
16 XPCOMUtils.defineLazyModuleGetters(lazy, {
17 ASRouter: "resource://activity-stream/lib/ASRouter.jsm",
20 const ACTION_ID_BOOKMARK = "bookmark";
21 const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
22 const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
24 const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
25 const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
27 // Escapes the given raw URL string, and returns an equivalent CSS url()
29 function escapeCSSURL(url) {
30 return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
33 export var PageActions = {
35 * Initializes PageActions.
37 * @param {boolean} addShutdownBlocker
38 * This param exists only for tests. Normally the default value of true
41 init(addShutdownBlocker = true) {
42 this._initBuiltInActions();
44 let callbacks = this._deferredAddActionCalls;
45 delete this._deferredAddActionCalls;
47 this._loadPersistedActions();
49 // Register the built-in actions, which are defined below in this file.
50 for (let options of gBuiltInActions) {
51 if (!this.actionForID(options.id)) {
52 this._registerAction(new Action(options));
56 // Now place them all in each window. Instead of splitting the register and
57 // place steps, we could simply call addAction, which does both, but doing
58 // it this way means that all windows initially place their actions in the
59 // urlbar the same way -- placeAllActions -- regardless of whether they're
60 // open when this method is called or opened later.
61 for (let bpa of allBrowserPageActions()) {
62 bpa.placeAllActionsInUrlbar();
65 // These callbacks are deferred until init happens and all built-in actions
67 while (callbacks && callbacks.length) {
71 if (addShutdownBlocker) {
72 // Purge removed actions from persisted state on shutdown. The point is
73 // not to do it on Action.remove(). That way actions that are removed and
74 // re-added while the app is running will have their urlbar placement and
75 // other state remembered and restored. This happens for upgraded and
76 // downgraded extensions, for example.
77 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
78 "PageActions: purging unregistered actions from cache",
79 () => this._purgeUnregisteredPersistedActions()
84 _deferredAddActionCalls: [],
87 * A list of all Action objects, not in any particular order. Not live.
88 * (array of Action objects)
93 this._nonBuiltInActions,
94 this._transientActions,
96 return lists.reduce((memo, list) => memo.concat(list), []);
100 * The list of Action objects that should appear in the panel for a given
101 * window, sorted in the order in which they appear. If there are both
102 * built-in and non-built-in actions, then the list will include the separator
103 * between the two. The list is not live. (array of Action objects)
105 * @param browserWindow (DOM window, required)
106 * This window's actions will be returned.
107 * @return (array of PageAction.Action objects) The actions currently in the
108 * given window's panel.
110 actionsInPanel(browserWindow) {
111 function filter(action) {
112 return action.shouldShowInPanel(browserWindow);
114 let actions = this._builtInActions.filter(filter);
115 let nonBuiltInActions = this._nonBuiltInActions.filter(filter);
116 if (nonBuiltInActions.length) {
117 if (actions.length) {
120 id: ACTION_ID_BUILT_IN_SEPARATOR,
125 actions.push(...nonBuiltInActions);
127 let transientActions = this._transientActions.filter(filter);
128 if (transientActions.length) {
129 if (actions.length) {
132 id: ACTION_ID_TRANSIENT_SEPARATOR,
137 actions.push(...transientActions);
143 * The list of actions currently in the urlbar, sorted in the order in which
144 * they appear. Not live.
146 * @param browserWindow (DOM window, required)
147 * This window's actions will be returned.
148 * @return (array of PageAction.Action objects) The actions currently in the
149 * given window's urlbar.
151 actionsInUrlbar(browserWindow) {
152 // Remember that IDs in idsInUrlbar may belong to actions that aren't
153 // currently registered.
154 return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
155 let action = this.actionForID(id);
156 if (action && action.shouldShowInUrlbar(browserWindow)) {
157 actions.push(action);
166 * @param id (string, required)
167 * The ID of the action to get.
168 * @return The Action object, or null if none.
171 return this._actionsByID.get(id);
175 * Registers an action.
177 * Actions are registered by their IDs. An error is thrown if an action with
178 * the given ID has already been added. Use actionForID() before calling this
179 * method if necessary.
181 * Be sure to call remove() on the action if the lifetime of the code that
182 * owns it is shorter than the browser's -- if it lives in an extension, for
185 * @param action (Action, required)
186 * The Action object to register.
187 * @return The given Action.
190 if (this._deferredAddActionCalls) {
191 // init() hasn't been called yet. Defer all additions until it's called,
192 // at which time _deferredAddActionCalls will be deleted.
193 this._deferredAddActionCalls.push(() => this.addAction(action));
196 this._registerAction(action);
197 for (let bpa of allBrowserPageActions()) {
198 bpa.placeAction(action);
203 _registerAction(action) {
204 if (this.actionForID(action.id)) {
205 throw new Error(`Action with ID '${action.id}' already added`);
207 this._actionsByID.set(action.id, action);
209 // Insert the action into the appropriate list, either _builtInActions or
210 // _nonBuiltInActions.
212 // Keep in mind that _insertBeforeActionID may be present but null, which
213 // means the action should be appended to the built-ins.
214 if ("__insertBeforeActionID" in action) {
215 // A "semi-built-in" action, probably an action from an extension
216 // bundled with the browser. Right now we simply assume that no other
217 // consumers will use _insertBeforeActionID.
218 let index = !action.__insertBeforeActionID
220 : this._builtInActions.findIndex(a => {
221 return a.id == action.__insertBeforeActionID;
224 // Append the action (excluding transient actions).
225 index = this._builtInActions.filter(a => !a.__transient).length;
227 this._builtInActions.splice(index, 0, action);
228 } else if (action.__transient) {
229 // A transient action.
230 this._transientActions.push(action);
231 } else if (action._isBuiltIn) {
232 // A built-in action. These are mostly added on init before all other
233 // actions, one after the other. Extension actions load later and should
234 // be at the end, so just push onto the array.
235 this._builtInActions.push(action);
237 // A non-built-in action, like a non-bundled extension potentially.
238 // Keep this list sorted by title.
239 let index = lazy.BinarySearch.insertionIndexOf(
241 return a1.getTitle().localeCompare(a2.getTitle());
243 this._nonBuiltInActions,
246 this._nonBuiltInActions.splice(index, 0, action);
249 let isNew = !this._persistedActions.ids.includes(action.id);
251 // The action is new. Store it in the persisted actions.
252 this._persistedActions.ids.push(action.id);
255 // Actions are always pinned to the urlbar, except for panel separators.
256 action._pinnedToUrlbar = !action.__isSeparator;
257 this._updateIDsPinnedToUrlbarForAction(action);
260 _updateIDsPinnedToUrlbarForAction(action) {
261 let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
262 if (action.pinnedToUrlbar) {
265 action.id == ACTION_ID_BOOKMARK
267 : this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
269 index = this._persistedActions.idsInUrlbar.length;
271 this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
273 } else if (index >= 0) {
274 this._persistedActions.idsInUrlbar.splice(index, 1);
276 this._storePersistedActions();
279 // These keep track of currently registered actions.
281 _nonBuiltInActions: [],
282 _transientActions: [],
283 _actionsByID: new Map(),
286 * Call this when an action is removed.
288 * @param action (Action object, required)
289 * The action that was removed.
291 onActionRemoved(action) {
292 if (!this.actionForID(action.id)) {
293 // The action isn't registered (yet). Not an error.
297 this._actionsByID.delete(action.id);
299 this._builtInActions,
300 this._nonBuiltInActions,
301 this._transientActions,
303 for (let list of lists) {
304 let index = list.findIndex(a => a.id == action.id);
306 list.splice(index, 1);
311 for (let bpa of allBrowserPageActions()) {
312 bpa.removeAction(action);
317 * Call this when an action's pinnedToUrlbar property changes.
319 * @param action (Action object, required)
320 * The action whose pinnedToUrlbar property changed.
322 onActionToggledPinnedToUrlbar(action) {
323 if (!this.actionForID(action.id)) {
324 // This may be called before the action has been added.
327 this._updateIDsPinnedToUrlbarForAction(action);
328 for (let bpa of allBrowserPageActions()) {
329 bpa.placeActionInUrlbar(action);
333 // For tests. See Bug 1413692.
335 PageActions._purgeUnregisteredPersistedActions();
336 PageActions._builtInActions = [];
337 PageActions._nonBuiltInActions = [];
338 PageActions._transientActions = [];
339 PageActions._actionsByID = new Map();
342 _storePersistedActions() {
343 let json = JSON.stringify(this._persistedActions);
344 Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
347 _loadPersistedActions() {
350 let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
351 actions = this._migratePersistedActions(JSON.parse(json));
354 // Handle migrating to and from Proton. We want to gracefully handle
355 // downgrades from Proton, and since Proton is controlled by a pref, we also
356 // don't want to assume that a downgrade is possible only by downgrading the
357 // app. That makes it hard to use the normal migration approach of creating
358 // a new persisted actions version, so we handle Proton migration specially.
359 // We try-catch it separately from the earlier _migratePersistedActions call
360 // because it should not be short-circuited when the pref load or usual
363 actions = this._migratePersistedActionsProton(actions);
366 // If `actions` is still not defined, then this._persistedActions will
367 // remain its default value.
369 this._persistedActions = actions;
373 _purgeUnregisteredPersistedActions() {
374 // Remove all action IDs from persisted state that do not correspond to
375 // currently registered actions.
376 for (let name of ["ids", "idsInUrlbar"]) {
377 this._persistedActions[name] = this._persistedActions[name].filter(id => {
378 return this.actionForID(id);
381 this._storePersistedActions();
384 _migratePersistedActions(actions) {
385 // Start with actions.version and migrate one version at a time, all the way
386 // up to the current version.
388 let version = actions.version || 0;
389 version < PERSISTED_ACTIONS_CURRENT_VERSION;
392 let methodName = `_migratePersistedActionsTo${version + 1}`;
393 actions = this[methodName](actions);
394 actions.version = version + 1;
399 _migratePersistedActionsTo1(actions) {
400 // The `ids` object is a mapping: action ID => true. Convert it to an array
401 // to save space in the prefs.
403 for (let id in actions.ids) {
406 // Move the bookmark ID to the end of idsInUrlbar. The bookmark action
407 // should always remain at the end of the urlbar, if present.
408 let bookmarkIndex = actions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
409 if (bookmarkIndex >= 0) {
410 actions.idsInUrlbar.splice(bookmarkIndex, 1);
411 actions.idsInUrlbar.push(ACTION_ID_BOOKMARK);
415 idsInUrlbar: actions.idsInUrlbar,
419 _migratePersistedActionsProton(actions) {
420 if (actions?.idsInUrlbarPreProton) {
421 // continue with Proton
422 } else if (actions) {
424 actions.idsInUrlbarPreProton = [...(actions.idsInUrlbar || [])];
426 // new profile with Proton
430 idsInUrlbarPreProton: [],
431 version: PERSISTED_ACTIONS_CURRENT_VERSION,
438 * Send an ASRouter trigger to possibly show messaging related to the page
439 * action that was placed in the urlbar.
441 * @param {Element} buttonNode The page action button node.
443 sendPlacedInUrlbarTrigger(buttonNode) {
444 lazy.setTimeout(async () => {
445 await lazy.ASRouter.initialized;
446 let win = buttonNode?.ownerGlobal;
447 if (!win || buttonNode.hidden) {
450 await lazy.ASRouter.sendTriggerMessage({
451 browser: win.gBrowser.selectedBrowser,
452 id: "pageActionInUrlbar",
453 context: { pageAction: buttonNode.id },
458 // This keeps track of all actions, even those that are not currently
459 // registered because they have been removed, so long as
460 // _purgeUnregisteredPersistedActions has not been called.
462 version: PERSISTED_ACTIONS_CURRENT_VERSION,
463 // action IDs that have ever been seen and not removed, order not important
465 // action IDs ordered by position in urlbar
471 * A single page action.
473 * Each action can have both per-browser-window state and global state.
474 * Per-window state takes precedence over global state. This is reflected in
475 * the title, tooltip, disabled, and icon properties. Each of these properties
476 * has a getter method and setter method that takes a browser window. Pass null
477 * to get the action's global state. Pass a browser window to get the per-
478 * window state. However, if you pass a window and the action has no state for
479 * that window, then the global state will be returned.
481 * `options` is a required object with the following properties. Regarding the
482 * properties discussed in the previous paragraph, the values in `options` set
485 * @param id (string, required)
486 * The action's ID. Treat this like the ID of a DOM node.
487 * @param title (string, optional)
488 * The action's title. It is optional for built in actions.
489 * @param anchorIDOverride (string, optional)
490 * Pass a string to override the node to which the action's activated-
491 * action panel is anchored.
492 * @param disabled (bool, optional)
493 * Pass true to cause the action to be disabled initially in all browser
494 * windows. False by default.
495 * @param extensionID (string, optional)
496 * If the action lives in an extension, pass its ID.
497 * @param iconURL (string or object, optional)
498 * The URL string of the action's icon. Usually you want to specify an
499 * icon in CSS, but this option is useful if that would be a pain for
500 * some reason. You can also pass an object that maps pixel sizes to
501 * URLs, like { 16: url16, 32: url32 }. The best size for the user's
502 * screen will be used.
503 * @param isBadged (bool, optional)
504 * If true, the toolbarbutton for this action will get a
505 * "badged" attribute.
506 * @param onBeforePlacedInWindow (function, optional)
507 * Called before the action is placed in the window:
508 * onBeforePlacedInWindow(window)
509 * * window: The window that the action will be placed in.
510 * @param onCommand (function, optional)
511 * Called when the action is clicked, but only if it has neither a
512 * subview nor an iframe:
513 * onCommand(event, buttonNode)
514 * * event: The triggering event.
515 * * buttonNode: The button node that was clicked.
516 * @param onIframeHiding (function, optional)
517 * Called when the action's iframe is hiding:
518 * onIframeHiding(iframeNode, parentPanelNode)
519 * * iframeNode: The iframe.
520 * * parentPanelNode: The panel node in which the iframe is shown.
521 * @param onIframeHidden (function, optional)
522 * Called when the action's iframe is hidden:
523 * onIframeHidden(iframeNode, parentPanelNode)
524 * * iframeNode: The iframe.
525 * * parentPanelNode: The panel node in which the iframe is shown.
526 * @param onIframeShowing (function, optional)
527 * Called when the action's iframe is showing to the user:
528 * onIframeShowing(iframeNode, parentPanelNode)
529 * * iframeNode: The iframe.
530 * * parentPanelNode: The panel node in which the iframe is shown.
531 * @param onLocationChange (function, optional)
532 * Called after tab switch or when the current <browser>'s location
534 * onLocationChange(browserWindow)
535 * * browserWindow: The browser window containing the tab switch or
537 * @param onPlacedInPanel (function, optional)
538 * Called when the action is added to the page action panel in a browser
540 * onPlacedInPanel(buttonNode)
541 * * buttonNode: The action's node in the page action panel.
542 * @param onPlacedInUrlbar (function, optional)
543 * Called when the action is added to the urlbar in a browser window:
544 * onPlacedInUrlbar(buttonNode)
545 * * buttonNode: The action's node in the urlbar.
546 * @param onRemovedFromWindow (function, optional)
547 * Called after the action is removed from a browser window:
548 * onRemovedFromWindow(browserWindow)
549 * * browserWindow: The browser window that the action was removed from.
550 * @param onShowingInPanel (function, optional)
551 * Called when a browser window's page action panel is showing:
552 * onShowingInPanel(buttonNode)
553 * * buttonNode: The action's node in the page action panel.
554 * @param onSubviewPlaced (function, optional)
555 * Called when the action's subview is added to its parent panel in a
557 * onSubviewPlaced(panelViewNode)
558 * * panelViewNode: The subview's panelview node.
559 * @param onSubviewShowing (function, optional)
560 * Called when the action's subview is showing in a browser window:
561 * onSubviewShowing(panelViewNode)
562 * * panelViewNode: The subview's panelview node.
563 * @param pinnedToUrlbar (bool, optional)
564 * Pass true to pin the action to the urlbar. An action is shown in the
565 * urlbar if it's pinned and not disabled. False by default.
566 * @param tooltip (string, optional)
567 * The action's button tooltip text.
568 * @param urlbarIDOverride (string, optional)
569 * Usually the ID of the action's button in the urlbar will be generated
570 * automatically. Pass a string for this property to override that with
572 * @param wantsIframe (bool, optional)
573 * Pass true to make an action that shows an iframe in a panel when
575 * @param wantsSubview (bool, optional)
576 * Pass true to make an action that shows a panel subview when clicked.
577 * @param disablePrivateBrowsing (bool, optional)
578 * Pass true to prevent the action from showing in a private browsing window.
580 function Action(options) {
581 setProperties(this, options, {
584 anchorIDOverride: false,
589 labelForHistogram: false,
590 onBeforePlacedInWindow: false,
592 onIframeHiding: false,
593 onIframeHidden: false,
594 onIframeShowing: false,
595 onLocationChange: false,
596 onPlacedInPanel: false,
597 onPlacedInUrlbar: false,
598 onRemovedFromWindow: false,
599 onShowingInPanel: false,
600 onSubviewPlaced: false,
601 onSubviewShowing: false,
602 onPinToUrlbarToggled: false,
603 pinnedToUrlbar: false,
605 urlbarIDOverride: false,
608 disablePrivateBrowsing: false,
612 // (string, optional)
613 // The ID of another action before which to insert this new action in the
615 _insertBeforeActionID: false,
618 // True if this isn't really an action but a separator to be shown in the
619 // page action panel.
623 // Transient actions have a couple of special properties: (1) They stick to
624 // the bottom of the panel, and (2) they're hidden in the panel when they're
625 // disabled. Other than that they behave like other actions.
629 // True if the action's urlbar button is defined in markup. In that case, a
630 // node with the action's urlbar node ID should already exist in the DOM
631 // (either the auto-generated ID or urlbarIDOverride). That node will be
632 // shown when the action is added to the urlbar and hidden when the action
633 // is removed from the urlbar.
634 _urlbarNodeInMarkup: false,
638 * A cache of the pre-computed CSS variable values for a given icon
639 * URLs object, as passed to _createIconProperties.
641 this._iconProperties = new WeakMap();
644 * The global values for the action properties.
646 this._globalProps = {
647 disabled: this._disabled,
648 iconURL: this._iconURL,
649 iconProps: this._createIconProperties(this._iconURL),
651 tooltip: this._tooltip,
652 wantsSubview: this._wantsSubview,
656 * A mapping of window-specific action property objects, each of which
657 * derives from the _globalProps object.
659 this._windowProps = new WeakMap();
664 * The ID of the action's parent extension (string)
667 return this._extensionID;
671 * The action's ID (string)
677 get disablePrivateBrowsing() {
678 return !!this._disablePrivateBrowsing;
682 * Verifies that the action can be shown in a private window. For
683 * extensions, verifies the extension has access to the window.
685 canShowInWindow(browserWindow) {
686 if (this._extensionID) {
687 let policy = WebExtensionPolicy.getByID(this._extensionID);
688 if (!policy.canAccessWindow(browserWindow)) {
693 this.disablePrivateBrowsing &&
694 lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow)
699 * True if the action is pinned to the urlbar. The action is shown in the
700 * urlbar if it's pinned and not disabled. (bool)
702 get pinnedToUrlbar() {
703 return this._pinnedToUrlbar || false;
705 set pinnedToUrlbar(shown) {
706 if (this.pinnedToUrlbar != shown) {
707 this._pinnedToUrlbar = shown;
708 PageActions.onActionToggledPinnedToUrlbar(this);
709 this.onPinToUrlbarToggled();
714 * The action's disabled state (bool)
716 getDisabled(browserWindow = null) {
717 return !!this._getProperties(browserWindow).disabled;
719 setDisabled(value, browserWindow = null) {
720 return this._setProperty("disabled", !!value, browserWindow);
724 * The action's icon URL string, or an object mapping sizes to URL strings
727 getIconURL(browserWindow = null) {
728 return this._getProperties(browserWindow).iconURL;
730 setIconURL(value, browserWindow = null) {
731 let props = this._getProperties(browserWindow, !!browserWindow);
732 props.iconURL = value;
733 props.iconProps = this._createIconProperties(value);
735 this._updateProperty("iconURL", props.iconProps, browserWindow);
740 * The set of CSS variables which define the action's icons in various
741 * sizes. This is generated automatically from the iconURL property.
743 getIconProperties(browserWindow = null) {
744 return this._getProperties(browserWindow).iconProps;
747 _createIconProperties(urls) {
748 if (urls && typeof urls == "object") {
749 let props = this._iconProperties.get(urls);
751 props = Object.freeze({
752 "--pageAction-image": `image-set(
753 ${escapeCSSURL(this._iconURLForSize(urls, 16))},
754 ${escapeCSSURL(this._iconURLForSize(urls, 32))} 2x
757 this._iconProperties.set(urls, props);
762 let cssURL = urls ? escapeCSSURL(urls) : null;
763 return Object.freeze({
764 "--pageAction-image": cssURL,
769 * The action's title (string). Note, built in actions will
770 * not have a title property.
772 getTitle(browserWindow = null) {
773 return this._getProperties(browserWindow).title;
775 setTitle(value, browserWindow = null) {
776 return this._setProperty("title", value, browserWindow);
780 * The action's tooltip (string)
782 getTooltip(browserWindow = null) {
783 return this._getProperties(browserWindow).tooltip;
785 setTooltip(value, browserWindow = null) {
786 return this._setProperty("tooltip", value, browserWindow);
790 * Whether the action wants a subview (bool)
792 getWantsSubview(browserWindow = null) {
793 return !!this._getProperties(browserWindow).wantsSubview;
795 setWantsSubview(value, browserWindow = null) {
796 return this._setProperty("wantsSubview", !!value, browserWindow);
800 * Sets a property, optionally for a particular browser window.
802 * @param name (string, required)
803 * The (non-underscored) name of the property.
806 * @param browserWindow (DOM window, optional)
807 * If given, then the property will be set in this window's state, not
810 _setProperty(name, value, browserWindow) {
811 let props = this._getProperties(browserWindow, !!browserWindow);
814 this._updateProperty(name, value, browserWindow);
818 _updateProperty(name, value, browserWindow) {
819 // This may be called before the action has been added.
820 if (PageActions.actionForID(this.id)) {
821 for (let bpa of allBrowserPageActions(browserWindow)) {
822 bpa.updateAction(this, name, { value });
828 * Returns the properties object for the given window, if it exists,
829 * or the global properties object if no window-specific properties
832 * @param {Window?} window
833 * The window for which to return the properties object, or
834 * null to return the global properties object.
835 * @param {bool} [forceWindowSpecific = false]
836 * If true, always returns a window-specific properties object.
837 * If a properties object does not exist for the given window,
838 * one is created and cached.
841 _getProperties(window, forceWindowSpecific = false) {
842 let props = window && this._windowProps.get(window);
844 if (!props && forceWindowSpecific) {
845 props = Object.create(this._globalProps);
846 this._windowProps.set(window, props);
849 return props || this._globalProps;
853 * Override for the ID of the action's activated-action panel anchor (string)
855 get anchorIDOverride() {
856 return this._anchorIDOverride;
860 * Override for the ID of the action's urlbar node (string)
862 get urlbarIDOverride() {
863 return this._urlbarIDOverride;
867 * True if the action is shown in an iframe (bool)
870 return this._wantsIframe || false;
874 return this._isBadged || false;
877 get labelForHistogram() {
878 // The histogram label value has a length limit of 20 and restricted to a
879 // pattern. See MAX_LABEL_LENGTH and CPP_IDENTIFIER_PATTERN in
880 // toolkit/components/telemetry/parse_histograms.py
882 this._labelForHistogram ||
883 this._id.replace(/_\w{1}/g, match => match[1].toUpperCase()).substr(0, 20)
888 * Selects the best matching icon from the given URLs object for the
889 * given preferred size.
891 * @param {object} urls
892 * An object containing square icons of various sizes. The name
893 * of each property is its width, and the value is its image URL.
894 * @param {integer} peferredSize
895 * The preferred icon width. The most appropriate icon in the
896 * urls object will be chosen to match that size. An exact
897 * match will be preferred, followed by an icon exactly double
898 * the size, followed by the smallest icon larger than the
899 * preferred size, followed by the largest available icon.
901 * The chosen icon URL.
903 _iconURLForSize(urls, preferredSize) {
904 // This case is copied from ExtensionParent.jsm so that our image logic is
905 // the same, so that WebExtensions page action tests that deal with icons
908 if (urls[preferredSize]) {
909 bestSize = preferredSize;
910 } else if (urls[2 * preferredSize]) {
911 bestSize = 2 * preferredSize;
913 let sizes = Object.keys(urls)
914 .map(key => parseInt(key, 10))
915 .sort((a, b) => a - b);
917 sizes.find(candidate => candidate > preferredSize) || sizes.pop();
919 return urls[bestSize];
923 * Performs the command for an action. If the action has an onCommand
924 * handler, then it's called. If the action has a subview or iframe, then a
925 * panel is opened, displaying the subview or iframe.
927 * @param browserWindow (DOM window, required)
928 * The browser window in which to perform the action.
930 doCommand(browserWindow) {
931 browserPageActions(browserWindow).doCommandForAction(this);
935 * Call this when before placing the action in the window.
937 * @param browserWindow (DOM window, required)
938 * The browser window the action will be placed in.
940 onBeforePlacedInWindow(browserWindow) {
941 if (this._onBeforePlacedInWindow) {
942 this._onBeforePlacedInWindow(browserWindow);
947 * Call this when the user activates the action.
949 * @param event (DOM event, required)
950 * The triggering event.
951 * @param buttonNode (DOM node, required)
952 * The action's panel or urlbar button node that was clicked.
954 onCommand(event, buttonNode) {
955 if (this._onCommand) {
956 this._onCommand(event, buttonNode);
961 * Call this when the action's iframe is hiding.
963 * @param iframeNode (DOM node, required)
964 * The iframe that's hiding.
965 * @param parentPanelNode (DOM node, required)
966 * The panel in which the iframe is hiding.
968 onIframeHiding(iframeNode, parentPanelNode) {
969 if (this._onIframeHiding) {
970 this._onIframeHiding(iframeNode, parentPanelNode);
975 * Call this when the action's iframe is hidden.
977 * @param iframeNode (DOM node, required)
978 * The iframe that's being hidden.
979 * @param parentPanelNode (DOM node, required)
980 * The panel in which the iframe is hidden.
982 onIframeHidden(iframeNode, parentPanelNode) {
983 if (this._onIframeHidden) {
984 this._onIframeHidden(iframeNode, parentPanelNode);
989 * Call this when the action's iframe is showing.
991 * @param iframeNode (DOM node, required)
992 * The iframe that's being shown.
993 * @param parentPanelNode (DOM node, required)
994 * The panel in which the iframe is shown.
996 onIframeShowing(iframeNode, parentPanelNode) {
997 if (this._onIframeShowing) {
998 this._onIframeShowing(iframeNode, parentPanelNode);
1003 * Call this on tab switch or when the current <browser>'s location changes.
1005 * @param browserWindow (DOM window, required)
1006 * The browser window containing the tab switch or changed <browser>.
1008 onLocationChange(browserWindow) {
1009 if (this._onLocationChange) {
1010 this._onLocationChange(browserWindow);
1015 * Call this when a DOM node for the action is added to the page action panel.
1017 * @param buttonNode (DOM node, required)
1018 * The action's panel button node.
1020 onPlacedInPanel(buttonNode) {
1021 if (this._onPlacedInPanel) {
1022 this._onPlacedInPanel(buttonNode);
1027 * Call this when a DOM node for the action is added to the urlbar.
1029 * @param buttonNode (DOM node, required)
1030 * The action's urlbar button node.
1032 onPlacedInUrlbar(buttonNode) {
1033 if (this._onPlacedInUrlbar) {
1034 this._onPlacedInUrlbar(buttonNode);
1039 * Call this when the DOM nodes for the action are removed from a browser
1042 * @param browserWindow (DOM window, required)
1043 * The browser window the action was removed from.
1045 onRemovedFromWindow(browserWindow) {
1046 if (this._onRemovedFromWindow) {
1047 this._onRemovedFromWindow(browserWindow);
1052 * Call this when the action's button is shown in the page action panel.
1054 * @param buttonNode (DOM node, required)
1055 * The action's panel button node.
1057 onShowingInPanel(buttonNode) {
1058 if (this._onShowingInPanel) {
1059 this._onShowingInPanel(buttonNode);
1064 * Call this when a panelview node for the action's subview is added to the
1067 * @param panelViewNode (DOM node, required)
1068 * The subview's panelview node.
1070 onSubviewPlaced(panelViewNode) {
1071 if (this._onSubviewPlaced) {
1072 this._onSubviewPlaced(panelViewNode);
1077 * Call this when a panelview node for the action's subview is showing.
1079 * @param panelViewNode (DOM node, required)
1080 * The subview's panelview node.
1082 onSubviewShowing(panelViewNode) {
1083 if (this._onSubviewShowing) {
1084 this._onSubviewShowing(panelViewNode);
1088 * Call this when an icon in the url is pinned or unpinned.
1090 onPinToUrlbarToggled() {
1091 if (this._onPinToUrlbarToggled) {
1092 this._onPinToUrlbarToggled();
1097 * Removes the action's DOM nodes from all browser windows.
1099 * PageActions will remember the action's urlbar placement, if any, after this
1100 * method is called until app shutdown. If the action is not added again
1101 * before shutdown, then PageActions will discard the placement, and the next
1102 * time the action is added, its placement will be reset.
1105 PageActions.onActionRemoved(this);
1109 * Returns whether the action should be shown in a given window's panel.
1111 * @param browserWindow (DOM window, required)
1113 * @return True if the action should be shown and false otherwise. Actions
1114 * are always shown in the panel unless they're both transient and
1117 shouldShowInPanel(browserWindow) {
1118 // When Proton is enabled, the extension page actions should behave similarly
1119 // to a transient action, and be hidden from the urlbar overflow menu if they
1120 // are disabled (as in the urlbar when the overflow menu isn't available)
1122 // TODO(Bug 1704139): as a follow up we may look into just set on all
1123 // extensions pageActions `_transient: true`, at least once we sunset
1124 // the proton preference and we don't need the pre-Proton behavior anymore,
1125 // and remove this special case.
1126 const isProtonExtensionAction = this.extensionID;
1129 (!(this.__transient || isProtonExtensionAction) ||
1130 !this.getDisabled(browserWindow)) &&
1131 this.canShowInWindow(browserWindow)
1136 * Returns whether the action should be shown in a given window's urlbar.
1138 * @param browserWindow (DOM window, required)
1140 * @return True if the action should be shown and false otherwise. The action
1141 * should be shown if it's both pinned and not disabled.
1143 shouldShowInUrlbar(browserWindow) {
1145 this.pinnedToUrlbar &&
1146 !this.getDisabled(browserWindow) &&
1147 this.canShowInWindow(browserWindow)
1152 let builtInIDs = ["screenshots_mozilla_org"].concat(
1153 gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id)
1155 return builtInIDs.includes(this.id);
1158 get _isMozillaAction() {
1159 return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org";
1163 PageActions.Action = Action;
1165 PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
1166 PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
1168 // These are only necessary so that the test can use them.
1169 PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK;
1170 PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS;
1172 // Sorted in the order in which they should appear in the page action panel.
1173 // Does not include the page actions of extensions bundled with the browser.
1174 // They're added by the relevant extension code.
1175 // NOTE: If you add items to this list (or system add-on actions that we
1176 // want to keep track of), make sure to also update Histograms.json for the
1178 var gBuiltInActions;
1180 PageActions._initBuiltInActions = function () {
1184 id: ACTION_ID_BOOKMARK,
1185 urlbarIDOverride: "star-button-box",
1186 _urlbarNodeInMarkup: true,
1187 pinnedToUrlbar: true,
1188 onShowingInPanel(buttonNode) {
1189 browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
1191 onCommand(event, buttonNode) {
1192 browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode);
1199 * Gets a BrowserPageActions object in a browser window.
1202 * Either a DOM node or a browser window.
1203 * @return The BrowserPageActions object in the browser window related to the
1206 function browserPageActions(obj) {
1207 if (obj.BrowserPageActions) {
1208 return obj.BrowserPageActions;
1210 return obj.ownerGlobal.BrowserPageActions;
1214 * A generator function for all open browser windows.
1216 * @param browserWindow (DOM window, optional)
1217 * If given, then only this window will be yielded. That may sound
1218 * pointless, but it can make callers nicer to write since they don't
1219 * need two separate cases, one where a window is given and another where
1222 function* allBrowserWindows(browserWindow = null) {
1223 if (browserWindow) {
1224 yield browserWindow;
1227 yield* Services.wm.getEnumerator("navigator:browser");
1231 * A generator function for BrowserPageActions objects in all open windows.
1233 * @param browserWindow (DOM window, optional)
1234 * If given, then the BrowserPageActions for only this window will be
1237 function* allBrowserPageActions(browserWindow = null) {
1238 for (let win of allBrowserWindows(browserWindow)) {
1239 yield browserPageActions(win);
1244 * A simple function that sets properties on a given object while doing basic
1245 * required-properties checking. If a required property isn't specified in the
1246 * given options object, or if the options object has properties that aren't in
1247 * the given schema, then an error is thrown.
1250 * The object to set properties on.
1252 * An options object supplied by the consumer.
1254 * An object a property for each required and optional property. The
1255 * keys are property names; the value of a key is a bool that is true if
1256 * the property is required.
1258 function setProperties(obj, options, schema) {
1259 for (let name in schema) {
1260 let required = schema[name];
1261 if (required && !(name in options)) {
1262 throw new Error(`'${name}' must be specified`);
1264 let nameInObj = "_" + name;
1265 if (name[0] == "_") {
1266 // The property is "private". If it's defined in the options, then define
1267 // it on obj exactly as it's defined on options.
1268 if (name in options) {
1269 obj[nameInObj] = options[name];
1272 // The property is "public". Make sure the property is defined on obj.
1273 obj[nameInObj] = options[name] || null;
1276 for (let name in options) {
1277 if (!(name in schema)) {
1278 throw new Error(`Unrecognized option '${name}'`);