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 { SearchOneOffs } from "resource:///modules/SearchOneOffs.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
11 UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
12 UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
16 * The one-off search buttons in the urlbar.
18 export class UrlbarSearchOneOffs extends SearchOneOffs {
22 * @param {UrlbarView} view
23 * The parent UrlbarView.
26 super(view.panel.querySelector(".search-one-offs"));
28 this.input = view.input;
29 lazy.UrlbarPrefs.addObserver(this);
30 // Override the SearchOneOffs.sys.mjs value for the Address Bar.
31 this.disableOneOffsHorizontalKeyNavigation = true;
32 this._webEngines = [];
33 this.addEventListener("rebuild", this);
37 * Returns the local search mode one-off buttons.
40 * The local one-off buttons.
43 return this.getSelectableButtons(false).filter(b => b.source);
47 * Invoked when Web provided search engines list changes.
49 * @param {Array} engines Array of Web provided search engines. Each engine
50 * is defined as { icon, name, tooltip, uri }.
52 updateWebEngines(engines) {
53 this._webEngines = engines;
54 this.invalidateCache();
55 if (this.view.isOpen) {
61 * Enables (shows) or disables (hides) the one-offs.
63 * @param {boolean} enable
64 * True to enable, false to disable.
67 if (lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.disableOneOffs")) {
71 this.telemetryOrigin = "urlbar";
72 this.style.display = "";
73 this.textbox = this.view.input.inputField;
74 if (this.view.isOpen) {
77 this.view.controller.addQueryListener(this);
79 this.telemetryOrigin = null;
80 this.style.display = "none";
82 this.view.controller.removeQueryListener(this);
87 * Query listener method. Delegates to the superclass.
90 this._on_popupshowing();
94 onQueryStarted(queryContext) {
95 this.#queryContext = queryContext;
98 onQueryFinished(queryContext) {
99 this.#buildQuickSuggestOptIn(queryContext);
102 this.#quickSuggestOptInContainer &&
103 !this.#quickSuggestOptInContainer.hidden
105 this.#quickSuggestOptInProvider._recordGlean("impression");
109 #quickSuggestOptInContainer;
110 get #quickSuggestOptInProvider() {
111 return lazy.UrlbarProvidersManager.getProvider(
112 "UrlbarProviderQuickSuggestContextualOptIn"
116 #buildQuickSuggestOptIn(queryContext) {
117 let provider = this.#quickSuggestOptInProvider;
119 !provider._shouldDisplayContextualOptIn(queryContext) ||
120 provider.isActive(queryContext)
122 if (this.#quickSuggestOptInContainer) {
123 this.#quickSuggestOptInContainer.hidden = true;
128 if (this.#quickSuggestOptInContainer) {
129 this.#quickSuggestOptInContainer.hidden = false;
130 this.#udpateQuickSuggestOptInCopy();
134 // The following is basically a copy of what UrlbarView generates for
135 // ProviderQuickSuggestContextualOptIn's view template. Gross but good
136 // enough for the experiment. Ultimately, if we decide to keep this UI at
137 // the bottom, and when we replace the one-off buttons footer with a better
138 // UI (e.g. search button), this can become a proper result again.
139 let parser = new DOMParser();
140 let doc = parser.parseFromString(
142 <div xmlns="http://www.w3.org/1999/xhtml" class="urlbarView-quickSuggestContextualOptIn-one-off-container">
143 <div class="urlbarView-row" role="presentation" type="dynamic">
144 <span class="urlbarView-row-inner">
145 <span class="urlbarView-dynamic-quickSuggestContextualOptIn-no-wrap urlbarView-no-wrap">
146 <img class="urlbarView-dynamic-quickSuggestContextualOptIn-icon urlbarView-favicon" src="chrome://branding/content/icon32.png" />
147 <span class="urlbarView-dynamic-quickSuggestContextualOptIn-text-container">
148 <strong class="urlbarView-dynamic-quickSuggestContextualOptIn-title"></strong>
149 <span class="urlbarView-dynamic-quickSuggestContextualOptIn-description">
150 <a class="urlbarView-dynamic-quickSuggestContextualOptIn-learn_more" data-l10n-name="learn-more-link" selectable="" name="learn_more" id="urlbarView-footer-quickSuggestContextualOptIn-learn_more"></a>
155 <span primary="" name="allow" class="urlbarView-button urlbarView-button-0" role="button" data-l10n-id="urlbar-firefox-suggest-contextual-opt-in-allow" id="urlbarView-footer-quickSuggestContextualOptIn-allow"></span>
156 <span name="dismiss" class="urlbarView-button urlbarView-button-1" role="button" data-l10n-id="urlbar-firefox-suggest-contextual-opt-in-dismiss" id="urlbarView-footer-quickSuggestContextualOptIn-dismiss"></span>
162 this.#quickSuggestOptInContainer = this.document.importNode(
163 doc.body.firstElementChild,
167 // DOMParser normalizes attribute names to lowercase, so need to set this one after the fact.
168 this.#quickSuggestOptInContainer.firstElementChild.setAttribute(
170 "quickSuggestContextualOptIn"
173 this.container.appendChild(this.#quickSuggestOptInContainer);
174 this.#quickSuggestOptInContainer.addEventListener("keydown", this);
175 this.#udpateQuickSuggestOptInCopy();
178 #udpateQuickSuggestOptInCopy() {
179 let alternativeCopy = lazy.UrlbarPrefs.get(
180 "quicksuggest.contextualOptIn.sayHello"
182 this.document.l10n.setAttributes(
183 this.#quickSuggestOptInContainer.querySelector(
184 ".urlbarView-dynamic-quickSuggestContextualOptIn-title"
187 ? "urlbar-firefox-suggest-contextual-opt-in-title-2"
188 : "urlbar-firefox-suggest-contextual-opt-in-title-1"
190 this.document.l10n.setAttributes(
191 this.#quickSuggestOptInContainer.querySelector(
192 ".urlbarView-dynamic-quickSuggestContextualOptIn-description"
195 ? "urlbar-firefox-suggest-contextual-opt-in-description-2"
196 : "urlbar-firefox-suggest-contextual-opt-in-description-1"
200 #isQuickSuggestOptInElement(element) {
202 this.#quickSuggestOptInContainer &&
203 element?.compareDocumentPosition(this.#quickSuggestOptInContainer) &
204 Node.DOCUMENT_POSITION_CONTAINS
208 #handleQuickSuggestOptInCommand(element) {
209 if (this.#isQuickSuggestOptInElement(element)) {
210 this.#quickSuggestOptInProvider._handleCommand(
212 this.view.controller,
214 this.#quickSuggestOptInContainer
222 * Query listener method. Delegates to the superclass.
225 this._on_popuphidden();
230 * True if the one-offs are connected to a view.
233 // Return true if the one-offs are enabled. We set style.display = "none"
234 // when they're disabled, and we hide the container when there are no
236 return this.style.display != "none" && !this.container.hidden;
241 * True if the view is open.
244 return this.view.isOpen;
248 * The selected one-off including the search-settings button.
250 * @param {DOMElement|null} button
251 * The selected one-off button. Null if no one-off is selected.
253 set selectedButton(button) {
254 if (this.selectedButton == button) {
258 if (this.#isQuickSuggestOptInElement(button)) {
259 this.#quickSuggestOptInProvider.onBeforeSelection(null, button);
262 super.selectedButton = button;
264 let expectedSearchMode;
265 if (button && button != this.view.oneOffSearchButtons.settingsButton) {
266 expectedSearchMode = {
267 engineName: button.engine?.name,
268 source: button.source,
271 this.input.searchMode = expectedSearchMode;
272 } else if (this.input.searchMode) {
273 // Restore the previous state. We do this only if we're in search mode, as
274 // an optimization in the common case of cycling through normal results.
275 this.input.restoreSearchModeState();
279 get selectedButton() {
280 return super.selectedButton;
283 getSelectableButtons(aIncludeNonEngineButtons) {
284 const buttons = super.getSelectableButtons(aIncludeNonEngineButtons);
287 aIncludeNonEngineButtons &&
288 this.#quickSuggestOptInContainer &&
289 !this.#quickSuggestOptInContainer.hidden
292 ...this.#quickSuggestOptInContainer.querySelectorAll(
293 "[role=button], [selectable]"
302 * The selected index in the view or -1 if there is no selection.
306 get selectedViewIndex() {
307 return this.view.selectedRowIndex;
309 set selectedViewIndex(val) {
310 this.view.selectedRowIndex = val;
323 * Called when a one-off is clicked.
325 * @param {event} event
326 * The event that triggered the pick.
327 * @param {object} searchMode
328 * Used by UrlbarInput.setSearchMode to enter search mode. See setSearchMode
329 * documentation for details.
331 handleSearchCommand(event, searchMode) {
332 // The settings button and adding engines are a special case and executed
335 this.selectedButton == this.view.oneOffSearchButtons.settingsButton ||
336 this.selectedButton.classList.contains(
337 "searchbar-engine-one-off-add-engine"
340 this.input.controller.engagementEvent.discard();
341 this.selectedButton.doCommand();
342 this.selectedButton = null;
346 if (this.#handleQuickSuggestOptInCommand(this.selectedButton)) {
347 this.input.controller.engagementEvent.discard();
348 this.selectedButton = null;
352 // We allow autofill in local but not remote search modes.
353 let startQueryParams = {
355 !searchMode.engineName &&
356 searchMode.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
360 let userTypedSearchString =
361 this.input.value && this.input.getAttribute("pageproxystate") != "valid";
362 let engine = Services.search.getEngineByName(searchMode.engineName);
364 let { where, params } = this._whereToOpen(event);
366 // Some key combinations should execute a search immediately. We handle
367 // these here, outside the switch statement.
369 userTypedSearchString &&
371 (event.shiftKey || where != "current")
373 this.input.handleNavigation({
378 engine: this.selectedButton.engine,
381 this.selectedButton = null;
385 // Handle opening search mode in either the current tab or in a new tab.
388 this.input.searchMode = searchMode;
389 this.input.startQuery(startQueryParams);
393 // We set this.selectedButton when switching tabs. If we entered search
394 // mode preview here, it could be cleared when this.selectedButton calls
396 searchMode.isPreview = false;
398 let newTab = this.input.window.gBrowser.addTrustedTab("about:newtab");
399 this.input.setSearchMode(searchMode, newTab.linkedBrowser);
400 if (userTypedSearchString) {
401 // Set the search string for the new tab.
402 newTab.linkedBrowser.userTypedValue = this.input.value;
404 if (!params?.inBackground) {
405 this.input.window.gBrowser.selectedTab = newTab;
406 newTab.ownerGlobal.gURLBar.startQuery(startQueryParams);
411 this.input.searchMode = searchMode;
412 this.input.startQuery(startQueryParams);
418 this.selectedButton = null;
422 * Sets the tooltip for a one-off button with an engine. This should set
423 * either the `tooltiptext` attribute or the relevant l10n ID.
425 * @param {element} button
426 * The one-off button.
428 setTooltipForEngineButton(button) {
429 let aliases = button.engine.aliases;
430 if (!aliases.length) {
431 super.setTooltipForEngineButton(button);
434 this.document.l10n.setAttributes(
436 "search-one-offs-engine-with-alias",
438 engineName: button.engine.name,
445 * Overrides the willHide method in the superclass to account for the local
446 * search mode buttons.
449 * True if we will hide the one-offs when they are requested.
452 // We need to call super.willHide() even when we return false below because
453 // it has the necessary side effect of creating this._engineInfo.
454 let superWillHide = await super.willHide();
456 lazy.UrlbarUtils.LOCAL_SEARCH_MODES.some(m =>
457 lazy.UrlbarPrefs.get(m.pref)
462 return superWillHide;
466 * Called when a pref tracked by UrlbarPrefs changes.
468 * @param {string} changedPref
469 * The name of the pref, relative to `browser.urlbar.` if the pref is in
472 onPrefChanged(changedPref) {
473 // Invalidate the engine cache when the local-one-offs-related prefs change
474 // so that the one-offs rebuild themselves the next time the view opens.
476 [...lazy.UrlbarUtils.LOCAL_SEARCH_MODES.map(m => m.pref)].includes(
480 this.invalidateCache();
485 * Overrides _getAddEngines to return engines that can be added.
487 * @returns {Array} engines
490 return this._webEngines;
494 * Overrides _rebuildEngineList to add the local one-offs.
496 * @param {Array} engines
497 * The search engines to add.
498 * @param {Array} addEngines
499 * The engines that can be added.
501 async _rebuildEngineList(engines, addEngines) {
502 await super._rebuildEngineList(engines, addEngines);
504 for (let { source, pref, restrict } of lazy.UrlbarUtils
505 .LOCAL_SEARCH_MODES) {
506 if (!lazy.UrlbarPrefs.get(pref)) {
509 let name = lazy.UrlbarUtils.getResultSourceName(source);
510 let button = this.document.createXULElement("button");
511 button.id = `urlbar-engine-one-off-item-${name}`;
512 button.setAttribute("class", "searchbar-engine-one-off-item");
513 button.setAttribute("tabindex", "-1");
514 this.document.l10n.setAttributes(button, `search-one-offs-${name}`, {
517 button.source = source;
518 this.buttons.appendChild(button);
523 * Overrides the superclass's click listener to handle clicks on local
524 * one-offs in addition to engine one-offs.
526 * @param {event} event
530 // Ignore right clicks.
531 if (event.button == 2) {
535 let button = event.originalTarget;
537 if (this.#handleQuickSuggestOptInCommand(button)) {
541 if (!button.engine && !button.source) {
545 this.selectedButton = button;
546 this.handleSearchCommand(event, {
547 engineName: button.engine?.name,
548 source: button.source,
554 * Overrides the superclass's contextmenu listener to handle the context menu.
556 * @param {event} event
557 * The contextmenu event.
559 _on_contextmenu(event) {
560 // Prevent the context menu from appearing.
561 event.preventDefault();
565 if (this.#queryContext) {
566 this.#buildQuickSuggestOptIn(this.#queryContext);