UrlbarProviderTabToSearch.sys.mjs (12603B)
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 /** 6 * This module exports a provider that offers a search engine when the user is 7 * typing a search engine domain. 8 */ 9 10 import { 11 UrlbarProvider, 12 UrlbarUtils, 13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 14 15 const lazy = {}; 16 17 ChromeUtils.defineESModuleGetters(lazy, { 18 ActionsProviderContextualSearch: 19 "moz-src:///browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs", 20 UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", 21 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 22 UrlbarProviderAutofill: 23 "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs", 24 UrlbarProviderGlobalActions: 25 "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs", 26 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 27 UrlbarSearchUtils: 28 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 29 UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", 30 }); 31 32 const DYNAMIC_RESULT_TYPE = "onboardTabToSearch"; 33 const VIEW_TEMPLATE = { 34 attributes: { 35 selectable: true, 36 }, 37 children: [ 38 { 39 name: "no-wrap", 40 tag: "span", 41 classList: ["urlbarView-no-wrap"], 42 children: [ 43 { 44 name: "icon", 45 tag: "img", 46 classList: ["urlbarView-favicon"], 47 }, 48 { 49 name: "text-container", 50 tag: "span", 51 children: [ 52 { 53 name: "first-row-container", 54 tag: "span", 55 children: [ 56 { 57 name: "title", 58 tag: "span", 59 classList: ["urlbarView-title"], 60 children: [ 61 { 62 name: "titleStrong", 63 tag: "strong", 64 }, 65 ], 66 }, 67 { 68 name: "title-separator", 69 tag: "span", 70 classList: ["urlbarView-title-separator"], 71 }, 72 { 73 name: "action", 74 tag: "span", 75 classList: ["urlbarView-action"], 76 attributes: { 77 "slide-in": true, 78 }, 79 }, 80 ], 81 }, 82 { 83 name: "description", 84 tag: "span", 85 }, 86 ], 87 }, 88 ], 89 }, 90 ], 91 }; 92 93 /** 94 * Initializes this provider's dynamic result. To be called after the creation 95 * of the provider singleton. 96 */ 97 function initializeDynamicResult() { 98 lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE); 99 lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE); 100 } 101 102 /** 103 * Class used to create the provider. 104 */ 105 export class UrlbarProviderTabToSearch extends UrlbarProvider { 106 static onboardingInteractionAtTime = null; 107 108 constructor() { 109 super(); 110 } 111 112 /** 113 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 114 */ 115 get type() { 116 return UrlbarUtils.PROVIDER_TYPE.PROFILE; 117 } 118 119 /** 120 * Whether this provider should be invoked for the given context. 121 * If this method returns false, the providers manager won't start a query 122 * with this provider, to save on resources. 123 * 124 * @param {UrlbarQueryContext} queryContext The query context object 125 */ 126 async isActive(queryContext) { 127 return ( 128 queryContext.searchString && 129 queryContext.tokens.length == 1 && 130 !queryContext.searchMode && 131 lazy.UrlbarPrefs.get("suggest.engines") && 132 !( 133 (await this.queryInstance 134 .getProvider(lazy.UrlbarProviderGlobalActions.name) 135 ?.isActive(queryContext)) && 136 lazy.ActionsProviderContextualSearch.isActive(queryContext) 137 ) 138 ); 139 } 140 141 /** 142 * Gets the provider's priority. 143 * 144 * @returns {number} The provider's priority for the given query. 145 */ 146 getPriority() { 147 return 0; 148 } 149 150 /** 151 * This is called only for dynamic result types, when the urlbar view updates 152 * the view of one of the results of the provider. It should return an object 153 * describing the view update. 154 * 155 * @param {UrlbarResult} result The result whose view will be updated. 156 * @returns {object} An object describing the view update. 157 */ 158 getViewUpdate(result) { 159 return { 160 icon: { 161 attributes: { 162 src: result.payload.icon, 163 }, 164 }, 165 titleStrong: { 166 l10n: { 167 id: "urlbar-result-action-search-w-engine", 168 args: { 169 engine: result.payload.engine, 170 }, 171 }, 172 }, 173 action: { 174 l10n: { 175 id: result.payload.isGeneralPurposeEngine 176 ? "urlbar-result-action-tabtosearch-web" 177 : "urlbar-result-action-tabtosearch-other-engine", 178 args: { 179 engine: result.payload.engine, 180 }, 181 }, 182 }, 183 description: { 184 l10n: { 185 id: "urlbar-tabtosearch-onboard", 186 }, 187 }, 188 }; 189 } 190 191 /** 192 * Called when a result from the provider is selected. "Selected" refers to 193 * the user highlighing the result with the arrow keys/Tab, before it is 194 * picked. onSelection is also called when a user clicks a result. In the 195 * event of a click, onSelection is called just before onEngagement. 196 * 197 * @param {UrlbarResult} result 198 * The result that was selected. 199 */ 200 onSelection(result) { 201 // We keep track of the number of times the user interacts with 202 // tab-to-search onboarding results so we stop showing them after 203 // `tabToSearch.onboard.interactionsLeft` interactions. 204 // Also do not increment the counter if the result was interacted with less 205 // than 5 minutes ago. This is a guard against the user running up the 206 // counter by interacting with the same result repeatedly. 207 if ( 208 result.payload.dynamicType && 209 (!UrlbarProviderTabToSearch.onboardingInteractionAtTime || 210 UrlbarProviderTabToSearch.onboardingInteractionAtTime < 211 Date.now() - 1000 * 60 * 5) 212 ) { 213 let interactionsLeft = lazy.UrlbarPrefs.get( 214 "tabToSearch.onboard.interactionsLeft" 215 ); 216 217 if (interactionsLeft > 0) { 218 lazy.UrlbarPrefs.set( 219 "tabToSearch.onboard.interactionsLeft", 220 --interactionsLeft 221 ); 222 } 223 224 UrlbarProviderTabToSearch.onboardingInteractionAtTime = Date.now(); 225 } 226 } 227 228 onEngagement(queryContext, controller, details) { 229 let { result, element } = details; 230 if (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) { 231 // Confirm search mode, but only for the onboarding (dynamic) result. The 232 // input will handle confirming search mode for the non-onboarding 233 // `RESULT_TYPE.SEARCH` result since it sets `providesSearchMode`. 234 element.ownerGlobal.gURLBar.maybeConfirmSearchModeFromResult({ 235 result, 236 checkValue: false, 237 }); 238 } 239 } 240 241 /** 242 * Defines whether the view should defer user selection events while waiting 243 * for the first result from this provider. 244 * 245 * @returns {boolean} Whether the provider wants to defer user selection 246 * events. 247 */ 248 get deferUserSelection() { 249 return true; 250 } 251 252 /** 253 * Starts querying. 254 * 255 * @param {UrlbarQueryContext} queryContext 256 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 257 * Callback invoked by the provider to add a new result. 258 */ 259 async startQuery(queryContext, addCallback) { 260 // enginesForDomainPrefix only matches against engine domains. 261 // Remove trailing slashes and www. from the search string and check if the 262 // resulting string is worth matching. 263 let [searchStr] = UrlbarUtils.stripPrefixAndTrim( 264 queryContext.searchString, 265 { 266 stripWww: true, 267 trimSlash: true, 268 } 269 ); 270 // Skip any string that cannot be an origin. 271 if ( 272 !lazy.UrlUtils.looksLikeOrigin(searchStr, { 273 ignoreKnownDomains: true, 274 noIp: true, 275 }) 276 ) { 277 return; 278 } 279 280 // Also remove the public suffix, if present, to allow for partial matches. 281 if (searchStr.includes(".")) { 282 searchStr = UrlbarUtils.stripPublicSuffixFromHost(searchStr); 283 } 284 285 // Add all matching engines. 286 let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix( 287 searchStr, 288 { 289 matchAllDomainLevels: true, 290 } 291 ); 292 if (!engines.length) { 293 return; 294 } 295 296 const onboardingInteractionsLeft = lazy.UrlbarPrefs.get( 297 "tabToSearch.onboard.interactionsLeft" 298 ); 299 300 // If the engine host begins with the search string, autofill may happen 301 // for it, and the Muxer will retain the result only if there's a matching 302 // autofill heuristic result. 303 // Otherwise, we may have a partial match, where the search string is at 304 // the boundary of a host part, for example "wiki" in "en.wikipedia.org". 305 // We put those engines apart, and later we check if their host satisfies 306 // the autofill threshold. If they do, we mark them with the 307 // "satisfiesAutofillThreshold" payload property, so the muxer can avoid 308 // filtering them out. 309 let partialMatchEnginesByHost = new Map(); 310 311 for (let engine of engines) { 312 // Trim the engine host. This will also be set as the result url, so the 313 // Muxer can use it to filter. 314 let [host] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, { 315 stripWww: true, 316 }); 317 // Check if the host may be autofilled. 318 if (host.startsWith(searchStr.toLocaleLowerCase())) { 319 if (onboardingInteractionsLeft > 0) { 320 addCallback(this, makeOnboardingResult(engine)); 321 } else { 322 addCallback(this, makeResult(queryContext, engine)); 323 } 324 continue; 325 } 326 327 // Otherwise it may be a partial match that would not be autofilled. 328 if (host.includes("." + searchStr.toLocaleLowerCase())) { 329 partialMatchEnginesByHost.set(engine.searchUrlDomain, engine); 330 // Don't continue here, we are looking for more partial matches. 331 } 332 // We also try to match the base domain of the searchUrlDomain, 333 // because otherwise for an engine like rakuten, we'd check pt.afl.rakuten.co.jp 334 // which redirects and is thus not saved in the history resulting in a low score. 335 336 let baseDomain = Services.eTLD.getBaseDomainFromHost( 337 engine.searchUrlDomain 338 ); 339 if (baseDomain.startsWith(searchStr)) { 340 partialMatchEnginesByHost.set(baseDomain, engine); 341 } 342 } 343 if (partialMatchEnginesByHost.size) { 344 let host = await lazy.UrlbarProviderAutofill.getTopHostOverThreshold( 345 queryContext, 346 Array.from(partialMatchEnginesByHost.keys()) 347 ); 348 if (host) { 349 let engine = partialMatchEnginesByHost.get(host); 350 if (onboardingInteractionsLeft > 0) { 351 addCallback(this, makeOnboardingResult(engine, true)); 352 } else { 353 addCallback(this, makeResult(queryContext, engine, true)); 354 } 355 } 356 } 357 } 358 } 359 360 function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) { 361 return new lazy.UrlbarResult({ 362 type: UrlbarUtils.RESULT_TYPE.DYNAMIC, 363 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 364 resultSpan: 2, 365 suggestedIndex: 1, 366 payload: { 367 engine: engine.name, 368 searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine), 369 providesSearchMode: true, 370 icon: UrlbarUtils.ICON.SEARCH_GLASS, 371 dynamicType: DYNAMIC_RESULT_TYPE, 372 satisfiesAutofillThreshold, 373 }, 374 }); 375 } 376 377 function makeResult(context, engine, satisfiesAutofillThreshold = false) { 378 return new lazy.UrlbarResult({ 379 type: UrlbarUtils.RESULT_TYPE.SEARCH, 380 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 381 suggestedIndex: 1, 382 payload: { 383 engine: engine.name, 384 isGeneralPurposeEngine: engine.isGeneralPurposeEngine, 385 searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine), 386 providesSearchMode: true, 387 icon: UrlbarUtils.ICON.SEARCH_GLASS, 388 query: "", 389 satisfiesAutofillThreshold, 390 }, 391 }); 392 } 393 394 function searchUrlDomainWithoutSuffix(engine) { 395 let [value] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, { 396 stripWww: true, 397 }); 398 return value.substr(0, value.length - engine.searchUrlPublicSuffix.length); 399 } 400 401 initializeDynamicResult();