ext-browserAction.js (6146B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", 11 ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", 12 }); 13 14 const { BrowserActionBase } = ChromeUtils.importESModule( 15 "resource://gre/modules/ExtensionActions.sys.mjs" 16 ); 17 18 const BROWSER_ACTION_PROPERTIES = [ 19 "title", 20 "icon", 21 "popup", 22 "badgeText", 23 "badgeBackgroundColor", 24 "badgeTextColor", 25 "enabled", 26 "patternMatching", 27 ]; 28 29 class BrowserAction extends BrowserActionBase { 30 constructor(extension, clickDelegate) { 31 const tabContext = new TabContext(() => this.getContextData(null)); 32 super(tabContext, extension); 33 this.clickDelegate = clickDelegate; 34 this.helper = new ExtensionActionHelper({ 35 extension, 36 tabTracker, 37 windowTracker, 38 tabContext, 39 properties: BROWSER_ACTION_PROPERTIES, 40 }); 41 } 42 43 updateOnChange(tab) { 44 const tabId = tab ? tab.id : null; 45 const action = tab 46 ? this.getContextData(tab) 47 : this.helper.extractProperties(this.globals); 48 this.helper.sendRequest(tabId, { 49 action, 50 type: "GeckoView:BrowserAction:Update", 51 }); 52 } 53 54 openPopup(tab, openPopupWithoutUserInteraction = false) { 55 const popupUri = openPopupWithoutUserInteraction 56 ? this.getPopupUrl(tab) 57 : this.triggerClickOrPopup(tab); 58 const actionObject = this.getContextData(tab); 59 const action = this.helper.extractProperties(actionObject); 60 this.helper.sendRequest(tab.id, { 61 action, 62 type: "GeckoView:BrowserAction:OpenPopup", 63 popupUri, 64 }); 65 } 66 67 triggerClickOrPopup(tab = tabTracker.activeTab) { 68 return super.triggerClickOrPopup(tab); 69 } 70 71 getTab(tabId) { 72 return this.helper.getTab(tabId); 73 } 74 75 getWindow(windowId) { 76 return this.helper.getWindow(windowId); 77 } 78 79 dispatchClick() { 80 this.clickDelegate.onClick(); 81 } 82 } 83 84 this.browserAction = class extends ExtensionAPIPersistent { 85 static for(extension) { 86 return GeckoViewWebExtension.browserActions.get(extension); 87 } 88 89 async onManifestEntry() { 90 const { extension } = this; 91 this.action = new BrowserAction(extension, this); 92 await this.action.loadIconData(); 93 94 GeckoViewWebExtension.browserActions.set(extension, this.action); 95 96 // Notify the embedder of this action 97 this.action.updateOnChange(null); 98 } 99 100 onShutdown() { 101 const { extension } = this; 102 this.action.onShutdown(); 103 GeckoViewWebExtension.browserActions.delete(extension); 104 } 105 106 onClick() { 107 this.emit("click", tabTracker.activeTab); 108 } 109 110 PERSISTENT_EVENTS = { 111 onClicked({ fire }) { 112 const { extension } = this; 113 const { tabManager } = extension; 114 async function listener(_event, tab) { 115 if (fire.wakeup) { 116 await fire.wakeup(); 117 } 118 // TODO: we should double-check if the tab is already being closed by the time 119 // the background script got started and we converted the primed listener. 120 fire.sync(tabManager.convert(tab)); 121 } 122 this.on("click", listener); 123 return { 124 unregister: () => { 125 this.off("click", listener); 126 }, 127 convert(newFire) { 128 fire = newFire; 129 }, 130 }; 131 }, 132 onUserSettingsChanged() { 133 // isOnToolBar is not supported, so this event will never fire. 134 // We stub out an implementation to avoid breaking extensions 135 // that are compatible with desktop and mobile browsers 136 return { 137 unregister: () => {}, 138 convert() {}, 139 }; 140 }, 141 }; 142 143 getAPI(context) { 144 const { extension } = context; 145 const { action } = this; 146 const namespace = 147 extension.manifestVersion < 3 ? "browserAction" : "action"; 148 149 return { 150 [namespace]: { 151 ...action.api(context), 152 153 onClicked: new EventManager({ 154 context, 155 // module name is "browserAction" because it the name used in the 156 // ext-android.json, independently from the manifest version. 157 module: "browserAction", 158 event: "onClicked", 159 inputHandling: true, 160 extensionApi: this, 161 }).api(), 162 163 onUserSettingsChanged: new EventManager({ 164 context, 165 // module name is "browserAction" because it the name used in the 166 // ext-android.json, independently from the manifest version. 167 module: "browserAction", 168 event: "onUserSettingsChanged", 169 extensionApi: this, 170 }).api(), 171 172 getUserSettings: () => { 173 return { 174 // isOnToolbar is not supported on Android. 175 // We intentionally omit the property, in case 176 // extensions would like to feature-detect support 177 // for this feature. 178 }; 179 }, 180 openPopup: options => { 181 const isHandlingUserInput = 182 context.callContextData?.isHandlingUserInput; 183 184 if ( 185 !Services.prefs.getBoolPref( 186 "extensions.openPopupWithoutUserGesture.enabled" 187 ) && 188 !isHandlingUserInput 189 ) { 190 throw new ExtensionError("openPopup requires a user gesture"); 191 } 192 193 const currentWindow = windowTracker.getCurrentWindow(context); 194 195 const window = 196 typeof options?.windowId === "number" 197 ? windowTracker.getWindow(options.windowId, context) 198 : currentWindow; 199 200 if (window !== currentWindow) { 201 throw new ExtensionError( 202 "Only the current window is supported on Android." 203 ); 204 } 205 206 if (this.action.getPopupUrl(window.tab, true)) { 207 action.openPopup(window.tab, !isHandlingUserInput); 208 } 209 }, 210 }, 211 }; 212 } 213 }; 214 215 global.browserActionFor = this.browserAction.for;