From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../components/urlbar/UrlbarSearchOneOffs.sys.mjs | 566 +++++++++++++++++++++ 1 file changed, 566 insertions(+) create mode 100644 browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs (limited to 'browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs') diff --git a/browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs b/browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs new file mode 100644 index 0000000000..cdbb3aea53 --- /dev/null +++ b/browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs @@ -0,0 +1,566 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { SearchOneOffs } from "resource:///modules/SearchOneOffs.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +/** + * The one-off search buttons in the urlbar. + */ +export class UrlbarSearchOneOffs extends SearchOneOffs { + /** + * Constructor. + * + * @param {UrlbarView} view + * The parent UrlbarView. + */ + constructor(view) { + super(view.panel.querySelector(".search-one-offs")); + this.view = view; + this.input = view.input; + lazy.UrlbarPrefs.addObserver(this); + // Override the SearchOneOffs.jsm value for the Address Bar. + this.disableOneOffsHorizontalKeyNavigation = true; + this._webEngines = []; + this.addEventListener("rebuild", this); + } + + /** + * Returns the local search mode one-off buttons. + * + * @returns {Array} + * The local one-off buttons. + */ + get localButtons() { + return this.getSelectableButtons(false).filter(b => b.source); + } + + /** + * Invoked when Web provided search engines list changes. + * + * @param {Array} engines Array of Web provided search engines. Each engine + * is defined as { icon, name, tooltip, uri }. + */ + updateWebEngines(engines) { + this._webEngines = engines; + this.invalidateCache(); + if (this.view.isOpen) { + this._rebuild(); + } + } + + /** + * Enables (shows) or disables (hides) the one-offs. + * + * @param {boolean} enable + * True to enable, false to disable. + */ + enable(enable) { + if (enable) { + this.telemetryOrigin = "urlbar"; + this.style.display = ""; + this.textbox = this.view.input.inputField; + if (this.view.isOpen) { + this._rebuild(); + } + this.view.controller.addQueryListener(this); + } else { + this.telemetryOrigin = null; + this.style.display = "none"; + this.textbox = null; + this.view.controller.removeQueryListener(this); + } + } + + /** + * Query listener method. Delegates to the superclass. + */ + onViewOpen() { + this._on_popupshowing(); + } + + #queryContext; + onQueryStarted(queryContext) { + this.#queryContext = queryContext; + } + + onQueryFinished(queryContext) { + this.#buildQuickSuggestOptIn(queryContext); + + if ( + this.#quickSuggestOptInContainer && + !this.#quickSuggestOptInContainer.hidden + ) { + this.#quickSuggestOptInProvider._recordGlean("impression"); + } + } + + #quickSuggestOptInContainer; + get #quickSuggestOptInProvider() { + return lazy.UrlbarProvidersManager.getProvider( + "UrlbarProviderQuickSuggestContextualOptIn" + ); + } + + #buildQuickSuggestOptIn(queryContext) { + let provider = this.#quickSuggestOptInProvider; + if ( + !provider._shouldDisplayContextualOptIn(queryContext) || + provider.isActive(queryContext) + ) { + if (this.#quickSuggestOptInContainer) { + this.#quickSuggestOptInContainer.hidden = true; + } + return; + } + + if (this.#quickSuggestOptInContainer) { + this.#quickSuggestOptInContainer.hidden = false; + this.#udpateQuickSuggestOptInCopy(); + return; + } + + // The following is basically a copy of what UrlbarView generates for + // ProviderQuickSuggestContextualOptIn's view template. Gross but good + // enough for the experiment. Ultimately, if we decide to keep this UI at + // the bottom, and when we replace the one-off buttons footer with a better + // UI (e.g. search button), this can become a proper result again. + let parser = new DOMParser(); + let doc = parser.parseFromString( + ` +
+ +
+ `, + "text/html" + ); + this.#quickSuggestOptInContainer = this.document.importNode( + doc.body.firstElementChild, + true + ); + + // DOMParser normalizes attribute names to lowercase, so need to set this one after the fact. + this.#quickSuggestOptInContainer.firstElementChild.setAttribute( + "dynamicType", + "quickSuggestContextualOptIn" + ); + + this.container.appendChild(this.#quickSuggestOptInContainer); + this.#quickSuggestOptInContainer.addEventListener("keydown", this); + this.#udpateQuickSuggestOptInCopy(); + } + + #udpateQuickSuggestOptInCopy() { + let alternativeCopy = lazy.UrlbarPrefs.get( + "quicksuggest.contextualOptIn.sayHello" + ); + this.document.l10n.setAttributes( + this.#quickSuggestOptInContainer.querySelector( + ".urlbarView-dynamic-quickSuggestContextualOptIn-title" + ), + alternativeCopy + ? "urlbar-firefox-suggest-contextual-opt-in-title-2" + : "urlbar-firefox-suggest-contextual-opt-in-title-1" + ); + this.document.l10n.setAttributes( + this.#quickSuggestOptInContainer.querySelector( + ".urlbarView-dynamic-quickSuggestContextualOptIn-description" + ), + alternativeCopy + ? "urlbar-firefox-suggest-contextual-opt-in-description-2" + : "urlbar-firefox-suggest-contextual-opt-in-description-1" + ); + } + + #isQuickSuggestOptInElement(element) { + return ( + this.#quickSuggestOptInContainer && + element?.compareDocumentPosition(this.#quickSuggestOptInContainer) & + Node.DOCUMENT_POSITION_CONTAINS + ); + } + + #handleQuickSuggestOptInCommand(element) { + if (this.#isQuickSuggestOptInElement(element)) { + this.#quickSuggestOptInProvider._handleCommand( + element, + this.view.controller, + null, + this.#quickSuggestOptInContainer + ); + return true; + } + return false; + } + + /** + * Query listener method. Delegates to the superclass. + */ + onViewClose() { + this._on_popuphidden(); + } + + /** + * @returns {boolean} + * True if the one-offs are connected to a view. + */ + get hasView() { + // Return true if the one-offs are enabled. We set style.display = "none" + // when they're disabled, and we hide the container when there are no + // engines to show. + return this.style.display != "none" && !this.container.hidden; + } + + /** + * @returns {boolean} + * True if the view is open. + */ + get isViewOpen() { + return this.view.isOpen; + } + + /** + * The selected one-off including the search-settings button. + * + * @param {DOMElement|null} button + * The selected one-off button. Null if no one-off is selected. + */ + set selectedButton(button) { + if (this.selectedButton == button) { + return; + } + + if (this.#isQuickSuggestOptInElement(button)) { + this.#quickSuggestOptInProvider.onBeforeSelection(null, button); + } + + super.selectedButton = button; + + let expectedSearchMode; + if (button && button != this.view.oneOffSearchButtons.settingsButton) { + expectedSearchMode = { + engineName: button.engine?.name, + source: button.source, + entry: "oneoff", + }; + this.input.searchMode = expectedSearchMode; + } else if (this.input.searchMode) { + // Restore the previous state. We do this only if we're in search mode, as + // an optimization in the common case of cycling through normal results. + this.input.restoreSearchModeState(); + } + } + + get selectedButton() { + return super.selectedButton; + } + + getSelectableButtons(aIncludeNonEngineButtons) { + const buttons = super.getSelectableButtons(aIncludeNonEngineButtons); + + if ( + aIncludeNonEngineButtons && + this.#quickSuggestOptInContainer && + !this.#quickSuggestOptInContainer.hidden + ) { + buttons.push( + ...this.#quickSuggestOptInContainer.querySelectorAll( + "[role=button], [selectable]" + ) + ); + } + + return buttons; + } + + /** + * The selected index in the view or -1 if there is no selection. + * + * @returns {number} + */ + get selectedViewIndex() { + return this.view.selectedRowIndex; + } + set selectedViewIndex(val) { + this.view.selectedRowIndex = val; + } + + /** + * Closes the view. + */ + closeView() { + if (this.view) { + this.view.close(); + } + } + + /** + * Called when a one-off is clicked. + * + * @param {event} event + * The event that triggered the pick. + * @param {object} searchMode + * Used by UrlbarInput.setSearchMode to enter search mode. See setSearchMode + * documentation for details. + */ + handleSearchCommand(event, searchMode) { + // The settings button and adding engines are a special case and executed + // immediately. + if ( + this.selectedButton == this.view.oneOffSearchButtons.settingsButton || + this.selectedButton.classList.contains( + "searchbar-engine-one-off-add-engine" + ) + ) { + this.input.controller.engagementEvent.discard(); + this.selectedButton.doCommand(); + this.selectedButton = null; + return; + } + + if (this.#handleQuickSuggestOptInCommand(this.selectedButton)) { + this.input.controller.engagementEvent.discard(); + this.selectedButton = null; + return; + } + + // We allow autofill in local but not remote search modes. + let startQueryParams = { + allowAutofill: + !searchMode.engineName && + searchMode.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, + event, + }; + + let userTypedSearchString = + this.input.value && this.input.getAttribute("pageproxystate") != "valid"; + let engine = Services.search.getEngineByName(searchMode.engineName); + + let { where, params } = this._whereToOpen(event); + + // Some key combinations should execute a search immediately. We handle + // these here, outside the switch statement. + if ( + userTypedSearchString && + engine && + (event.shiftKey || where != "current") + ) { + this.input.handleNavigation({ + event, + oneOffParams: { + openWhere: where, + openParams: params, + engine: this.selectedButton.engine, + }, + }); + this.selectedButton = null; + return; + } + + // Handle opening search mode in either the current tab or in a new tab. + switch (where) { + case "current": { + this.input.searchMode = searchMode; + this.input.startQuery(startQueryParams); + break; + } + case "tab": { + // We set this.selectedButton when switching tabs. If we entered search + // mode preview here, it could be cleared when this.selectedButton calls + // setSearchMode. + searchMode.isPreview = false; + + let newTab = this.input.window.gBrowser.addTrustedTab("about:newtab"); + this.input.setSearchMode(searchMode, newTab.linkedBrowser); + if (userTypedSearchString) { + // Set the search string for the new tab. + newTab.linkedBrowser.userTypedValue = this.input.value; + } + if (!params?.inBackground) { + this.input.window.gBrowser.selectedTab = newTab; + newTab.ownerGlobal.gURLBar.startQuery(startQueryParams); + } + break; + } + default: { + this.input.searchMode = searchMode; + this.input.startQuery(startQueryParams); + this.input.select(); + break; + } + } + + this.selectedButton = null; + } + + /** + * Sets the tooltip for a one-off button with an engine. This should set + * either the `tooltiptext` attribute or the relevant l10n ID. + * + * @param {element} button + * The one-off button. + */ + setTooltipForEngineButton(button) { + let aliases = button.engine.aliases; + if (!aliases.length) { + super.setTooltipForEngineButton(button); + return; + } + this.document.l10n.setAttributes( + button, + "search-one-offs-engine-with-alias", + { + engineName: button.engine.name, + alias: aliases[0], + } + ); + } + + /** + * Overrides the willHide method in the superclass to account for the local + * search mode buttons. + * + * @returns {boolean} + * True if we will hide the one-offs when they are requested. + */ + async willHide() { + // We need to call super.willHide() even when we return false below because + // it has the necessary side effect of creating this._engineInfo. + let superWillHide = await super.willHide(); + if ( + lazy.UrlbarUtils.LOCAL_SEARCH_MODES.some(m => + lazy.UrlbarPrefs.get(m.pref) + ) + ) { + return false; + } + return superWillHide; + } + + /** + * Called when a pref tracked by UrlbarPrefs changes. + * + * @param {string} changedPref + * The name of the pref, relative to `browser.urlbar.` if the pref is in + * that branch. + */ + onPrefChanged(changedPref) { + // Invalidate the engine cache when the local-one-offs-related prefs change + // so that the one-offs rebuild themselves the next time the view opens. + if ( + [...lazy.UrlbarUtils.LOCAL_SEARCH_MODES.map(m => m.pref)].includes( + changedPref + ) + ) { + this.invalidateCache(); + } + } + + /** + * Overrides _getAddEngines to return engines that can be added. + * + * @returns {Array} engines + */ + _getAddEngines() { + return this._webEngines; + } + + /** + * Overrides _rebuildEngineList to add the local one-offs. + * + * @param {Array} engines + * The search engines to add. + * @param {Array} addEngines + * The engines that can be added. + */ + _rebuildEngineList(engines, addEngines) { + super._rebuildEngineList(engines, addEngines); + + for (let { source, pref, restrict } of lazy.UrlbarUtils + .LOCAL_SEARCH_MODES) { + if (!lazy.UrlbarPrefs.get(pref)) { + continue; + } + let name = lazy.UrlbarUtils.getResultSourceName(source); + let button = this.document.createXULElement("button"); + button.id = `urlbar-engine-one-off-item-${name}`; + button.setAttribute("class", "searchbar-engine-one-off-item"); + button.setAttribute("tabindex", "-1"); + this.document.l10n.setAttributes(button, `search-one-offs-${name}`, { + restrict, + }); + button.source = source; + this.buttons.appendChild(button); + } + } + + /** + * Overrides the superclass's click listener to handle clicks on local + * one-offs in addition to engine one-offs. + * + * @param {event} event + * The click event. + */ + _on_click(event) { + // Ignore right clicks. + if (event.button == 2) { + return; + } + + let button = event.originalTarget; + + if (this.#handleQuickSuggestOptInCommand(button)) { + return; + } + + if (!button.engine && !button.source) { + return; + } + + this.selectedButton = button; + this.handleSearchCommand(event, { + engineName: button.engine?.name, + source: button.source, + entry: "oneoff", + }); + } + + /** + * Overrides the superclass's contextmenu listener to handle the context menu. + * + * @param {event} event + * The contextmenu event. + */ + _on_contextmenu(event) { + // Prevent the context menu from appearing. + event.preventDefault(); + } + + _on_rebuild() { + if (this.#queryContext) { + this.#buildQuickSuggestOptIn(this.#queryContext); + } + } +} -- cgit v1.2.3