ActionsProviderQuickActions.sys.mjs (5876B)
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/. */ 4 5 import { 6 ActionsProvider, 7 ActionsResult, 8 } from "moz-src:///browser/components/urlbar/ActionsProvider.sys.mjs"; 9 10 const lazy = {}; 11 ChromeUtils.defineESModuleGetters(lazy, { 12 QuickActionsLoaderDefault: 13 "moz-src:///browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs", 14 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 15 }); 16 17 // These prefs are relative to the `browser.urlbar` branch. 18 const ENABLED_PREF = "suggest.quickactions"; 19 const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase"; 20 const MIN_SEARCH_PREF = "quickactions.minimumSearchString"; 21 22 /** 23 * @typedef QuickActionsDefinition 24 * @property {string[]} commands 25 * The possible typed entries that this command will be displayed for. 26 * @property {string} icon 27 * The URI of the icon associated with this command. 28 * @property {string} label 29 * The id of the label for the result element. 30 * @property {() => boolean} [isVisible] 31 * A function to call to check if this action should be visible or not. 32 * @property {() => null|{focusContent: boolean}} onPick 33 * The function to call when the quick action is picked. It may return an object 34 * with property focusContent to indicate if the content area should be focussed 35 * after the pick. 36 */ 37 38 /** 39 * A provider that matches the urlbar input to built in actions. 40 */ 41 class ProviderQuickActions extends ActionsProvider { 42 get name() { 43 return "ActionsProviderQuickActions"; 44 } 45 46 isActive(queryContext) { 47 return ( 48 queryContext.sapName == "urlbar" && 49 lazy.UrlbarPrefs.get(ENABLED_PREF) && 50 !queryContext.searchMode && 51 queryContext.trimmedSearchString.length < 50 && 52 queryContext.trimmedSearchString.length >= 53 lazy.UrlbarPrefs.get(MIN_SEARCH_PREF) 54 ); 55 } 56 57 async queryActions(queryContext) { 58 let input = queryContext.trimmedLowerCaseSearchString; 59 let results = await this.getActions({ input }); 60 61 if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) { 62 for (let [keyword, keys] of this.#keywords) { 63 if (input.includes(keyword) && keys.length) { 64 keys.forEach(key => results.add(key)); 65 } 66 } 67 } 68 69 // Remove invisible actions. 70 results.forEach(key => { 71 const action = this.#actions.get(key); 72 if (!(action.isVisible?.() ?? true)) { 73 results.delete(key); 74 } 75 }); 76 77 if (!results.size) { 78 return null; 79 } 80 81 return [...results].map(key => { 82 let action = this.#actions.get(key); 83 return new ActionsResult({ 84 key, 85 l10nId: action.label, 86 icon: action.icon, 87 dataset: { 88 action: key, 89 inputLength: queryContext.trimmedSearchString.length, 90 }, 91 onPick: action.onPick, 92 }); 93 }); 94 } 95 96 async getActions({ input, includesExactMatch = false }) { 97 await lazy.QuickActionsLoaderDefault.ensureLoaded(); 98 99 let results = this.#prefixes.get(input) ?? new Set(); 100 101 if (includesExactMatch) { 102 let actions = this.#keywords.get(input); 103 actions?.forEach(action => results.add(action)); 104 } 105 106 return results; 107 } 108 109 getAction(key) { 110 return this.#actions.get(key); 111 } 112 113 pickAction(_queryContext, _controller, element) { 114 let action = element.dataset.action; 115 let inputLength = Math.min(element.dataset.inputLength, 10); 116 Glean.urlbarQuickaction.picked[`${action}-${inputLength}`].add(1); 117 let options = this.#actions.get(action).onPick(); 118 if (options?.focusContent) { 119 element.ownerGlobal.gBrowser.selectedBrowser.focus(); 120 } 121 } 122 123 /** 124 * Adds a new QuickAction. 125 * 126 * @param {string} key A key to identify this action. 127 * @param {QuickActionsDefinition} definition An object that describes the action. 128 */ 129 addAction(key, definition) { 130 this.#actions.set(key, definition); 131 definition.commands.forEach(cmd => { 132 let keys = this.#keywords.get(cmd) ?? []; 133 keys.push(key); 134 this.#keywords.set(cmd, keys); 135 }); 136 this.#loopOverPrefixes(definition.commands, prefix => { 137 let result = this.#prefixes.get(prefix); 138 if (result) { 139 result.add(key); 140 } else { 141 result = new Set([key]); 142 } 143 this.#prefixes.set(prefix, result); 144 }); 145 } 146 147 /** 148 * Removes an action. 149 * 150 * @param {string} key A key to identify this action. 151 */ 152 removeAction(key) { 153 let definition = this.#actions.get(key); 154 this.#actions.delete(key); 155 definition.commands.forEach(cmd => { 156 let keys = this.#keywords.get(cmd) ?? []; 157 this.#keywords.set( 158 cmd, 159 keys.filter(k => k != key) 160 ); 161 }); 162 this.#loopOverPrefixes(definition.commands, prefix => { 163 let result = this.#prefixes.get(prefix); 164 if (result) { 165 result.delete(key); 166 } 167 this.#prefixes.set(prefix, result); 168 }); 169 } 170 171 /** 172 * A map from keywords to an action. 173 * 174 * @type {Map<string, Array>} 175 */ 176 #keywords = new Map(); 177 178 /** 179 * A map of all prefixes to an array of actions. 180 * 181 * @type {Map<string, Set>} 182 */ 183 #prefixes = new Map(); 184 185 /** 186 * The actions that have been added. 187 * 188 * @type {Map<string, QuickActionsDefinition>} 189 */ 190 #actions = new Map(); 191 192 #loopOverPrefixes(commands, fun) { 193 for (const command of commands) { 194 // Loop over all the prefixes of the word, ie 195 // "", "w", "wo", "wor", stopping just before the full 196 // word itself which will be matched by the whole 197 // phrase matching. 198 for (let i = 1; i <= command.length; i++) { 199 let prefix = command.substring(0, command.length - i); 200 fun(prefix); 201 } 202 } 203 } 204 } 205 206 export var ActionsProviderQuickActions = new ProviderQuickActions();