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