commit ab9720d3d57b3d75da4ec17729036d70770cf682
parent d839523e0358aafd55a4acdb8fe5697d5e02598e
Author: Florian Zia <fzia@mozilla.com>
Date: Fri, 2 Jan 2026 15:39:14 +0000
Bug 2003063 - Part 1: Fork UrlbarInput into SmartbarInput r=mak,urlbar-reviewers
Creates a hard fork of UrlbarInput to enable faster iteration on AI Window multiline input requirements. Once stable, we’ll evaluate convergence and merge changes back into the UrlbarInput.
Differential Revision: https://phabricator.services.mozilla.com/D276132
Diffstat:
3 files changed, 6019 insertions(+), 0 deletions(-)
diff --git a/browser/components/urlbar/content/SmartbarInput.mjs b/browser/components/urlbar/content/SmartbarInput.mjs
@@ -0,0 +1,6017 @@
+/* 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/. */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs"
+ */
+
+const lazy = XPCOMUtils.declareLazy({
+ ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
+ BrowserSearchTelemetry:
+ "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
+ BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ ExtensionSearchHandler:
+ "resource://gre/modules/ExtensionSearchHandler.sys.mjs",
+ ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
+ PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
+ SearchModeSwitcher:
+ "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs",
+ SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
+ SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
+ UrlbarController:
+ "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs",
+ UrlbarEventBufferer:
+ "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs",
+ UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
+ UrlbarQueryContext:
+ "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
+ UrlbarProviderGlobalActions:
+ "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs",
+ UrlbarProviderOpenTabs:
+ "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
+ UrlbarSearchUtils:
+ "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
+ UrlbarTokenizer:
+ "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
+ UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
+ UrlbarValueFormatter:
+ "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs",
+ UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
+ UrlbarSearchTermsPersistence:
+ "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs",
+ UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
+ ClipboardHelper: {
+ service: "@mozilla.org/widget/clipboardhelper;1",
+ iid: Ci.nsIClipboardHelper,
+ },
+ QueryStringStripper: {
+ service: "@mozilla.org/url-query-string-stripper;1",
+ iid: Ci.nsIURLQueryStringStripper,
+ },
+ QUERY_STRIPPING_STRIP_ON_SHARE: {
+ pref: "privacy.query_stripping.strip_on_share.enabled",
+ default: false,
+ },
+ logger: () => lazy.UrlbarUtils.getLogger({ prefix: "Input" }),
+});
+
+const UNLIMITED_MAX_RESULTS = 99;
+
+let getBoundsWithoutFlushing = element =>
+ element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
+let px = number => number.toFixed(2) + "px";
+
+/**
+ * Implements the text input part of the address bar UI.
+ */
+export class UrlbarInput extends HTMLElement {
+ static get #markup() {
+ return `
+ <hbox class="urlbar-background"/>
+ <hbox class="urlbar-input-container"
+ flex="1"
+ pageproxystate="invalid">
+ <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot>
+ <toolbarbutton id="urlbar-searchmode-switcher"
+ class="searchmode-switcher chromeclass-toolbar-additional"
+ align="center"
+ aria-expanded="false"
+ aria-haspopup="menu"
+ tooltip="dynamic-shortcut-tooltip"
+ data-l10n-id="urlbar-searchmode-default"
+ type="menu">
+ <image class="searchmode-switcher-icon toolbarbutton-icon"/>
+ <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker"
+ data-l10n-id="urlbar-searchmode-dropmarker" />
+ <menupopup class="searchmode-switcher-popup toolbar-menupopup"
+ consumeoutsideclicks="false">
+ <label class="searchmode-switcher-popup-description"
+ data-l10n-id="urlbar-searchmode-popup-description"
+ role="heading" />
+ <menuseparator/>
+ <menuseparator class="searchmode-switcher-popup-footer-separator"/>
+ <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic"
+ data-action="openpreferences"
+ image="chrome://global/skin/icons/settings.svg"
+ data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/>
+ </menupopup>
+ </toolbarbutton>
+ <box class="searchmode-switcher-chicklet">
+ <label class="searchmode-switcher-title" />
+ <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button"
+ data-action="exitsearchmode"
+ role="button"
+ data-l10n-id="urlbar-searchmode-exit-button" />
+ </box>
+ <moz-urlbar-slot name="site-info"> </moz-urlbar-slot>
+ <moz-input-box tooltip="aHTMLTooltip"
+ class="urlbar-input-box"
+ flex="1"
+ role="combobox"
+ aria-owns="urlbar-results">
+ <html:input id="urlbar-scheme"
+ required="required"/>
+ <html:input id="urlbar-input"
+ class="urlbar-input textbox-input"
+ aria-controls="urlbar-results"
+ aria-autocomplete="both"
+ inputmode="mozAwesomebar"
+ data-l10n-id="urlbar-placeholder"/>
+ </moz-input-box>
+ <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot>
+ <image class="urlbar-icon urlbar-go-button"
+ role="button"
+ data-l10n-id="urlbar-go-button"/>
+ <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot>
+ </hbox>
+ <vbox class="urlbarView"
+ context=""
+ role="group"
+ tooltip="aHTMLTooltip">
+ <html:div class="urlbarView-body-outer">
+ <html:div class="urlbarView-body-inner">
+ <html:div id="urlbar-results"
+ class="urlbarView-results"
+ role="listbox"/>
+ </html:div>
+ </html:div>
+ <menupopup class="urlbarView-result-menu"
+ consumeoutsideclicks="false"/>
+ <hbox class="search-one-offs"
+ includecurrentengine="true"
+ disabletab="true"/>
+ </vbox>`;
+ }
+
+ /** @type {DocumentFragment} */
+ static get fragment() {
+ if (!UrlbarInput.#fragment) {
+ UrlbarInput.#fragment = window.MozXULElement.parseXULToFragment(
+ UrlbarInput.#markup
+ );
+ }
+ // @ts-ignore
+ return document.importNode(UrlbarInput.#fragment, true);
+ }
+
+ /**
+ * @type {DocumentFragment=}
+ *
+ * The cached fragment.
+ */
+ static #fragment;
+
+ static #inputFieldEvents = [
+ "compositionstart",
+ "compositionend",
+ "contextmenu",
+ "dragover",
+ "dragstart",
+ "drop",
+ "focus",
+ "blur",
+ "input",
+ "beforeinput",
+ "keydown",
+ "keyup",
+ "mouseover",
+ "overflow",
+ "underflow",
+ "paste",
+ "scrollend",
+ "select",
+ "selectionchange",
+ ];
+
+ #allowBreakout = false;
+ #gBrowserListenersAdded = false;
+ #breakoutBlockerCount = 0;
+ #isAddressbar = false;
+ #sapName = "";
+ _userTypedValue = "";
+ _actionOverrideKeyCount = 0;
+ _lastValidURLStr = "";
+ _valueOnLastSearch = "";
+ _suppressStartQuery = false;
+ _suppressPrimaryAdjustment = false;
+ _lastSearchString = "";
+ // Tracks IME composition.
+ #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
+ #compositionClosedPopup = false;
+
+ valueIsTyped = false;
+
+ // Properties accessed in tests.
+ lastQueryContextPromise = Promise.resolve();
+ _autofillPlaceholder = null;
+ _resultForCurrentValue = null;
+ _untrimmedValue = "";
+ _enableAutofillPlaceholder = true;
+
+ constructor() {
+ super();
+
+ this.window = this.ownerGlobal;
+ this.document = this.window.document;
+ this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window);
+
+ lazy.UrlbarPrefs.addObserver(this);
+ window.addEventListener("unload", () => {
+ // Stop listening to pref changes to make sure we don't init the new
+ // searchbar in closed windows that have not been gc'd yet.
+ lazy.UrlbarPrefs.removeObserver(this);
+ });
+ }
+
+ /**
+ * Populates moz-urlbar-slots by moving all children with a urlbar-slot
+ * attribute into their moz-urlbar-slots and removing the slots.
+ *
+ * Should only be called once all children have been parsed.
+ */
+ #populateSlots() {
+ let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]");
+ for (let slot of urlbarSlots) {
+ let slotName = slot.getAttribute("name");
+ let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`);
+
+ for (let node of nodes) {
+ slot.parentNode.insertBefore(node, slot);
+ }
+
+ slot.remove();
+ }
+
+ // Slotted elements only used by the addressbar.
+ // Will be null for searchbar and others.
+ this._identityBox = this.querySelector(".identity-box");
+ this._revertButton = this.querySelector(".urlbar-revert-button");
+ // Pre scotch bonnet search mode indicator (addressbar only).
+ this._searchModeIndicator = this.querySelector(
+ "#urlbar-search-mode-indicator"
+ );
+ this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector(
+ "#urlbar-search-mode-indicator-title"
+ );
+ this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ }
+
+ /**
+ * Initialization that happens once on the first connect.
+ */
+ #initOnce() {
+ this.#sapName = this.getAttribute("sap-name");
+ this.#isAddressbar = this.#sapName == "urlbar";
+
+ // This listener must be added before connecting the fragment
+ // because the event could fire while or after connecting it.
+ this.addEventListener(
+ "moz-input-box-rebuilt",
+ this.#onContextMenuRebuilt.bind(this)
+ );
+
+ this.appendChild(UrlbarInput.fragment);
+
+ // Make sure all children have been parsed before calling #populateSlots.
+ if (document.readyState === "loading") {
+ document.addEventListener(
+ "DOMContentLoaded",
+ () => this.#populateSlots(),
+ { once: true }
+ );
+ } else {
+ this.#populateSlots();
+ }
+
+ this.panel = this.querySelector(".urlbarView");
+ this.inputField = /** @type {HTMLInputElement} */ (
+ this.querySelector(".urlbar-input")
+ );
+ this._inputContainer = this.querySelector(".urlbar-input-container");
+
+ this.controller = new lazy.UrlbarController({ input: this });
+ this.view = new lazy.UrlbarView(this);
+ this.searchModeSwitcher = new lazy.SearchModeSwitcher(this);
+
+ // The event bufferer can be used to defer events that may affect users
+ // muscle memory; for example quickly pressing DOWN+ENTER should end up
+ // on a predictable result, regardless of the search status. The event
+ // bufferer will invoke the handling code at the right time.
+ this.eventBufferer = new lazy.UrlbarEventBufferer(this);
+
+ // Forward certain properties.
+ // Note if you are extending these, you'll also need to extend the inline
+ // type definitions.
+ const READ_WRITE_PROPERTIES = [
+ "placeholder",
+ "readOnly",
+ "selectionStart",
+ "selectionEnd",
+ ];
+
+ for (let property of READ_WRITE_PROPERTIES) {
+ Object.defineProperty(this, property, {
+ enumerable: true,
+ get() {
+ return this.inputField[property];
+ },
+ set(val) {
+ this.inputField[property] = val;
+ },
+ });
+ }
+
+ // The engine name is not known yet, but update placeholder anyway to
+ // reflect value of keyword.enabled or set the searchbar placeholder.
+ this._setPlaceholder(null);
+
+ if (this.#isAddressbar) {
+ let searchContainersPref = lazy.UrlbarPrefs.get(
+ "switchTabs.searchAllContainers"
+ );
+ Glean.urlbar.prefSwitchTabsSearchAllContainers.set(searchContainersPref);
+ }
+ }
+
+ connectedCallback() {
+ if (
+ this.getAttribute("sap-name") == "searchbar" &&
+ !lazy.UrlbarPrefs.get("browser.search.widget.new")
+ ) {
+ return;
+ }
+
+ this.#init();
+ }
+
+ #init() {
+ if (!this.controller) {
+ this.#initOnce();
+ }
+
+ if (this.sapName == "searchbar") {
+ this.parentNode.setAttribute("overflows", "false");
+ }
+
+ // Don't attach event listeners if the toolbar is not visible
+ // in this window or the urlbar is readonly.
+ if (
+ !this.window.toolbar.visible ||
+ this.window.document.documentElement.hasAttribute("taskbartab") ||
+ this.readOnly
+ ) {
+ return;
+ }
+
+ this._initCopyCutController();
+
+ for (let event of UrlbarInput.#inputFieldEvents) {
+ this.inputField.addEventListener(event, this);
+ }
+
+ // These are on the window to detect focusing shortcuts like F6.
+ this.window.addEventListener("keydown", this);
+ this.window.addEventListener("keyup", this);
+
+ this.window.addEventListener("mousedown", this);
+ if (AppConstants.platform == "win") {
+ this.window.addEventListener("draggableregionleftmousedown", this);
+ }
+ this.addEventListener("mousedown", this);
+
+ // This listener handles clicks from our children too, included the search mode
+ // indicator close button.
+ this._inputContainer.addEventListener("click", this);
+
+ // This is used to detect commands launched from the panel, to avoid
+ // recording abandonment events when the command causes a blur event.
+ this.view.panel.addEventListener("command", this, true);
+
+ this.window.addEventListener("customizationstarting", this);
+ this.window.addEventListener("aftercustomization", this);
+ this.window.addEventListener("toolbarvisibilitychange", this);
+ let menuToolbar = this.window.document.getElementById("toolbar-menubar");
+ if (menuToolbar) {
+ menuToolbar.addEventListener("DOMMenuBarInactive", this);
+ menuToolbar.addEventListener("DOMMenuBarActive", this);
+ }
+
+ if (this.window.gBrowser) {
+ // On startup, this will be called again by browser-init.js
+ // once gBrowser has been initialized.
+ this.addGBrowserListeners();
+ }
+
+ // If the search service is not initialized yet, the placeholder
+ // and icon will be updated in delayedStartupInit.
+ if (
+ Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") &&
+ Services.search.isInitialized
+ ) {
+ this.searchModeSwitcher.updateSearchIcon();
+ this._updatePlaceholderFromDefaultEngine();
+ }
+
+ // Expanding requires a parent toolbar, and us not being read-only.
+ this.#allowBreakout = !!this.closest("toolbar");
+ if (this.#allowBreakout) {
+ // TODO(emilio): This could use CSS anchor positioning rather than this
+ // ResizeObserver, eventually.
+ this._resizeObserver = new this.window.ResizeObserver(([entry]) => {
+ this.style.setProperty(
+ "--urlbar-width",
+ px(entry.borderBoxSize[0].inlineSize)
+ );
+ });
+ this._resizeObserver.observe(this.parentNode);
+ }
+
+ this.#updateLayoutBreakout();
+
+ this._addObservers();
+ }
+
+ disconnectedCallback() {
+ if (
+ this.getAttribute("sap-name") == "searchbar" &&
+ !lazy.UrlbarPrefs.get("browser.search.widget.new")
+ ) {
+ return;
+ }
+
+ this.#uninit();
+ }
+
+ #uninit() {
+ if (this.sapName == "searchbar") {
+ this.parentNode.removeAttribute("overflows");
+ }
+
+ if (this._copyCutController) {
+ this.inputField.controllers.removeController(this._copyCutController);
+ delete this._copyCutController;
+ }
+
+ for (let event of UrlbarInput.#inputFieldEvents) {
+ this.inputField.removeEventListener(event, this);
+ }
+
+ // These are on the window to detect focusing shortcuts like F6.
+ this.window.removeEventListener("keydown", this);
+ this.window.removeEventListener("keyup", this);
+
+ this.window.removeEventListener("mousedown", this);
+ if (AppConstants.platform == "win") {
+ this.window.removeEventListener("draggableregionleftmousedown", this);
+ }
+ this.removeEventListener("mousedown", this);
+
+ // This listener handles clicks from our children too, included the search mode
+ // indicator close button.
+ this._inputContainer.removeEventListener("click", this);
+
+ // This is used to detect commands launched from the panel, to avoid
+ // recording abandonment events when the command causes a blur event.
+ this.view.panel.removeEventListener("command", this, true);
+
+ this.window.removeEventListener("customizationstarting", this);
+ this.window.removeEventListener("aftercustomization", this);
+ this.window.removeEventListener("toolbarvisibilitychange", this);
+ let menuToolbar = this.window.document.getElementById("toolbar-menubar");
+ if (menuToolbar) {
+ menuToolbar.removeEventListener("DOMMenuBarInactive", this);
+ menuToolbar.removeEventListener("DOMMenuBarActive", this);
+ }
+ if (this.#gBrowserListenersAdded) {
+ this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ this.window.gBrowser.tabContainer.removeEventListener("TabClose", this);
+ this.window.gBrowser.removeTabsProgressListener(this);
+ this.#gBrowserListenersAdded = false;
+ }
+
+ this._resizeObserver?.disconnect();
+
+ this._removeObservers();
+ }
+
+ /**
+ * This method is used to attach new context menu options to the urlbar
+ * context menu, i.e. the context menu of the moz-input-box.
+ * It is called when the moz-input-box rebuilds its context menu.
+ *
+ * Note that it might be called before #init has finished.
+ */
+ #onContextMenuRebuilt() {
+ this._initStripOnShare();
+ this._initPasteAndGo();
+ }
+
+ addGBrowserListeners() {
+ if (this.window.gBrowser && !this.#gBrowserListenersAdded) {
+ this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
+ this.window.gBrowser.tabContainer.addEventListener("TabClose", this);
+ this.window.gBrowser.addTabsProgressListener(this);
+ this.#gBrowserListenersAdded = true;
+ }
+ }
+
+ #lazy = XPCOMUtils.declareLazy({
+ valueFormatter: () => new lazy.UrlbarValueFormatter(this),
+ addSearchEngineHelper: () => new AddSearchEngineHelper(this),
+ });
+
+ /**
+ * Manages the Add Search Engine contextual menu entries.
+ */
+ get addSearchEngineHelper() {
+ return this.#lazy.addSearchEngineHelper;
+ }
+
+ /**
+ * The search access point name of the UrlbarInput for use with telemetry or
+ * logging, e.g. `urlbar`, `searchbar`.
+ */
+ get sapName() {
+ return this.#sapName;
+ }
+
+ blur() {
+ this.inputField.blur();
+ }
+
+ /**
+ * @type {typeof HTMLInputElement.prototype.placeholder}
+ */
+ placeholder;
+
+ /**
+ * @type {typeof HTMLInputElement.prototype.readOnly}
+ */
+ readOnly;
+
+ /**
+ * @type {typeof HTMLInputElement.prototype.selectionStart}
+ */
+ selectionStart;
+
+ /**
+ * @type {typeof HTMLInputElement.prototype.selectionEnd}
+ */
+ selectionEnd;
+
+ /**
+ * Called when a urlbar or urlbar related pref changes.
+ *
+ * @param {string} pref
+ * The name of the pref. Relative to `browser.urlbar` for urlbar prefs.
+ */
+ onPrefChanged(pref) {
+ switch (pref) {
+ case "keyword.enabled":
+ this._updatePlaceholderFromDefaultEngine().catch(e =>
+ // This can happen if the search service failed.
+ console.warn("Falied to update urlbar placeholder:", e)
+ );
+ break;
+ case "browser.search.widget.new": {
+ if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) {
+ if (lazy.UrlbarPrefs.get("browser.search.widget.new")) {
+ // The connectedCallback was skipped. Init now.
+ this.#init();
+ } else {
+ // Uninit now, the disconnectedCallback will be skipped.
+ this.#uninit();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Applies styling to the text in the urlbar input, depending on the text.
+ */
+ formatValue() {
+ // The editor may not exist if the toolbar is not visible.
+ if (this.#isAddressbar && this.editor) {
+ this.#lazy.valueFormatter.update();
+ }
+ }
+
+ focus() {
+ let beforeFocus = new CustomEvent("beforefocus", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.inputField.dispatchEvent(beforeFocus);
+ if (beforeFocus.defaultPrevented) {
+ return;
+ }
+
+ this.inputField.focus();
+ }
+
+ select() {
+ let beforeSelect = new CustomEvent("beforeselect", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.inputField.dispatchEvent(beforeSelect);
+ if (beforeSelect.defaultPrevented) {
+ return;
+ }
+
+ // See _on_select(). HTMLInputElement.select() dispatches a "select"
+ // event but does not set the primary selection.
+ this._suppressPrimaryAdjustment = true;
+ this.inputField.select();
+ this._suppressPrimaryAdjustment = false;
+ }
+
+ setSelectionRange(selectionStart, selectionEnd) {
+ let beforeSelect = new CustomEvent("beforeselect", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.inputField.dispatchEvent(beforeSelect);
+ if (beforeSelect.defaultPrevented) {
+ return;
+ }
+
+ // See _on_select(). HTMLInputElement.select() dispatches a "select"
+ // event but does not set the primary selection.
+ this._suppressPrimaryAdjustment = true;
+ this.inputField.setSelectionRange(selectionStart, selectionEnd);
+ this._suppressPrimaryAdjustment = false;
+ }
+
+ saveSelectionStateForBrowser(browser) {
+ let state = this.getBrowserState(browser);
+ state.selection = {
+ // When the value is empty, we're either on a blank page, or the whole
+ // text has been edited away. In the latter case we'll restore value to
+ // the current URI, and we want to fully select it.
+ start: this.value ? this.selectionStart : 0,
+ end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER,
+ // When restoring a URI from an empty value, we don't want to untrim it.
+ shouldUntrim: this.value && !this._protocolIsTrimmed,
+ };
+ }
+
+ restoreSelectionStateForBrowser(browser) {
+ // Address bar must be focused to untrim and for selection to make sense.
+ this.focus();
+ let state = this.getBrowserState(browser);
+ if (state.selection) {
+ if (state.selection.shouldUntrim) {
+ this.#maybeUntrimUrl();
+ }
+ this.setSelectionRange(
+ state.selection.start,
+ // When selecting all the end value may be larger than the actual value.
+ Math.min(state.selection.end, this.value.length)
+ );
+ }
+ }
+
+ /**
+ * Sets the URI to display in the location bar.
+ *
+ * @param {object} [options]
+ * @param {?nsIURI} [options.uri]
+ * If this is unspecified, the current URI will be used.
+ * @param {boolean} [options.dueToTabSwitch=false]
+ * Whether this is being called due to switching tabs.
+ * @param {boolean} [options.dueToSessionRestore=false]
+ * Whether this is being called due to session restore.
+ * @param {boolean} [options.hideSearchTerms=false]
+ * True if userTypedValue should not be overidden by search terms
+ * and false otherwise.
+ * @param {boolean} [options.isSameDocument=false]
+ * Whether the caller loaded a new document or not (e.g. location
+ * change from an anchor scroll or a pushState event).
+ */
+ setURI({
+ uri = null,
+ dueToTabSwitch = false,
+ dueToSessionRestore = false,
+ hideSearchTerms = false,
+ isSameDocument = false,
+ } = {}) {
+ if (!this.#isAddressbar) {
+ throw new Error(
+ "Cannot set URI for UrlbarInput that is not an address bar"
+ );
+ }
+ // We only need to update the searchModeUI on tab switch conditionally
+ // as we only persist searchMode with ScotchBonnet enabled.
+ if (
+ dueToTabSwitch &&
+ lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode")
+ ) {
+ this._updateSearchModeUI(this.searchMode);
+ }
+
+ let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
+ this.#handlePersistedSearchTerms({
+ state,
+ uri,
+ dueToTabSwitch,
+ hideSearchTerms,
+ isSameDocument,
+ });
+
+ let value = this.userTypedValue;
+ let valid = false;
+ let isReverting = !uri;
+
+ // If `value` is null or if it's an empty string and we're switching tabs
+ // set value to the browser's current URI. When a user empties the input,
+ // switches tabs, and switches back, we want the URI to become visible again
+ // so the user knows what URI they're viewing.
+ // An exception to this is made in case of an auth request from a different
+ // base domain. To avoid auth prompt spoofing we already display the url of
+ // the cross domain resource, although the page is not loaded yet.
+ // This url will be set/unset by PromptParent. See bug 791594 for reference.
+ if (value === null || (!value && dueToTabSwitch)) {
+ uri =
+ this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
+ uri ||
+ this.#isOpenedPageInBlankTargetLoading ||
+ this.window.gBrowser.currentURI;
+ // Strip off usernames and passwords for the location bar
+ try {
+ uri = Services.io.createExposableURI(uri);
+ } catch (e) {}
+
+ let isInitialPageControlledByWebContent = false;
+
+ // Replace initial page URIs with an empty string
+ // only if there's no opener (bug 370555).
+ if (
+ this.window.isInitialPage(uri) &&
+ lazy.BrowserUIUtils.checkEmptyPageOrigin(
+ this.window.gBrowser.selectedBrowser,
+ uri
+ )
+ ) {
+ value = "";
+ } else {
+ isInitialPageControlledByWebContent = true;
+
+ // We should deal with losslessDecodeURI throwing for exotic URIs
+ try {
+ value = losslessDecodeURI(uri);
+ } catch (ex) {
+ value = "about:blank";
+ }
+ }
+ // If we update the URI while restoring a session, set the proxyState to
+ // invalid, because we don't have a valid security state to show via site
+ // identity yet. See Bug 1746383.
+ valid =
+ !dueToSessionRestore &&
+ (!this.window.isBlankPageURL(uri.spec) ||
+ lazy.ExtensionUtils.isExtensionUrl(uri) ||
+ isInitialPageControlledByWebContent);
+ } else if (
+ this.window.isInitialPage(value) &&
+ lazy.BrowserUIUtils.checkEmptyPageOrigin(
+ this.window.gBrowser.selectedBrowser
+ )
+ ) {
+ value = "";
+ valid = true;
+ }
+
+ const previousUntrimmedValue = this.untrimmedValue;
+ // When calculating the selection indices we must take into account a
+ // trimmed protocol.
+ let offset = this._protocolIsTrimmed
+ ? lazy.BrowserUIUtils.trimURLProtocol.length
+ : 0;
+ const previousSelectionStart = this.selectionStart + offset;
+ const previousSelectionEnd = this.selectionEnd + offset;
+
+ this._setValue(value, { allowTrim: true, valueIsTyped: !valid });
+ this.toggleAttribute("usertyping", !valid && value);
+
+ if (this.focused && value != previousUntrimmedValue) {
+ if (
+ previousSelectionStart != previousSelectionEnd &&
+ value.substring(previousSelectionStart, previousSelectionEnd) ===
+ previousUntrimmedValue.substring(
+ previousSelectionStart,
+ previousSelectionEnd
+ )
+ ) {
+ // If the same text is in the same place as the previously selected text,
+ // the selection is kept.
+ this.inputField.setSelectionRange(
+ previousSelectionStart - offset,
+ previousSelectionEnd - offset
+ );
+ } else if (
+ previousSelectionEnd &&
+ (previousUntrimmedValue.length === previousSelectionEnd ||
+ value.length <= previousSelectionEnd)
+ ) {
+ // If the previous end caret is not 0 and the caret is at the end of the
+ // input or its position is beyond the end of the new value, keep the
+ // position at the end.
+ this.inputField.setSelectionRange(value.length, value.length);
+ } else {
+ // Otherwise clear selection and set the caret position to the previous
+ // caret end position.
+ this.inputField.setSelectionRange(
+ previousSelectionEnd - offset,
+ previousSelectionEnd - offset
+ );
+ }
+ }
+
+ // The proxystate must be set before setting search mode below because
+ // search mode depends on it.
+ this.setPageProxyState(
+ valid ? "valid" : "invalid",
+ dueToTabSwitch,
+ !isReverting &&
+ dueToTabSwitch &&
+ this.getBrowserState(this.window.gBrowser.selectedBrowser)
+ .isUnifiedSearchButtonAvailable
+ );
+
+ if (
+ state.persist?.shouldPersist &&
+ !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState(
+ this.searchMode,
+ state
+ )
+ ) {
+ // When search terms persist, on non-default engine search result pages
+ // the address bar should show the same search mode. For default engines,
+ // search mode should not persist.
+ if (state.persist.isDefaultEngine) {
+ this.searchMode = null;
+ } else {
+ this.searchMode = {
+ engineName: state.persist.originalEngineName,
+ source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
+ isPreview: false,
+ };
+ }
+ } else if (dueToTabSwitch && !valid) {
+ // If we're switching tabs, restore the tab's search mode.
+ this.restoreSearchModeState();
+ } else if (valid) {
+ // If the URI is valid, exit search mode. This must happen
+ // after setting proxystate above because search mode depends on it.
+ this.searchMode = null;
+ }
+
+ // Dispatch URIUpdate event to synchronize the tab status when switching.
+ let event = new CustomEvent("SetURI", { bubbles: true });
+ this.inputField.dispatchEvent(event);
+ }
+
+ /**
+ * Converts an internal URI (e.g. a URI with a username or password) into one
+ * which we can expose to the user.
+ *
+ * @param {nsIURI} uri
+ * The URI to be converted
+ * @returns {nsIURI}
+ * The converted, exposable URI
+ */
+ makeURIReadable(uri) {
+ // Avoid copying 'about:reader?url=', and always provide the original URI:
+ // Reader mode ensures we call createExposableURI itself.
+ let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay(
+ uri.displaySpec
+ );
+ if (readerStrippedURI) {
+ return readerStrippedURI;
+ }
+
+ try {
+ return Services.io.createExposableURI(uri);
+ } catch (ex) {}
+
+ return uri;
+ }
+
+ /**
+ * Function for tabs progress listener.
+ *
+ * @param {nsIBrowser} browser
+ * @param {nsIWebProgress} webProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param {nsIRequest} request
+ * The associated nsIRequest. This may be null in some cases.
+ * @param {nsIURI} locationURI
+ * The URI of the location that is being loaded.
+ */
+ onLocationChange(browser, webProgress, request, locationURI) {
+ if (!webProgress.isTopLevel) {
+ return;
+ }
+
+ if (
+ browser != this.window.gBrowser.selectedBrowser &&
+ !this.window.isBlankPageURL(locationURI.spec)
+ ) {
+ // If the page is loaded on background tab, make Unified Search Button
+ // unavailable when back to the tab.
+ this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false;
+ }
+
+ // Using browser navigation buttons should potentially trigger a bounce
+ // telemetry event.
+ if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+ this.controller.engagementEvent.handleBounceEventTrigger(browser);
+ }
+ }
+
+ /**
+ * Passes DOM events to the _on_<event type> methods.
+ *
+ * @param {Event} event The event to handle.
+ */
+ handleEvent(event) {
+ let methodName = "_on_" + event.type;
+ if (methodName in this) {
+ try {
+ this[methodName](event);
+ } catch (e) {
+ console.error(`Error calling UrlbarInput::${methodName}:`, e);
+ }
+ } else {
+ throw new Error("Unrecognized UrlbarInput event: " + event.type);
+ }
+ }
+
+ /**
+ * Handles an event which might open text or a URL. If the event requires
+ * doing so, handleCommand forwards it to handleNavigation.
+ *
+ * @param {Event} [event] The event triggering the open.
+ */
+ handleCommand(event = null) {
+ let isMouseEvent = MouseEvent.isInstance(event);
+ if (isMouseEvent && event.button == 2) {
+ // Do nothing for right clicks.
+ return;
+ }
+
+ // Determine whether to use the selected one-off search button. In
+ // one-off search buttons parlance, "selected" means that the button
+ // has been navigated to via the keyboard. So we want to use it if
+ // the triggering event is not a mouse click -- i.e., it's a Return
+ // key -- or if the one-off was mouse-clicked.
+ if (this.view.isOpen) {
+ let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton;
+ if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
+ this.view.oneOffSearchButtons.handleSearchCommand(event, {
+ engineName: selectedOneOff.engine?.name,
+ source: selectedOneOff.source,
+ entry: "oneoff",
+ });
+ return;
+ }
+ }
+
+ this.handleNavigation({ event });
+ }
+
+ /**
+ * @typedef {object} HandleNavigationOneOffParams
+ *
+ * @property {string} openWhere
+ * Where we expect the result to be opened.
+ * @property {object} openParams
+ * The parameters related to where the result will be opened.
+ * @property {nsISearchEngine} engine
+ * The selected one-off's engine.
+ */
+
+ /**
+ * Handles an event which would cause a URL or text to be opened.
+ *
+ * @param {object} options
+ * Options for the navigation.
+ * @param {Event} [options.event]
+ * The event triggering the open.
+ * @param {HandleNavigationOneOffParams} [options.oneOffParams]
+ * Optional. Pass if this navigation was triggered by a one-off. Practically
+ * speaking, UrlbarSearchOneOffs passes this when the user holds certain key
+ * modifiers while picking a one-off. In those cases, we do an immediate
+ * search using the one-off's engine instead of entering search mode.
+ * @param {object} [options.triggeringPrincipal]
+ * The principal that the action was triggered from.
+ */
+ handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
+ let element = this.view.selectedElement;
+ let result = this.view.getResultFromElement(element);
+ let openParams = oneOffParams?.openParams || { triggeringPrincipal };
+
+ // If the value was submitted during composition, the result may not have
+ // been updated yet, because the input event happens after composition end.
+ // We can't trust element nor _resultForCurrentValue targets in that case,
+ // so we always generate a new heuristic to load.
+ let isComposing = this.editor.composing;
+
+ // Use the selected element if we have one; this is usually the case
+ // when the view is open.
+ let selectedPrivateResult =
+ result &&
+ result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.payload.inPrivateWindow;
+ let selectedPrivateEngineResult =
+ selectedPrivateResult && result.payload.isPrivateEngine;
+ // Whether the user has been editing the value in the URL bar after selecting
+ // the result. However, if the result type is tip, pick as it is. The result
+ // heuristic is also kept the behavior as is for safety.
+ let safeToPickResult =
+ result &&
+ (result.heuristic ||
+ !this.valueIsTyped ||
+ result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP ||
+ this.value == this.#getValueFromResult(result));
+ if (
+ !isComposing &&
+ element &&
+ (!oneOffParams?.engine || selectedPrivateEngineResult) &&
+ safeToPickResult
+ ) {
+ this.pickElement(element, event);
+ return;
+ }
+
+ // Use the hidden heuristic if it exists and there's no selection.
+ if (
+ lazy.UrlbarPrefs.get("experimental.hideHeuristic") &&
+ !element &&
+ !isComposing &&
+ !oneOffParams?.engine &&
+ this._resultForCurrentValue?.heuristic
+ ) {
+ this.pickResult(this._resultForCurrentValue, event);
+ return;
+ }
+
+ // We don't select a heuristic result when we're autofilling a token alias,
+ // but we want pressing Enter to behave like the first result was selected.
+ if (!result && this.value.startsWith("@")) {
+ let tokenAliasResult = this.view.getResultAtIndex(0);
+ if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
+ this.pickResult(tokenAliasResult, event);
+ return;
+ }
+ }
+
+ let url;
+ let selType = this.controller.engagementEvent.typeFromElement(
+ result,
+ element
+ );
+ let typedValue = this.value;
+ if (oneOffParams?.engine) {
+ selType = "oneoff";
+ typedValue = this._lastSearchString;
+ // If there's a selected one-off button then load a search using
+ // the button's engine.
+ result = this._resultForCurrentValue;
+
+ let searchString =
+ (result && (result.payload.suggestion || result.payload.query)) ||
+ this._lastSearchString;
+ [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl(
+ oneOffParams.engine,
+ searchString
+ );
+ if (oneOffParams.openWhere == "tab") {
+ this.window.gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ tabEvent =>
+ this._recordSearch(
+ oneOffParams.engine,
+ event,
+ {},
+ tabEvent.target.linkedBrowser
+ ),
+ { once: true }
+ );
+ } else {
+ this._recordSearch(oneOffParams.engine, event);
+ }
+
+ lazy.UrlbarUtils.addToFormHistory(
+ this,
+ searchString,
+ oneOffParams.engine.name
+ ).catch(console.error);
+ } else {
+ // Use the current value if we don't have a UrlbarResult e.g. because the
+ // view is closed.
+ url = this.untrimmedValue;
+ openParams.postData = null;
+ }
+
+ if (!url) {
+ return;
+ }
+
+ // When the user hits enter in a local search mode and there's no selected
+ // result or one-off, don't do anything.
+ if (
+ this.searchMode &&
+ !this.searchMode.engineName &&
+ !result &&
+ !oneOffParams
+ ) {
+ return;
+ }
+
+ let where = oneOffParams?.openWhere || this._whereToOpen(event);
+ if (selectedPrivateResult) {
+ where = "window";
+ openParams.private = true;
+ }
+ openParams.allowInheritPrincipal = false;
+ url = this._maybeCanonizeURL(event, url) || url.trim();
+
+ let selectedResult = result || this.view.selectedResult;
+ this.controller.engagementEvent.record(event, {
+ element,
+ selType,
+ searchString: typedValue,
+ result: selectedResult || this._resultForCurrentValue || null,
+ });
+
+ if (URL.canParse(url)) {
+ // Annotate if the untrimmed value contained a scheme, to later potentially
+ // be upgraded by schemeless HTTPS-First.
+ openParams.schemelessInput = this.#getSchemelessInput(
+ this.untrimmedValue
+ );
+ this._loadURL(url, event, where, openParams);
+ return;
+ }
+
+ // This is not a URL and there's no selected element, because likely the
+ // view is closed, or paste&go was used.
+ // We must act consistently here, having or not an open view should not
+ // make a difference if the search string is the same.
+
+ // If we have a result for the current value, we can just use it.
+ if (!isComposing && this._resultForCurrentValue) {
+ this.pickResult(this._resultForCurrentValue, event);
+ return;
+ }
+
+ // Otherwise, we must fetch the heuristic result for the current value.
+ // TODO (Bug 1604927): If the urlbar results are restricted to a specific
+ // engine, here we must search with that specific engine; indeed the
+ // docshell wouldn't know about our engine restriction.
+ // Also remember to invoke this._recordSearch, after replacing url with
+ // the appropriate engine submission url.
+ let browser = this.window.gBrowser.selectedBrowser;
+ let lastLocationChange = browser.lastLocationChange;
+
+ // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit.
+ Glean.urlbar.heuristicResultMissing.addToDenominator(1);
+
+ lazy.UrlbarUtils.getHeuristicResultFor(url, this)
+ .then(newResult => {
+ // Because this happens asynchronously, we must verify that the browser
+ // location did not change in the meanwhile.
+ if (
+ where != "current" ||
+ browser.lastLocationChange == lastLocationChange
+ ) {
+ this.pickResult(newResult, event, null, browser);
+ }
+ })
+ .catch(() => {
+ if (url) {
+ // Something went wrong, we should always have a heuristic result,
+ // otherwise it means we're not able to search at all, maybe because
+ // some parts of the profile are corrupt.
+ // The urlbar should still allow to search or visit the typed string,
+ // so that the user can look for help to resolve the problem.
+
+ // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit.
+ Glean.urlbar.heuristicResultMissing.addToNumerator(1);
+
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ let {
+ preferredURI: uri,
+ postData,
+ keywordAsSent,
+ } = Services.uriFixup.getFixupURIInfo(url, flags);
+ if (
+ where != "current" ||
+ browser.lastLocationChange == lastLocationChange
+ ) {
+ openParams.postData = postData;
+ if (!keywordAsSent) {
+ // `uri` is not a search engine url, so we annotate if the untrimmed
+ // value contained a scheme, to potentially be later upgraded by
+ // schemeless HTTPS-First.
+ openParams.schemelessInput = this.#getSchemelessInput(
+ this.untrimmedValue
+ );
+ }
+ this._loadURL(uri.spec, event, where, openParams, null, browser);
+ }
+ }
+ });
+ // Don't add further handling here, the catch above is our last resort.
+ }
+
+ handleRevert() {
+ this.userTypedValue = null;
+ // Nullify search mode before setURI so it won't try to restore it.
+ this.searchMode = null;
+ if (this.#isAddressbar) {
+ this.setURI({
+ dueToTabSwitch: true,
+ hideSearchTerms: true,
+ });
+ } else {
+ this.value = "";
+ }
+ if (this.value && this.focused) {
+ this.select();
+ }
+ }
+
+ maybeHandleRevertFromPopup(anchorElement) {
+ let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
+ if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) {
+ this.handleRevert();
+ Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1);
+ }
+ }
+
+ /**
+ * Called by inputs that resemble search boxes, but actually hand input off
+ * to the Urlbar. We use these fake inputs on the new tab page and
+ * about:privatebrowsing.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {nsISearchEngine} [searchEngine]
+ * Optional. If included and the right prefs are set, we will enter search
+ * mode when handing `searchString` from the fake input to the Urlbar.
+ * @param {string} [newtabSessionId]
+ * Optional. The id of the newtab session that handed off this search.
+ */
+ handoff(searchString, searchEngine, newtabSessionId) {
+ this._isHandoffSession = true;
+ this._handoffSession = newtabSessionId;
+ if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) {
+ this.search(searchString, {
+ searchEngine,
+ searchModeEntry: "handoff",
+ });
+ } else {
+ this.search(searchString);
+ }
+ }
+
+ /**
+ * Called when an element of the view is picked.
+ *
+ * @param {HTMLElement} element The element that was picked.
+ * @param {Event} event The event that picked the element.
+ */
+ pickElement(element, event) {
+ let result = this.view.getResultFromElement(element);
+ lazy.logger.debug(
+ `pickElement ${element} with event ${event?.type}, result: ${result}`
+ );
+ if (!result) {
+ return;
+ }
+ this.pickResult(result, event, element);
+ }
+
+ /**
+ * Called when a result is picked.
+ *
+ * @param {UrlbarResult} result The result that was picked.
+ * @param {Event} event The event that picked the result.
+ * @param {HTMLElement} element the picked view element, if available.
+ * @param {object} browser The browser to use for the load.
+ */
+ // eslint-disable-next-line complexity
+ pickResult(
+ result,
+ event,
+ element = null,
+ browser = this.window.gBrowser.selectedBrowser
+ ) {
+ if (element?.classList.contains("urlbarView-button-menu")) {
+ this.view.openResultMenu(result, element);
+ return;
+ }
+
+ if (element?.dataset.command) {
+ this.#pickMenuResult(result, event, element, browser);
+ return;
+ }
+
+ if (
+ result.providerName == lazy.UrlbarProviderGlobalActions.name &&
+ this.#providesSearchMode(result)
+ ) {
+ this.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ });
+ return;
+ }
+
+ // When a one-off is selected, we restyle heuristic results to look like
+ // search results. In the unlikely event that they are clicked, instead of
+ // picking the results as usual, we confirm search mode, same as if the user
+ // had selected them and pressed the enter key. Restyling results in this
+ // manner was agreed on as a compromise between consistent UX and
+ // engineering effort. See review discussion at bug 1667766.
+ if (
+ (this.searchMode?.isPreview &&
+ result.providerName == lazy.UrlbarProviderGlobalActions.name) ||
+ (result.heuristic &&
+ this.searchMode?.isPreview &&
+ this.view.oneOffSearchButtons?.selectedButton)
+ ) {
+ this.confirmSearchMode();
+ this.search(this.value);
+ return;
+ }
+
+ if (
+ result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP &&
+ result.payload.type == "dismissalAcknowledgment"
+ ) {
+ // The user clicked the "Got it" button inside the dismissal
+ // acknowledgment tip. Dismiss the tip.
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: "dismiss",
+ });
+ this.view.onQueryResultRemoved(result.rowIndex);
+ return;
+ }
+
+ let resultUrl = element?.dataset.url;
+ let originalUntrimmedValue = this.untrimmedValue;
+ let isCanonized = this.setValueFromResult({
+ result,
+ event,
+ element,
+ urlOverride: resultUrl,
+ });
+ let where = this._whereToOpen(event);
+ let openParams = {
+ allowInheritPrincipal: false,
+ globalHistoryOptions: {
+ triggeringSource: this.#sapName,
+ triggeringSearchEngine: result.payload?.engine,
+ triggeringSponsoredURL: result.payload?.isSponsored
+ ? result.payload.url
+ : undefined,
+ },
+ private: this.isPrivate,
+ };
+
+ if (resultUrl && where == "current") {
+ // Open help links in a new tab.
+ where = "tab";
+ }
+
+ if (!this.#providesSearchMode(result)) {
+ this.view.close({ elementPicked: true });
+ }
+
+ if (isCanonized) {
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ selType: "canonized",
+ searchString: this._lastSearchString,
+ });
+ this._loadURL(this._untrimmedValue, event, where, openParams, browser);
+ return;
+ }
+
+ let { url, postData } = resultUrl
+ ? { url: resultUrl, postData: null }
+ : lazy.UrlbarUtils.getUrlFromResult(result, { element });
+ openParams.postData = postData;
+
+ switch (result.type) {
+ case lazy.UrlbarUtils.RESULT_TYPE.URL: {
+ if (result.heuristic) {
+ // Bug 1578856: both the provider and the docshell run heuristics to
+ // decide how to handle a non-url string, either fixing it to a url, or
+ // searching for it.
+ // Some preferences can control the docshell behavior, for example
+ // if dns_first_for_single_words is true, the docshell looks up the word
+ // against the dns server, and either loads it as an url or searches for
+ // it, depending on the lookup result. The provider instead will always
+ // return a fixed url in this case, because URIFixup is synchronous and
+ // can't do a synchronous dns lookup. A possible long term solution
+ // would involve sharing the docshell logic with the provider, along
+ // with the dns lookup.
+ // For now, in this specific case, we'll override the result's url
+ // with the input value, and let it pass through to _loadURL(), and
+ // finally to the docshell.
+ // This also means that in some cases the heuristic result will show a
+ // Visit entry, but the docshell will instead execute a search. It's a
+ // rare case anyway, most likely to happen for enterprises customizing
+ // the urifixup prefs.
+ if (
+ lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
+ lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
+ ) {
+ url = originalUntrimmedValue;
+ }
+ // Annotate if the untrimmed value contained a scheme, to later potentially
+ // be upgraded by schemeless HTTPS-First.
+ openParams.schemelessInput = this.#getSchemelessInput(
+ originalUntrimmedValue
+ );
+ }
+ break;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: {
+ // If this result comes from a bookmark keyword, let it inherit the
+ // current document's principal, otherwise bookmarklets would break.
+ openParams.allowInheritPrincipal = true;
+ break;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
+ // Behaviour is reversed with SecondaryActions, default behaviour is to navigate
+ // and button is provided to switch to tab.
+ if (
+ this.hasAttribute("action-override") ||
+ (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") &&
+ element?.dataset.action !== "tabswitch")
+ ) {
+ where = "current";
+ break;
+ }
+
+ // Keep the searchMode for telemetry since handleRevert sets it to null.
+ const searchMode = this.searchMode;
+ this.handleRevert();
+ let prevTab = this.window.gBrowser.selectedTab;
+ let loadOpts = {
+ adoptIntoActiveWindow: lazy.UrlbarPrefs.get(
+ "switchTabs.adoptIntoActiveWindow"
+ ),
+ };
+
+ // We cache the search string because switching tab may clear it.
+ let searchString = this._lastSearchString;
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString,
+ searchMode,
+ selType: this.controller.engagementEvent.typeFromElement(
+ result,
+ element
+ ),
+ });
+
+ let switched = this.window.switchToTabHavingURI(
+ Services.io.newURI(url),
+ true,
+ loadOpts,
+ lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
+ lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(
+ result.payload.userContextId
+ )
+ ? result.payload.userContextId
+ : null
+ );
+ if (switched && prevTab.isEmpty) {
+ this.window.gBrowser.removeTab(prevTab);
+ }
+
+ if (switched && !this.isPrivate && !result.heuristic) {
+ // We don't await for this, because a rejection should not interrupt
+ // the load. Just reportError it.
+ lazy.UrlbarUtils.addToInputHistory(url, searchString).catch(
+ console.error
+ );
+ }
+
+ // TODO (Bug 1865757): We should not show a "switchtotab" result for
+ // tabs that are not currently open. Find out why tabs are not being
+ // properly unregistered when they are being closed.
+ if (!switched) {
+ console.error(`Tried to switch to non-existent tab: ${url}`);
+ lazy.UrlbarProviderOpenTabs.unregisterOpenTab(
+ url,
+ result.payload.userContextId,
+ result.payload.tabGroup,
+ this.isPrivate
+ );
+ }
+
+ return;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
+ if (result.payload.providesSearchMode) {
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(
+ result,
+ element
+ ),
+ });
+ this.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ });
+ return;
+ }
+
+ if (
+ !this.searchMode &&
+ result.heuristic &&
+ // If we asked the DNS earlier, avoid the post-facto check.
+ !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
+ // TODO (bug 1642623): for now there is no smart heuristic to skip the
+ // DNS lookup, so any value above 0 will run it.
+ lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
+ this.window.gKeywordURIFixup &&
+ lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
+ ) {
+ // When fixing a single word to a search, the docShell would also
+ // query the DNS and if resolved ask the user whether they would
+ // rather visit that as a host. On a positive answer, it adds the host
+ // to the list that we use to make decisions.
+ // Because we are directly asking for a search here, bypassing the
+ // docShell, we need to do the same ourselves.
+ // See also URIFixupChild.sys.mjs and keyword-uri-fixup.
+ let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
+ if (fixupInfo) {
+ this.window.gKeywordURIFixup.check(
+ this.window.gBrowser.selectedBrowser,
+ fixupInfo
+ );
+ }
+ }
+
+ if (result.payload.inPrivateWindow) {
+ where = "window";
+ openParams.private = true;
+ }
+
+ const actionDetails = {
+ isSuggestion: !!result.payload.suggestion,
+ isFormHistory:
+ result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY,
+ alias: result.payload.keyword,
+ };
+ const engine = Services.search.getEngineByName(result.payload.engine);
+
+ if (where == "tab") {
+ // The TabOpen event is fired synchronously so tabEvent.target
+ // is guaranteed to be our new search tab.
+ this.window.gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ tabEvent =>
+ this._recordSearch(
+ engine,
+ event,
+ actionDetails,
+ tabEvent.target.linkedBrowser
+ ),
+ { once: true }
+ );
+ } else {
+ this._recordSearch(engine, event, actionDetails);
+ }
+
+ if (!result.payload.inPrivateWindow) {
+ lazy.UrlbarUtils.addToFormHistory(
+ this,
+ result.payload.suggestion || result.payload.query,
+ engine.name
+ ).catch(console.error);
+ }
+ break;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
+ if (url) {
+ break;
+ }
+ this.handleRevert();
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ selType: "tip",
+ searchString: this._lastSearchString,
+ });
+ return;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
+ if (!url) {
+ // If we're not loading a URL, the engagement is done. First revert
+ // and then record the engagement since providers expect the urlbar to
+ // be reverted when they're notified of the engagement, but before
+ // reverting, copy the search mode since it's nulled on revert.
+ const { searchMode } = this;
+ this.handleRevert();
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchMode,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(
+ result,
+ element
+ ),
+ });
+ return;
+ }
+ break;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: {
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ selType: "extension",
+ searchString: this._lastSearchString,
+ });
+
+ // The urlbar needs to revert to the loaded url when a command is
+ // handled by the extension.
+ this.handleRevert();
+ // We don't directly handle a load when an Omnibox API result is picked,
+ // instead we forward the request to the WebExtension itself, because
+ // the value may not even be a url.
+ // We pass the keyword and content, that actually is the retrieved value
+ // prefixed by the keyword. ExtensionSearchHandler uses this keyword
+ // redundancy as a sanity check.
+ lazy.ExtensionSearchHandler.handleInputEntered(
+ result.payload.keyword,
+ result.payload.content,
+ where
+ );
+ return;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: {
+ this.handleRevert();
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(
+ result,
+ element
+ ),
+ });
+ this.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ });
+
+ return;
+ }
+ }
+
+ if (!url) {
+ throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
+ }
+
+ // Record input history but only in non-private windows.
+ if (!this.isPrivate) {
+ let input;
+ if (!result.heuristic) {
+ input = this._lastSearchString;
+ } else if (result.autofill?.type == "adaptive") {
+ input = result.autofill.adaptiveHistoryInput;
+ }
+ // `input` may be an empty string, so do a strict comparison here.
+ if (input !== undefined) {
+ // We don't await for this, because a rejection should not interrupt
+ // the load. Just reportError it.
+ lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error);
+ }
+ }
+
+ this.controller.engagementEvent.startTrackingBounceEvent(browser, event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(result, element),
+ searchSource: this.getSearchSource(event),
+ });
+
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(result, element),
+ searchSource: this.getSearchSource(event),
+ });
+
+ if (result.payload.sendAttributionRequest) {
+ lazy.PartnerLinkAttribution.makeRequest({
+ targetURL: result.payload.url,
+ source: this.#sapName,
+ campaignID: Services.prefs.getStringPref(
+ "browser.partnerlink.campaign.topsites"
+ ),
+ });
+ if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") {
+ // The position is 1-based for telemetry
+ const position = result.rowIndex + 1;
+ Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1);
+ }
+ }
+
+ this._loadURL(
+ url,
+ event,
+ where,
+ openParams,
+ {
+ source: result.source,
+ type: result.type,
+ searchTerm: result.payload.suggestion ?? result.payload.query,
+ },
+ browser
+ );
+ }
+
+ /**
+ * Called by the view when moving through results with the keyboard, and when
+ * picking a result. This sets the input value to the value of the result and
+ * invalidates the pageproxystate. It also sets the result that is associated
+ * with the current input value. If you need to set this result but don't
+ * want to also set the input value, then use setResultForCurrentValue.
+ *
+ * @param {object} options
+ * Options.
+ * @param {UrlbarResult} [options.result]
+ * The result that was selected or picked, null if no result was selected.
+ * @param {Event} [options.event]
+ * The event that picked the result.
+ * @param {string} [options.urlOverride]
+ * Normally the URL is taken from `result.payload.url`, but if `urlOverride`
+ * is specified, it's used instead. See `#getValueFromResult()`.
+ * @param {Element} [options.element]
+ * The element that was selected or picked, if available. For results that
+ * have multiple selectable children, the value may be taken from a child
+ * element rather than the result. See `#getValueFromResult()`.
+ * @returns {boolean}
+ * Whether the value has been canonized
+ */
+ setValueFromResult({
+ result = null,
+ event = null,
+ urlOverride = null,
+ element = null,
+ } = {}) {
+ // Usually this is set by a previous input event, but in certain cases, like
+ // when opening Top Sites on a loaded page, it wouldn't happen. To avoid
+ // confusing the user, we always enforce it when a result changes our value.
+ this.setPageProxyState("invalid", true);
+
+ // A previous result may have previewed search mode. If we don't expect that
+ // we might stay in a search mode of some kind, exit it now.
+ if (
+ this.searchMode?.isPreview &&
+ !this.#providesSearchMode(result) &&
+ !this.view.oneOffSearchButtons?.selectedButton
+ ) {
+ this.searchMode = null;
+ }
+
+ if (!result) {
+ // This happens when there's no selection, for example when moving to the
+ // one-offs search settings button, or to the input field when Top Sites
+ // are shown; then we must reset the input value.
+ // Note that for Top Sites the last search string would be empty, thus we
+ // must restore the last text value.
+ // Note that unselected autofill results will still arrive in this
+ // function with a non-null `result`. They are handled below.
+ this.value = this._lastSearchString || this._valueOnLastSearch;
+ this.setResultForCurrentValue(result);
+ return false;
+ }
+
+ // We won't allow trimming when calling _setValue, since it makes too easy
+ // for the user to wrongly transform `https` into `http`, for example by
+ // picking a https://site/path_1 result and editing the path to path_2,
+ // then we'd end up visiting http://site/path_2.
+ // Trimming `http` would be ok, but there's other cases where it's unsafe,
+ // like transforming a url into a search.
+ // This choice also makes it easier to copy the full url of a result.
+
+ // We are supporting canonization of any result, in particular this allows
+ // for single word search suggestions to be converted to a .com URL.
+ // For autofilled results, the value to canonize is the user typed string,
+ // not the autofilled value.
+ let canonizedUrl = this._maybeCanonizeURL(
+ event,
+ result.autofill ? this._lastSearchString : this.value
+ );
+ if (canonizedUrl) {
+ this._setValue(canonizedUrl);
+
+ this.setResultForCurrentValue(result);
+ return true;
+ }
+
+ if (result.autofill) {
+ this._autofillValue(result.autofill);
+ }
+
+ if (this.#providesSearchMode(result)) {
+ let enteredSearchMode;
+ // Only preview search mode if the result is selected.
+ if (this.view.resultIsSelected(result)) {
+ // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used
+ // for selection of the urlbar results, keep the search mode as preview
+ // mode if there are multiple results.
+ // If ScotchBonnet is disabled, not starting a query means we will only
+ // preview search mode.
+ enteredSearchMode = this.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ startQuery:
+ lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") &&
+ this.view.visibleResults.length == 1,
+ });
+ }
+ if (!enteredSearchMode) {
+ this._setValue(this.#getValueFromResult(result), {
+ actionType: this.#getActionTypeFromResult(result),
+ });
+ this.searchMode = null;
+ }
+ this.setResultForCurrentValue(result);
+ return false;
+ }
+
+ if (!result.autofill) {
+ let value = this.#getValueFromResult(result, { urlOverride, element });
+ this._setValue(value, {
+ actionType: this.#getActionTypeFromResult(result),
+ });
+ }
+
+ this.setResultForCurrentValue(result);
+
+ // Update placeholder selection and value to the current selected result to
+ // prevent the on_selectionchange event to detect a "accent-character"
+ // insertion.
+ if (!result.autofill && this._autofillPlaceholder) {
+ this._autofillPlaceholder.value = this.value;
+ this._autofillPlaceholder.selectionStart = this.value.length;
+ this._autofillPlaceholder.selectionEnd = this.value.length;
+ }
+ return false;
+ }
+
+ /**
+ * The input keeps track of the result associated with the current input
+ * value. This result can be set by calling either setValueFromResult or this
+ * method. Use this method when you need to set the result without also
+ * setting the input value. This can be the case when either the selection is
+ * cleared and no other result becomes selected, or when the result is the
+ * heuristic and we don't want to modify the value the user is typing.
+ *
+ * @param {UrlbarResult} result
+ * The result to associate with the current input value.
+ */
+ setResultForCurrentValue(result) {
+ this._resultForCurrentValue = result;
+ }
+
+ /**
+ * Called by the controller when the first result of a new search is received.
+ * If it's an autofill result, then it may need to be autofilled, subject to a
+ * few restrictions.
+ *
+ * @param {UrlbarResult} result
+ * The first result.
+ */
+ _autofillFirstResult(result) {
+ if (!result.autofill) {
+ return;
+ }
+
+ let isPlaceholderSelected =
+ this._autofillPlaceholder &&
+ this.selectionEnd == this._autofillPlaceholder.value.length &&
+ this.selectionStart == this._lastSearchString.length &&
+ this._autofillPlaceholder.value
+ .toLocaleLowerCase()
+ .startsWith(this._lastSearchString.toLocaleLowerCase());
+
+ // Don't autofill if there's already a selection (with one caveat described
+ // next) or the cursor isn't at the end of the input. But if there is a
+ // selection and it's the autofill placeholder value, then do autofill.
+ if (
+ !isPlaceholderSelected &&
+ !this._autofillIgnoresSelection &&
+ (this.selectionStart != this.selectionEnd ||
+ this.selectionEnd != this._lastSearchString.length)
+ ) {
+ return;
+ }
+
+ this.setValueFromResult({ result });
+ }
+ /**
+ * Clears displayed autofill values and unsets the autofill placeholder.
+ */
+ #clearAutofill() {
+ if (!this._autofillPlaceholder) {
+ return;
+ }
+ let currentSelectionStart = this.selectionStart;
+ let currentSelectionEnd = this.selectionEnd;
+
+ // Overriding this value clears the selection.
+ this.inputField.value = this.value.substring(
+ 0,
+ this._autofillPlaceholder.selectionStart
+ );
+ this._autofillPlaceholder = null;
+ // Restore selection
+ this.setSelectionRange(currentSelectionStart, currentSelectionEnd);
+ }
+
+ /**
+ * Invoked by the controller when the first result is received.
+ *
+ * @param {UrlbarResult} firstResult
+ * The first result received.
+ * @returns {boolean}
+ * True if this method canceled the query and started a new one. False
+ * otherwise.
+ */
+ onFirstResult(firstResult) {
+ // If the heuristic result has a keyword but isn't a keyword offer, we may
+ // need to enter search mode.
+ if (
+ firstResult.heuristic &&
+ firstResult.payload.keyword &&
+ !this.#providesSearchMode(firstResult) &&
+ this.maybeConfirmSearchModeFromResult({
+ result: firstResult,
+ entry: "typed",
+ checkValue: false,
+ })
+ ) {
+ return true;
+ }
+
+ // To prevent selection flickering, we apply autofill on input through a
+ // placeholder, without waiting for results. But, if the first result is
+ // not an autofill one, the autofill prediction was wrong and we should
+ // restore the original user typed string.
+ if (firstResult.autofill) {
+ this._autofillFirstResult(firstResult);
+ } else if (
+ this._autofillPlaceholder &&
+ // Avoid clobbering added spaces (for token aliases, for example).
+ !this.value.endsWith(" ")
+ ) {
+ this._autofillPlaceholder = null;
+ this._setValue(this.userTypedValue);
+ }
+
+ return false;
+ }
+
+ /**
+ * Starts a query based on the current input value.
+ *
+ * @param {object} [options]
+ * Object options
+ * @param {boolean} [options.allowAutofill]
+ * Whether or not to allow providers to include autofill results.
+ * @param {boolean} [options.autofillIgnoresSelection]
+ * Normally we autofill only if the cursor is at the end of the string,
+ * if this is set we'll autofill regardless of selection.
+ * @param {string} [options.searchString]
+ * The search string. If not given, the current input value is used.
+ * Otherwise, the current input value must start with this value.
+ * @param {boolean} [options.resetSearchState]
+ * If this is the first search of a user interaction with the input, set
+ * this to true (the default) so that search-related state from the previous
+ * interaction doesn't interfere with the new interaction. Otherwise set it
+ * to false so that state is maintained during a single interaction. The
+ * intended use for this parameter is that it should be set to false when
+ * this method is called due to input events.
+ * @param {event} [options.event]
+ * The user-generated event that triggered the query, if any. If given, we
+ * will record engagement event telemetry for the query.
+ */
+ startQuery({
+ allowAutofill,
+ autofillIgnoresSelection = false,
+ searchString,
+ resetSearchState = true,
+ event,
+ } = {}) {
+ if (!searchString) {
+ searchString =
+ this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
+ } else if (!this.value.startsWith(searchString)) {
+ throw new Error("The current value doesn't start with the search string");
+ }
+
+ let queryContext = this.#makeQueryContext({
+ allowAutofill,
+ event,
+ searchString,
+ });
+
+ if (event) {
+ this.controller.engagementEvent.start(event, queryContext, searchString);
+ }
+
+ if (this._suppressStartQuery) {
+ return;
+ }
+
+ this._autofillIgnoresSelection = autofillIgnoresSelection;
+ if (resetSearchState) {
+ this._resetSearchState();
+ }
+
+ if (this.searchMode) {
+ this.confirmSearchMode();
+ }
+
+ this._lastSearchString = searchString;
+ this._valueOnLastSearch = this.value;
+
+ // TODO (Bug 1522902): This promise is necessary for tests, because some
+ // tests are not listening for completion when starting a query through
+ // other methods than startQuery (input events for example).
+ this.lastQueryContextPromise = this.controller.startQuery(queryContext);
+ }
+
+ /**
+ * Sets the input's value, starts a search, and opens the view.
+ *
+ * @param {string} value
+ * The input's value will be set to this value, and the search will
+ * use it as its query.
+ * @param {object} [options]
+ * Object options
+ * @param {nsISearchEngine} [options.searchEngine]
+ * Search engine to use when the search is using a known alias.
+ * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
+ * If provided, we will record this parameter as the search mode entry point
+ * in Telemetry. Consumers should provide this if they expect their call
+ * to enter search mode.
+ * @param {boolean} [options.focus]
+ * If true, the urlbar will be focused. If false, the focus will remain
+ * unchanged.
+ * @param {boolean} [options.startQuery]
+ * If true, start query to show urlbar result by fireing input event. If
+ * false, not fire the event.
+ */
+ search(value, options = {}) {
+ let { searchEngine, searchModeEntry, startQuery = true } = options;
+ if (options.focus ?? true) {
+ this.focus();
+ }
+ let trimmedValue = value.trim();
+ let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES);
+ let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end);
+ // Enter search mode if the string starts with a restriction token.
+ let searchMode = this.searchModeForToken(firstToken);
+ let firstTokenIsRestriction = !!searchMode;
+ if (!searchMode && searchEngine) {
+ searchMode = { engineName: searchEngine.name };
+ firstTokenIsRestriction = searchEngine.aliases.includes(firstToken);
+ }
+
+ if (searchMode) {
+ searchMode.entry = searchModeEntry;
+ this.searchMode = searchMode;
+ if (firstTokenIsRestriction) {
+ // Remove the restriction token/alias from the string to be searched for
+ // in search mode.
+ value = value.replace(firstToken, "");
+ }
+ if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) {
+ // If there was a trailing space after the restriction token/alias,
+ // remove it.
+ value = value.slice(1);
+ }
+ } else if (
+ Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken)
+ ) {
+ this.searchMode = null;
+ // If the entire value is a restricted token, append a space.
+ if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) {
+ value += " ";
+ }
+ }
+ this.inputField.value = value;
+ // Avoid selecting the text if this method is called twice in a row.
+ this.selectionStart = -1;
+
+ if (startQuery) {
+ // Note: proper IME Composition handling depends on the fact this generates
+ // an input event, rather than directly invoking the controller; everything
+ // goes through _on_input, that will properly skip the search until the
+ // composition is committed. _on_input also skips the search when it's the
+ // same as the previous search, but we want to allow consecutive searches
+ // with the same string. So clear _lastSearchString first.
+ this._lastSearchString = "";
+ let event = new UIEvent("input", {
+ bubbles: true,
+ cancelable: false,
+ view: this.window,
+ detail: 0,
+ });
+ this.inputField.dispatchEvent(event);
+ }
+ }
+
+ /**
+ * Returns a search mode object if a token should enter search mode when
+ * typed. This does not handle engine aliases.
+ *
+ * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token
+ * A restriction token to convert to search mode.
+ * @returns {?object}
+ * A search mode object. Null if search mode should not be entered. See
+ * setSearchMode documentation for details.
+ */
+ searchModeForToken(token) {
+ if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) {
+ return {
+ engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)
+ ?.name,
+ };
+ }
+
+ let mode =
+ this.#isAddressbar &&
+ lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
+ if (mode) {
+ // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
+ return { ...mode };
+ }
+
+ return null;
+ }
+
+ /**
+ * Opens a search page if the value is non-empty, otherwise opens the
+ * search engine homepage (searchform).
+ *
+ * @param {string} value
+ * @param {object} options
+ * @param {nsISearchEngine} options.searchEngine
+ */
+ openEngineHomePage(value, { searchEngine }) {
+ if (!searchEngine) {
+ console.warn("No searchEngine parameter");
+ return;
+ }
+
+ let trimmedValue = value.trim();
+ let url;
+ if (trimmedValue) {
+ url = searchEngine.getSubmission(trimmedValue, null).uri.spec;
+ // TODO: record SAP telemetry, see Bug 1961789.
+ } else {
+ url = searchEngine.searchForm;
+ lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName);
+ }
+
+ this._lastSearchString = "";
+ if (this.#isAddressbar) {
+ this.inputField.value = url;
+ }
+ this.selectionStart = -1;
+
+ this.window.openTrustedLinkIn(url, "current");
+ }
+
+ /**
+ * Focus without the focus styles.
+ * This is used by Activity Stream and about:privatebrowsing for search hand-off.
+ */
+ setHiddenFocus() {
+ this._hideFocus = true;
+ if (this.focused) {
+ this.removeAttribute("focused");
+ } else {
+ this.focus();
+ }
+ }
+
+ /**
+ * Restore focus styles.
+ * This is used by Activity Stream and about:privatebrowsing for search hand-off.
+ *
+ * @param {boolean} forceSuppressFocusBorder
+ * Set true to suppress-focus-border attribute if this flag is true.
+ */
+ removeHiddenFocus(forceSuppressFocusBorder = false) {
+ this._hideFocus = false;
+ if (this.focused) {
+ this.toggleAttribute("focused", true);
+
+ if (forceSuppressFocusBorder) {
+ this.toggleAttribute("suppress-focus-border", true);
+ }
+ }
+ }
+
+ /**
+ * Addressbar: Gets the search mode for a specific browser instance.
+ * Searchbar: Gets the window-global search mode.
+ *
+ * @param {MozBrowser} browser
+ * The search mode for this browser will be returned.
+ * Pass the selected browser for the searchbar.
+ * @param {boolean} [confirmedOnly]
+ * Normally, if the browser has both preview and confirmed modes, preview
+ * mode will be returned since it takes precedence. If this argument is
+ * true, then only confirmed search mode will be returned, or null if
+ * search mode hasn't been confirmed.
+ * @returns {?object}
+ * A search mode object or null if the browser/window is not in search mode.
+ * See setSearchMode documentation.
+ */
+ getSearchMode(browser, confirmedOnly = false) {
+ let modes = this.#getSearchModesObject(browser);
+
+ // Return copies so that callers don't modify the stored values.
+ if (!confirmedOnly && modes.preview) {
+ return { ...modes.preview };
+ }
+ if (modes.confirmed) {
+ return { ...modes.confirmed };
+ }
+ return null;
+ }
+
+ /**
+ * Addressbar: Sets the search mode for a specific browser instance.
+ * Searchbar: Sets the window-global search mode.
+ * If the given browser is selected, then this will also enter search mode.
+ *
+ * @param {object} searchMode
+ * A search mode object.
+ * @param {string} searchMode.engineName
+ * The name of the search engine to restrict to.
+ * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
+ * A result source to restrict to.
+ * @param {string} searchMode.entry
+ * How search mode was entered. This is recorded in event telemetry. One of
+ * the values in UrlbarUtils.SEARCH_MODE_ENTRY.
+ * @param {boolean} [searchMode.isPreview]
+ * If true, we will preview search mode. Search mode preview does not record
+ * telemetry and has slighly different UI behavior. The preview is exited in
+ * favor of full search mode when a query is executed. False should be
+ * passed if the caller needs to enter search mode but expects it will not
+ * be interacted with right away. Defaults to true.
+ * @param {MozBrowser} browser
+ * The browser for which to set search mode.
+ * Pass the selected browser for the searchbar.
+ */
+ async setSearchMode(searchMode, browser) {
+ let currentSearchMode = this.getSearchMode(browser);
+ let areSearchModesSame =
+ (!currentSearchMode && !searchMode) ||
+ lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode);
+
+ // Exit search mode if the passed-in engine is invalid or hidden.
+ let engine;
+ if (searchMode?.engineName) {
+ if (!Services.search.isInitialized) {
+ await Services.search.init();
+ }
+ engine = Services.search.getEngineByName(searchMode.engineName);
+ if (!engine || engine.hidden) {
+ searchMode = null;
+ }
+ }
+
+ let {
+ engineName,
+ source,
+ entry,
+ restrictType,
+ isPreview = true,
+ } = searchMode || {};
+
+ searchMode = null;
+
+ if (engineName) {
+ searchMode = {
+ engineName,
+ isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
+ };
+ if (source) {
+ searchMode.source = source;
+ } else if (searchMode.isGeneralPurposeEngine) {
+ // History results for general-purpose search engines are often not
+ // useful, so we hide them in search mode. See bug 1658646 for
+ // discussion.
+ searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ } else if (source) {
+ let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
+ if (sourceName) {
+ searchMode = { source };
+ } else {
+ console.error(`Unrecognized source: ${source}`);
+ }
+ }
+
+ let modes = this.#getSearchModesObject(browser);
+
+ if (searchMode) {
+ searchMode.isPreview = isPreview;
+ if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
+ searchMode.entry = entry;
+ } else {
+ // If we see this value showing up in telemetry, we should review
+ // search mode's entry points.
+ searchMode.entry = "other";
+ }
+
+ if (!searchMode.isPreview) {
+ modes.confirmed = searchMode;
+ delete modes.preview;
+ } else {
+ modes.preview = searchMode;
+ }
+ } else {
+ delete modes.preview;
+ delete modes.confirmed;
+ }
+
+ if (restrictType) {
+ searchMode.restrictType = restrictType;
+ }
+
+ // Enter search mode if the browser is selected.
+ if (browser == this.window.gBrowser.selectedBrowser) {
+ this._updateSearchModeUI(searchMode);
+ if (searchMode) {
+ // Set userTypedValue to the query string so that it's properly restored
+ // when switching back to the current tab and across sessions.
+ this.userTypedValue = this.untrimmedValue;
+ this.valueIsTyped = true;
+ if (!searchMode.isPreview && !areSearchModesSame) {
+ try {
+ lazy.BrowserSearchTelemetry.recordSearchMode(searchMode);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @typedef {object} SearchModesObject
+ *
+ * @property {object} [preview] preview search mode
+ * @property {object} [confirmed] confirmed search mode
+ */
+
+ /**
+ * @type {SearchModesObject|undefined}
+ *
+ * The (lazily initialized) search mode object for the searchbar.
+ * This is needed because the searchbar has one search mode per window that
+ * shouldn't change when switching tabs. For the address bar, the search mode
+ * is stored per browser in #browserStates and this is always undefined.
+ */
+ #searchbarSearchModes;
+
+ /**
+ * Addressbar: Gets the search modes object for a specific browser instance.
+ * Searchbar: Gets the window-global search modes object.
+ *
+ * @param {MozBrowser} browser
+ * The browser to get the search modes object for.
+ * Pass the selected browser for the searchbar.
+ * @returns {SearchModesObject}
+ */
+ #getSearchModesObject(browser) {
+ if (!this.#isAddressbar) {
+ // The passed browser doesn't matter here, but it does in setSearchMode.
+ this.#searchbarSearchModes ??= {};
+ return this.#searchbarSearchModes;
+ }
+
+ let state = this.getBrowserState(browser);
+ state.searchModes ??= {};
+ return state.searchModes;
+ }
+
+ /**
+ * Restores the current browser search mode from a previously stored state.
+ */
+ restoreSearchModeState() {
+ this.searchMode = this.#getSearchModesObject(
+ this.window.gBrowser.selectedBrowser
+ ).confirmed;
+ }
+
+ /**
+ * Enters search mode with the default engine.
+ */
+ searchModeShortcut() {
+ // We restrict to search results when entering search mode from this
+ // shortcut to honor historical behaviour.
+ this.searchMode = {
+ source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name,
+ entry: "shortcut",
+ };
+ // The searchMode setter clears the input if pageproxystate is valid, so
+ // we know at this point this.value will either be blank or the user's
+ // typed string.
+ this.search(this.value);
+ this.select();
+ }
+
+ /**
+ * Confirms the current search mode.
+ */
+ confirmSearchMode() {
+ let searchMode = this.searchMode;
+ if (searchMode?.isPreview) {
+ searchMode.isPreview = false;
+ this.searchMode = searchMode;
+
+ // Unselect the one-off search button to ensure UI consistency.
+ if (this.view.oneOffSearchButtons) {
+ this.view.oneOffSearchButtons.selectedButton = null;
+ }
+ }
+ }
+
+ // Getters and Setters below.
+
+ get editor() {
+ return this.inputField.editor;
+ }
+
+ get focused() {
+ return this.document.activeElement == this.inputField;
+ }
+
+ get goButton() {
+ return this.querySelector(".urlbar-go-button");
+ }
+
+ get value() {
+ return this.inputField.value;
+ }
+
+ set value(val) {
+ this._setValue(val, { allowTrim: true });
+ }
+
+ get untrimmedValue() {
+ return this._untrimmedValue;
+ }
+
+ get userTypedValue() {
+ return this.#isAddressbar
+ ? this.window.gBrowser.userTypedValue
+ : this._userTypedValue;
+ }
+
+ set userTypedValue(val) {
+ if (this.#isAddressbar) {
+ this.window.gBrowser.userTypedValue = val;
+ } else {
+ this._userTypedValue = val;
+ }
+ }
+
+ get lastSearchString() {
+ return this._lastSearchString;
+ }
+
+ get searchMode() {
+ if (!this.window.gBrowser) {
+ // This only happens before DOMContentLoaded.
+ return null;
+ }
+ return this.getSearchMode(this.window.gBrowser.selectedBrowser);
+ }
+
+ set searchMode(searchMode) {
+ this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
+ this.searchModeSwitcher?.onSearchModeChanged();
+ lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window);
+ }
+
+ getBrowserState(browser) {
+ let state = this.#browserStates.get(browser);
+ if (!state) {
+ state = {};
+ this.#browserStates.set(browser, state);
+ }
+ return state;
+ }
+
+ async #updateLayoutBreakout() {
+ if (!this.#allowBreakout) {
+ return;
+ }
+ if (this.document.fullscreenElement) {
+ // Toolbars are hidden in DOM fullscreen mode, so we can't get proper
+ // layout information and need to retry after leaving that mode.
+ this.window.addEventListener(
+ "fullscreen",
+ () => {
+ this.#updateLayoutBreakout();
+ },
+ { once: true }
+ );
+ return;
+ }
+ await this.#updateLayoutBreakoutDimensions();
+ }
+
+ startLayoutExtend() {
+ if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
+ // Do not expand if the Urlbar does not support being expanded or it is
+ // already expanded.
+ return;
+ }
+ if (!this.view.isOpen) {
+ return;
+ }
+
+ this.#updateTextboxPosition();
+
+ this.setAttribute("breakout-extend", "true");
+
+ // Enable the animation only after the first extend call to ensure it
+ // doesn't run when opening a new window.
+ if (!this.hasAttribute("breakout-extend-animate")) {
+ this.window.promiseDocumentFlushed(() => {
+ this.window.requestAnimationFrame(() => {
+ this.setAttribute("breakout-extend-animate", "true");
+ });
+ });
+ }
+ }
+
+ endLayoutExtend() {
+ // If reduce motion is enabled, we want to collapse the Urlbar here so the
+ // user sees only sees two states: not expanded, and expanded with the view
+ // open.
+ if (!this.hasAttribute("breakout-extend") || this.view.isOpen) {
+ return;
+ }
+
+ this.removeAttribute("breakout-extend");
+ this.#updateTextboxPosition();
+ }
+
+ /**
+ * Updates the user interface to indicate whether the URI in the address bar
+ * is different than the loaded page, because it's being edited or because a
+ * search result is currently selected and is displayed in the location bar.
+ *
+ * @param {string} state
+ * The string "valid" indicates that the security indicators and other
+ * related user interface elments should be shown because the URI in
+ * the location bar matches the loaded page. The string "invalid"
+ * indicates that the URI in the location bar is different than the
+ * loaded page.
+ * @param {boolean} [updatePopupNotifications]
+ * Indicates whether we should update the PopupNotifications
+ * visibility due to this change, otherwise avoid doing so as it is
+ * being handled somewhere else.
+ * @param {boolean} [forceUnifiedSearchButtonAvailable]
+ * If this parameter is true, force to make Unified Search Button available.
+ * Otherwise, the availability will be depedent on the proxy state.
+ * Default value is false.
+ */
+ setPageProxyState(
+ state,
+ updatePopupNotifications,
+ forceUnifiedSearchButtonAvailable = false
+ ) {
+ let prevState = this.getAttribute("pageproxystate");
+
+ this.setAttribute("pageproxystate", state);
+ this._inputContainer.setAttribute("pageproxystate", state);
+ this._identityBox?.setAttribute("pageproxystate", state);
+ this.setUnifiedSearchButtonAvailability(
+ forceUnifiedSearchButtonAvailable || state == "invalid"
+ );
+
+ if (state == "valid") {
+ this._lastValidURLStr = this.value;
+ }
+
+ if (
+ updatePopupNotifications &&
+ prevState != state &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+ }
+
+ /**
+ * When switching tabs quickly, TabSelect sometimes happens before
+ * _adjustFocusAfterTabSwitch and due to the focus still being on the old
+ * tab, we end up flickering the results pane briefly.
+ */
+ afterTabSwitchFocusChange() {
+ this._gotFocusChange = true;
+ this._afterTabSelectAndFocusChange();
+ }
+
+ /**
+ * Confirms search mode and starts a new search if appropriate for the given
+ * result. See also _searchModeForResult.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} [options.entry]
+ * If provided, this will be recorded as the entry point into search mode.
+ * See setSearchMode documentation for details.
+ * @param {UrlbarResult} [options.result]
+ * The result to confirm. Defaults to the currently selected result.
+ * @param {boolean} [options.checkValue]
+ * If true, the trimmed input value must equal the result's keyword in order
+ * to enter search mode.
+ * @param {boolean} [options.startQuery]
+ * If true, start a query after entering search mode. Defaults to true.
+ * @returns {boolean}
+ * True if we entered search mode and false if not.
+ */
+ maybeConfirmSearchModeFromResult({
+ entry,
+ result = this._resultForCurrentValue,
+ checkValue = true,
+ startQuery = true,
+ }) {
+ if (
+ !result ||
+ (checkValue &&
+ this.value.trim() != result.payload.keyword?.trim() &&
+ this.value.trim() != result.payload.autofillKeyword?.trim())
+ ) {
+ return false;
+ }
+
+ let searchMode = this._searchModeForResult(result, entry);
+ if (!searchMode) {
+ return false;
+ }
+
+ this.searchMode = searchMode;
+
+ let value = result.payload.query?.trimStart() || "";
+ this._setValue(value);
+
+ if (startQuery) {
+ this.startQuery({ allowAutofill: false });
+ }
+
+ return true;
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ switch (data) {
+ case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
+ case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: {
+ let searchMode = this.searchMode;
+ if (searchMode?.engineName == engine.name) {
+ // Exit search mode if the current search mode engine was removed.
+ this.searchMode = searchMode;
+ }
+ break;
+ }
+ case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT:
+ if (!this.isPrivate) {
+ this._updatePlaceholder(engine.name);
+ }
+ break;
+ case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE:
+ if (this.isPrivate) {
+ this._updatePlaceholder(engine.name);
+ }
+ break;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get search source.
+ *
+ * @param {Event} event
+ * The event that triggered this query.
+ * @returns {string}
+ * The source name.
+ */
+ getSearchSource(event) {
+ if (this.#isAddressbar) {
+ if (this._isHandoffSession) {
+ return "urlbar-handoff";
+ }
+
+ const isOneOff =
+ this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
+ if (this.searchMode && !isOneOff) {
+ // Without checking !isOneOff, we might record the string
+ // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
+ // oneoff_urlbar and oneoff_searchbar). The extra information is not
+ // necessary; the intent is the same regardless of whether the user is
+ // in search mode when they do a key-modified click/enter on a one-off.
+ return "urlbar-searchmode";
+ }
+
+ let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
+ if (state.persist?.searchTerms && !isOneOff) {
+ // Normally, we use state.persist.shouldPersist to check if search terms
+ // persisted. However when the user modifies the search term, the boolean
+ // will become false. Thus, we check the presence of the search terms to
+ // know whether or not search terms ever persisted in the address bar.
+ return "urlbar-persisted";
+ }
+ }
+ return this.#sapName;
+ }
+
+ // Private methods below.
+
+ /*
+ * Actions can have several buttons in the same result where not all
+ * will provide a searchMode so check the currently selected button
+ * in that case.
+ */
+ #providesSearchMode(result) {
+ if (!result) {
+ return false;
+ }
+ if (
+ this.view.selectedElement &&
+ result.providerName == lazy.UrlbarProviderGlobalActions.name
+ ) {
+ return this.view.selectedElement.dataset.providesSearchmode == "true";
+ }
+ return result.payload.providesSearchMode;
+ }
+
+ _addObservers() {
+ this._observer ??= {
+ observe: this.observe.bind(this),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ Services.obs.addObserver(
+ this._observer,
+ lazy.SearchUtils.TOPIC_ENGINE_MODIFIED,
+ true
+ );
+ }
+
+ _removeObservers() {
+ if (this._observer) {
+ Services.obs.removeObserver(
+ this._observer,
+ lazy.SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ this._observer = null;
+ }
+ }
+
+ _getURIFixupInfo(searchString) {
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ try {
+ return Services.uriFixup.getFixupURIInfo(searchString, flags);
+ } catch (ex) {
+ console.error(
+ `An error occured while trying to fixup "${searchString}"`,
+ ex
+ );
+ }
+ return null;
+ }
+
+ _afterTabSelectAndFocusChange() {
+ // We must have seen both events to proceed safely.
+ if (!this._gotFocusChange || !this._gotTabSelect) {
+ return;
+ }
+ this._gotFocusChange = this._gotTabSelect = false;
+
+ this.formatValue();
+ this._resetSearchState();
+
+ // We don't use the original TabSelect event because caching it causes
+ // leaks on MacOS.
+ const event = new CustomEvent("tabswitch");
+ // If the urlbar is focused after a tab switch, record a potential
+ // engagement event. When switching from a focused to a non-focused urlbar,
+ // the blur event would record the abandonment. When switching from an
+ // unfocused to a focused urlbar, there should be no search session ongoing,
+ // so this will be a no-op.
+ if (this.focused) {
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ searchSource: this.getSearchSource(event),
+ });
+ }
+
+ // Switching tabs doesn't always change urlbar focus, so we must try to
+ // reopen here too, not just on focus.
+ if (this.view.autoOpen({ event })) {
+ return;
+ }
+ // The input may retain focus when switching tabs in which case we
+ // need to close the view and search mode switcher popup explicitly.
+ this.searchModeSwitcher.closePanel();
+ this.view.close();
+ }
+
+ #updateTextboxPosition() {
+ if (!this.view.isOpen) {
+ this.style.top = "";
+ return;
+ }
+ this.style.top = px(
+ this.parentNode.getBoxQuads({
+ ignoreTransforms: true,
+ flush: false,
+ })[0].p1.y
+ );
+ }
+
+ #updateTextboxPositionNextFrame() {
+ if (!this.hasAttribute("breakout")) {
+ return;
+ }
+ // Allow for any layout changes to take place (e.g. when the menubar becomes
+ // inactive) before re-measuring to position the textbox
+ this.window.requestAnimationFrame(() => {
+ this.window.requestAnimationFrame(() => {
+ this.#updateTextboxPosition();
+ });
+ });
+ }
+
+ #stopBreakout() {
+ this.removeAttribute("breakout");
+ this.parentNode.removeAttribute("breakout");
+ this.style.top = "";
+ try {
+ this.hidePopover();
+ } catch (ex) {
+ // No big deal if not a popover already.
+ }
+ this._layoutBreakoutUpdateKey = {};
+ }
+
+ incrementBreakoutBlockerCount() {
+ this.#breakoutBlockerCount++;
+ if (this.#breakoutBlockerCount == 1) {
+ this.#stopBreakout();
+ }
+ }
+
+ decrementBreakoutBlockerCount() {
+ if (this.#breakoutBlockerCount > 0) {
+ this.#breakoutBlockerCount--;
+ }
+ if (this.#breakoutBlockerCount === 0) {
+ this.#updateLayoutBreakout();
+ }
+ }
+
+ async #updateLayoutBreakoutDimensions() {
+ this.#stopBreakout();
+
+ // When this method gets called a second time before the first call
+ // finishes, we need to disregard the first one.
+ let updateKey = {};
+ this._layoutBreakoutUpdateKey = updateKey;
+ await this.window.promiseDocumentFlushed(() => {});
+ await new Promise(resolve => {
+ this.window.requestAnimationFrame(() => {
+ if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) {
+ return;
+ }
+
+ this.parentNode.style.setProperty(
+ "--urlbar-container-height",
+ px(getBoundsWithoutFlushing(this.parentNode).height)
+ );
+ this.style.setProperty(
+ "--urlbar-height",
+ px(getBoundsWithoutFlushing(this).height)
+ );
+
+ if (this.#breakoutBlockerCount) {
+ return;
+ }
+
+ this.setAttribute("breakout", "true");
+ this.parentNode.setAttribute("breakout", "true");
+ this.showPopover();
+ this.#updateTextboxPosition();
+
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Sets the input field value.
+ *
+ * @param {string} val The new value to set.
+ * @param {object} [options] Options for setting.
+ * @param {boolean} [options.allowTrim] Whether the value can be trimmed.
+ * @param {string} [options.untrimmedValue] Override for this._untrimmedValue.
+ * @param {boolean} [options.valueIsTyped] Override for this.valueIsTypede.
+ * @param {string} [options.actionType] Value for the `actiontype` attribute.
+ *
+ * @returns {string} The set value.
+ */
+ _setValue(
+ val,
+ {
+ allowTrim = false,
+ untrimmedValue = null,
+ valueIsTyped = false,
+ actionType = undefined,
+ } = {}
+ ) {
+ // Don't expose internal about:reader URLs to the user.
+ let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
+ if (originalUrl) {
+ val = originalUrl.displaySpec;
+ }
+ this._untrimmedValue = untrimmedValue ?? val;
+ this._protocolIsTrimmed = false;
+ if (allowTrim) {
+ let oldVal = val;
+ val = this._trimValue(val);
+ this._protocolIsTrimmed =
+ oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
+ !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol);
+ }
+
+ this.valueIsTyped = valueIsTyped;
+ this._resultForCurrentValue = null;
+ this.inputField.value = val;
+ this.formatValue();
+
+ if (actionType !== undefined) {
+ this.setAttribute("actiontype", actionType);
+ } else {
+ this.removeAttribute("actiontype");
+ }
+
+ // Dispatch ValueChange event for accessibility.
+ let event = this.document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ this.inputField.dispatchEvent(event);
+
+ return val;
+ }
+
+ /**
+ * Extracts a input value from a UrlbarResult, used when filling the input
+ * field on selecting a result.
+ *
+ * Some examples:
+ * - If the result is a bookmark keyword or dynamic, the value will be
+ * its `input` property.
+ * - If the result is search, the value may be `keyword` combined with
+ * `suggestion` or `query`.
+ * - If the result is WebExtension Omnibox, the value will be extracted
+ * from `content`.
+ * - For results returning URLs the value may be `urlOverride` or `url`.
+ *
+ * @param {UrlbarResult} result
+ * The result to extract the value from.
+ * @param {object} options
+ * Options object.
+ * @param {string} [options.urlOverride]
+ * For results normally returning a url string, this allows to override
+ * it. A blank string may passed-in to clear the input.
+ * @param {HTMLElement} [options.element]
+ * The element that was selected or picked, if available. For results that
+ * have multiple selectable children, the value may be taken from a child
+ * element rather than the result.
+ * @returns {string} The value.
+ */
+ #getValueFromResult(result, { urlOverride = null, element = null } = {}) {
+ switch (result.type) {
+ case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
+ return result.payload.input;
+ case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
+ let value = "";
+ if (result.payload.keyword) {
+ value += result.payload.keyword + " ";
+ }
+ value += result.payload.suggestion || result.payload.query;
+ return value;
+ }
+ case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return result.payload.content;
+ case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
+ return (
+ element?.dataset.query ||
+ element?.dataset.url ||
+ result.payload.input ||
+ result.payload.query ||
+ ""
+ );
+ case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT:
+ return result.payload.autofillKeyword + " ";
+ case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
+ let value = element?.dataset.url || element?.dataset.input;
+ if (value) {
+ return value;
+ }
+ break;
+ }
+ }
+
+ // Always respect a set urlOverride property.
+ if (urlOverride !== null) {
+ // This returns null for the empty string, allowing callers to clear the
+ // input by passing an empty string as urlOverride.
+ let url = URL.parse(urlOverride);
+ return url ? losslessDecodeURI(url.URI) : "";
+ }
+
+ let parsedUrl = URL.parse(result.payload.url);
+ // If the url is not parsable, just return an empty string;
+ if (!parsedUrl) {
+ return "";
+ }
+
+ let url = losslessDecodeURI(parsedUrl.URI);
+ // If the user didn't originally type a protocol, and we generated one,
+ // trim the http protocol from the input value, as https-first may upgrade
+ // it to https, breaking user expectations.
+ let stripHttp =
+ result.heuristic &&
+ result.payload.url.startsWith("http://") &&
+ this.userTypedValue &&
+ this.#getSchemelessInput(this.userTypedValue) ==
+ Ci.nsILoadInfo.SchemelessInputTypeSchemeless;
+ if (!stripHttp) {
+ return url;
+ }
+ // Attempt to trim the url. If doing so results in a string that is
+ // interpreted as search (e.g. unknown single word host, or domain suffix),
+ // use the unmodified url instead. Otherwise, if the user edits the url
+ // and confirms the new value, we may transform the url into a search.
+ let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0];
+ let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent;
+ if (isSearch) {
+ // Although https-first might not respect the shown protocol, converting
+ // the result to a search would be more disruptive.
+ return url;
+ }
+ return trimmedUrl;
+ }
+
+ /**
+ * Extracts from a result the value to use for the `actiontype` attribute.
+ *
+ * @param {UrlbarResult} result The UrlbarResult to consider.
+ *
+ * @returns {string} The `actiontype` value, or undefined.
+ */
+ #getActionTypeFromResult(result) {
+ switch (result.type) {
+ case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ return "switchtab";
+ case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return "extension";
+ default:
+ return undefined;
+ }
+ }
+
+ /**
+ * Resets some state so that searches from the user's previous interaction
+ * with the input don't interfere with searches from a new interaction.
+ */
+ _resetSearchState() {
+ this._lastSearchString = this.value;
+ this._autofillPlaceholder = null;
+ }
+
+ /**
+ * Autofills the autofill placeholder string if appropriate, and determines
+ * whether autofill should be allowed for the new search started by an input
+ * event.
+ *
+ * @param {string} value
+ * The new search string.
+ * @returns {boolean}
+ * Whether autofill should be allowed in the new search.
+ */
+ _maybeAutofillPlaceholder(value) {
+ // We allow autofill in local but not remote search modes.
+ let allowAutofill =
+ this.selectionEnd == value.length &&
+ !this.searchMode?.engineName &&
+ this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
+
+ if (!allowAutofill) {
+ this.#clearAutofill();
+ return false;
+ }
+
+ // Determine whether we can autofill the placeholder. The placeholder is a
+ // value that we autofill now, when the search starts and before we wait on
+ // its first result, in order to prevent a flicker in the input caused by
+ // the previous autofilled substring disappearing and reappearing when the
+ // first result arrives. Of course we can only autofill the placeholder if
+ // it starts with the new search string, and we shouldn't autofill anything
+ // if the caret isn't at the end of the input.
+ let canAutofillPlaceholder = false;
+ if (this._autofillPlaceholder) {
+ if (this._autofillPlaceholder.type == "adaptive") {
+ canAutofillPlaceholder =
+ value.length >=
+ this._autofillPlaceholder.adaptiveHistoryInput.length &&
+ this._autofillPlaceholder.value
+ .toLocaleLowerCase()
+ .startsWith(value.toLocaleLowerCase());
+ } else {
+ canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL(
+ this._autofillPlaceholder.value,
+ value
+ );
+ }
+ }
+
+ if (!canAutofillPlaceholder) {
+ this._autofillPlaceholder = null;
+ } else if (
+ this._autofillPlaceholder &&
+ this.selectionEnd == this.value.length &&
+ this._enableAutofillPlaceholder
+ ) {
+ let autofillValue =
+ value + this._autofillPlaceholder.value.substring(value.length);
+ this._autofillValue({
+ value: autofillValue,
+ selectionStart: value.length,
+ selectionEnd: autofillValue.length,
+ type: this._autofillPlaceholder.type,
+ adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
+ untrimmedValue: this._autofillPlaceholder.untrimmedValue,
+ });
+ }
+
+ return true;
+ }
+
+ /**
+ * Invoked on overflow/underflow/scrollend events to update attributes
+ * related to the input text directionality. Overflow fade masks use these
+ * attributes to appear at the proper side of the urlbar.
+ */
+ updateTextOverflow() {
+ if (!this._overflowing) {
+ this.removeAttribute("textoverflow");
+ return;
+ }
+
+ let isRTL =
+ this.getAttribute("domaindir") === "rtl" &&
+ lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window);
+
+ this.window.promiseDocumentFlushed(() => {
+ // Check overflow again to ensure it didn't change in the meanwhile.
+ let input = this.inputField;
+ if (input && this._overflowing) {
+ // Normally we overflow at the end side of the text direction, though
+ // RTL domains may cause us to overflow at the opposite side.
+ // The outcome differs depending on the input field contents and applied
+ // formatting, and reports the final state of all the scrolling into an
+ // attribute available to css rules.
+ // Note it's also possible to scroll an unfocused input field using
+ // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
+ // scroll (without modifiers) on Mac.
+ let side = "both";
+ if (isRTL) {
+ if (input.scrollLeft == 0) {
+ side = "left";
+ } else if (input.scrollLeft == input.scrollLeftMin) {
+ side = "right";
+ }
+ } else if (input.scrollLeft == 0) {
+ side = "right";
+ } else if (input.scrollLeft == input.scrollLeftMax) {
+ side = "left";
+ }
+
+ this.window.requestAnimationFrame(() => {
+ // And check once again, since we might have stopped overflowing
+ // since the promiseDocumentFlushed callback fired.
+ if (this._overflowing) {
+ this.setAttribute("textoverflow", side);
+ }
+ });
+ }
+ });
+ }
+
+ _updateUrlTooltip() {
+ if (this.focused || !this._overflowing) {
+ this.inputField.removeAttribute("title");
+ } else {
+ this.inputField.setAttribute("title", this.untrimmedValue);
+ }
+ }
+
+ _getSelectedValueForClipboard() {
+ let selectedVal = this.#selectedText;
+
+ // Handle multiple-range selection as a string for simplicity.
+ if (this.editor.selection.rangeCount > 1) {
+ return selectedVal;
+ }
+
+ // If the selection doesn't start at the beginning or doesn't span the
+ // full domain or the URL bar is modified or there is no text at all,
+ // nothing else to do here.
+ // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often
+ // it doesn't really indicate a user typed a value, it's rather used as
+ // a way to tell if the value was modified.
+ if (
+ this.selectionStart > 0 ||
+ selectedVal == "" ||
+ (this.valueIsTyped && !this._protocolIsTrimmed)
+ ) {
+ return selectedVal;
+ }
+
+ // The selection doesn't span the full domain if it doesn't contain a slash and is
+ // followed by some character other than a slash.
+ if (!selectedVal.includes("/")) {
+ let remainder = this.value.replace(selectedVal, "");
+ if (remainder != "" && remainder[0] != "/") {
+ return selectedVal;
+ }
+ }
+
+ let uri;
+ if (this.getAttribute("pageproxystate") == "valid") {
+ uri = this.#isOpenedPageInBlankTargetLoading
+ ? this.window.gBrowser.selectedBrowser.browsingContext
+ .nonWebControlledBlankURI
+ : this.window.gBrowser.currentURI;
+ } else {
+ // The value could be:
+ // 1. a trimmed url, set by selecting a result
+ // 2. a search string set by selecting a result
+ // 3. a url that was confirmed but didn't finish loading yet
+ // If it's an url the untrimmedValue should resolve to a valid URI,
+ // otherwise it's a search string that should be copied as-is.
+
+ // If the copied text is that autofilled value, return the url including
+ // the protocol from its suggestion.
+ let result = this._resultForCurrentValue;
+
+ if (result?.autofill?.value == selectedVal) {
+ return result.payload.url;
+ }
+
+ uri = URL.parse(this._untrimmedValue)?.URI;
+ if (!uri) {
+ return selectedVal;
+ }
+ }
+ uri = this.makeURIReadable(uri);
+ let displaySpec = uri.displaySpec;
+
+ // If the entire URL is selected, just use the actual loaded URI,
+ // unless we want a decoded URI, or it's a data: or javascript: URI,
+ // since those are hard to read when encoded.
+ if (
+ this.value == selectedVal &&
+ !uri.schemeIs("javascript") &&
+ !uri.schemeIs("data") &&
+ !lazy.UrlbarPrefs.get("decodeURLsOnCopy")
+ ) {
+ return displaySpec;
+ }
+
+ // Just the beginning of the URL is selected, or we want a decoded
+ // url. First check for a trimmed value.
+
+ if (
+ !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
+ // Note _trimValue may also trim a trailing slash, thus we can't just do
+ // a straight string compare to tell if the protocol was trimmed.
+ !displaySpec.startsWith(this._trimValue(displaySpec))
+ ) {
+ selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal;
+ }
+
+ // If selection starts from the beginning and part or all of the URL
+ // is selected, we check for decoded characters and encode them.
+ // Unless decodeURLsOnCopy is set. Do not encode data: URIs.
+ if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) {
+ try {
+ if (URL.canParse(selectedVal)) {
+ // Use encodeURI instead of URL.href because we don't want
+ // trailing slash.
+ selectedVal = encodeURI(selectedVal);
+ }
+ } catch (ex) {
+ // URL is invalid. Return original selected value.
+ }
+ }
+
+ return selectedVal;
+ }
+
+ _toggleActionOverride(event) {
+ if (
+ event.keyCode == KeyEvent.DOM_VK_SHIFT ||
+ event.keyCode == KeyEvent.DOM_VK_ALT ||
+ event.keyCode ==
+ (AppConstants.platform == "macosx"
+ ? KeyEvent.DOM_VK_META
+ : KeyEvent.DOM_VK_CONTROL)
+ ) {
+ if (event.type == "keydown") {
+ this._actionOverrideKeyCount++;
+ this.toggleAttribute("action-override", true);
+ this.view.panel.setAttribute("action-override", true);
+ } else if (
+ this._actionOverrideKeyCount &&
+ --this._actionOverrideKeyCount == 0
+ ) {
+ this._clearActionOverride();
+ }
+ }
+ }
+
+ _clearActionOverride() {
+ this._actionOverrideKeyCount = 0;
+ this.removeAttribute("action-override");
+ this.view.panel.removeAttribute("action-override");
+ }
+
+ /**
+ * Records in telemetry that a search is being loaded,
+ * updates an incremental total number of searches in a pref,
+ * and informs ASRouter that a search has occurred via a trigger send
+ *
+ * @param {nsISearchEngine} engine
+ * The engine to generate the query for.
+ * @param {Event} event
+ * The event that triggered this query.
+ * @param {object} [searchActionDetails]
+ * The details associated with this search query.
+ * @param {boolean} [searchActionDetails.isSuggestion]
+ * True if this query was initiated from a suggestion from the search engine.
+ * @param {boolean} [searchActionDetails.alias]
+ * True if this query was initiated via a search alias.
+ * @param {boolean} [searchActionDetails.isFormHistory]
+ * True if this query was initiated from a form history result.
+ * @param {string} [searchActionDetails.url]
+ * The url this query was triggered with.
+ * @param {MozBrowser} [browser]
+ * The browser where the search is being opened.
+ * Defaults to the window's selected browser.
+ */
+ _recordSearch(
+ engine,
+ event,
+ searchActionDetails = {},
+ browser = this.window.gBrowser.selectedBrowser
+ ) {
+ const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
+ const searchSource = this.getSearchSource(event);
+
+ // Record when the user uses the search bar to be
+ // used for message targeting. This is arbitrarily capped
+ // at 100, only to prevent the number from growing ifinitely.
+ const totalSearches = Services.prefs.getIntPref(
+ "browser.search.totalSearches"
+ );
+ const totalSearchesCap = 100;
+ if (totalSearches < totalSearchesCap) {
+ Services.prefs.setIntPref(
+ "browser.search.totalSearches",
+ totalSearches + 1
+ );
+ }
+
+ // Sending a trigger to ASRouter when a search happens
+ lazy.ASRouter.sendTriggerMessage({
+ browser,
+ id: "onSearch",
+ context: {
+ isSuggestion: searchActionDetails.isSuggestion || false,
+ searchSource,
+ isOneOff,
+ },
+ });
+
+ lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, {
+ ...searchActionDetails,
+ isOneOff,
+ newtabSessionId: this._handoffSession,
+ });
+ }
+
+ /**
+ * Shortens the given value, usually by removing http:// and trailing slashes.
+ *
+ * @param {string} val
+ * The string to be trimmed if it appears to be URI
+ * @returns {string}
+ * The trimmed string
+ */
+ _trimValue(val) {
+ if (!this.#isAddressbar) {
+ return val;
+ }
+ let trimmedValue = lazy.UrlbarPrefs.get("trimURLs")
+ ? lazy.BrowserUIUtils.trimURL(val)
+ : val;
+ // Only trim value if the directionality doesn't change to RTL and we're not
+ // showing a strikeout https protocol.
+ return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) ||
+ this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val)
+ ? val
+ : trimmedValue;
+ }
+
+ /**
+ * Returns whether the passed-in event may represents a canonization request.
+ *
+ * @param {Event} event
+ * An Event to examine.
+ * @returns {boolean}
+ * Whether the event is a KeyboardEvent that triggers canonization.
+ */
+ #isCanonizeKeyboardEvent(event) {
+ return (
+ KeyboardEvent.isInstance(event) &&
+ event.keyCode == KeyEvent.DOM_VK_RETURN &&
+ (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) &&
+ !event._disableCanonization &&
+ lazy.UrlbarPrefs.get("ctrlCanonizesURLs")
+ );
+ }
+
+ /**
+ * If appropriate, this prefixes a search string with 'www.' and suffixes it
+ * with browser.fixup.alternate.suffix prior to navigating.
+ *
+ * @param {Event} event
+ * The event that triggered this query.
+ * @param {string} value
+ * The search string that should be canonized.
+ * @returns {string}
+ * Returns the canonized URL if available and null otherwise.
+ */
+ _maybeCanonizeURL(event, value) {
+ // Only add the suffix when the URL bar value isn't already "URL-like",
+ // and only if we get a keyboard event, to match user expectations.
+ if (
+ this.sapName == "searchbar" ||
+ !this.#isCanonizeKeyboardEvent(event) ||
+ !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
+ ) {
+ return null;
+ }
+
+ let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
+ if (!suffix.endsWith("/")) {
+ suffix += "/";
+ }
+
+ // trim leading/trailing spaces (bug 233205)
+ value = value.trim();
+
+ // Tack www. and suffix on. If user has appended directories, insert
+ // suffix before them (bug 279035). Be careful not to get two slashes.
+ let firstSlash = value.indexOf("/");
+ if (firstSlash >= 0) {
+ value =
+ value.substring(0, firstSlash) +
+ suffix +
+ value.substring(firstSlash + 1);
+ } else {
+ value = value + suffix;
+ }
+
+ try {
+ const info = Services.uriFixup.getFixupURIInfo(
+ value,
+ Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI
+ );
+ value = info.fixedURI.spec;
+ } catch (ex) {
+ console.error(`An error occured while trying to fixup "${value}"`, ex);
+ }
+
+ this.value = value;
+ return value;
+ }
+
+ /**
+ * Autofills a value into the input. The value will be autofilled regardless
+ * of the input's current value.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {string} options.value
+ * The value to autofill.
+ * @param {number} options.selectionStart
+ * The new selectionStart.
+ * @param {number} options.selectionEnd
+ * The new selectionEnd.
+ * @param {"origin" | "url" | "adaptive"} options.type
+ * The autofill type, one of: "origin", "url", "adaptive"
+ * @param {string} options.adaptiveHistoryInput
+ * If the autofill type is "adaptive", this is the matching `input` value
+ * from adaptive history.
+ * @param {string} [options.untrimmedValue]
+ * Untrimmed value including a protocol.
+ */
+ _autofillValue({
+ value,
+ selectionStart,
+ selectionEnd,
+ type,
+ adaptiveHistoryInput,
+ untrimmedValue,
+ }) {
+ // The autofilled value may be a URL that includes a scheme at the
+ // beginning. Do not allow it to be trimmed.
+ this._setValue(value, { untrimmedValue });
+ this.inputField.setSelectionRange(selectionStart, selectionEnd);
+ this._autofillPlaceholder = {
+ value,
+ type,
+ adaptiveHistoryInput,
+ selectionStart,
+ selectionEnd,
+ untrimmedValue,
+ };
+ }
+
+ /**
+ * Called when a menu item from results menu is picked.
+ *
+ * @param {UrlbarResult} result The result that was picked.
+ * @param {Event} event The event that picked the result.
+ * @param {HTMLElement} element the picked view element, if available.
+ * @param {object} browser The browser to use for the load.
+ */
+ #pickMenuResult(result, event, element, browser) {
+ this.controller.engagementEvent.record(event, {
+ result,
+ element,
+ searchString: this._lastSearchString,
+ selType: element.dataset.command,
+ });
+
+ if (element.dataset.command == "manage") {
+ this.window.openPreferences("search-locationBar");
+ return;
+ }
+
+ let url;
+ if (element.dataset.command == "help") {
+ url = result.payload.helpUrl;
+ }
+ url ||= element.dataset.url;
+
+ if (!url) {
+ return;
+ }
+
+ let where = this._whereToOpen(event);
+ if (element.dataset.command == "help" && where == "current") {
+ // Open help links in a new tab.
+ where = "tab";
+ }
+
+ this.view.close({ elementPicked: true });
+
+ this._loadURL(
+ url,
+ event,
+ where,
+ {
+ allowInheritPrincipal: false,
+ private: this.isPrivate,
+ },
+ {
+ source: result.source,
+ type: result.type,
+ },
+ browser
+ );
+ }
+
+ /**
+ * Loads the url in the appropriate place.
+ *
+ * @param {string} url
+ * The URL to open.
+ * @param {string} openUILinkWhere
+ * Where we expect the result to be opened.
+ * @param {object} params
+ * The parameters related to how and where the result will be opened.
+ * Further supported paramters are listed in _loadURL.
+ * @param {object} [params.triggeringPrincipal]
+ * The principal that the action was triggered from.
+ * @param {object} [resultDetails]
+ * Details of the selected result, if any.
+ * Further supported details are listed in _loadURL.
+ * @param {string} [resultDetails.searchTerm]
+ * Search term of the result source, if any.
+ * @param {object} browser the browser to use for the load.
+ */
+ #prepareAddressbarLoad(
+ url,
+ openUILinkWhere,
+ params,
+ resultDetails = null,
+ browser
+ ) {
+ if (!this.#isAddressbar) {
+ throw new Error(
+ "Can't prepare addressbar load when this isn't an addressbar input"
+ );
+ }
+
+ // No point in setting these because we'll handleRevert() a few rows below.
+ if (openUILinkWhere == "current") {
+ // Make sure URL is formatted properly (don't show punycode).
+ let formattedURL = url;
+ try {
+ formattedURL = losslessDecodeURI(new URL(url).URI);
+ } catch {}
+
+ this.value =
+ lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() &&
+ resultDetails?.searchTerm
+ ? resultDetails.searchTerm
+ : formattedURL;
+ browser.userTypedValue = this.value;
+ }
+
+ // No point in setting this if we are loading in a new window.
+ if (
+ openUILinkWhere != "window" &&
+ this.window.gInitialPages.includes(url)
+ ) {
+ browser.initialPageLoadedFromUserAction = url;
+ }
+
+ try {
+ lazy.UrlbarUtils.addToUrlbarHistory(url, this.window);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ console.error(ex);
+ }
+
+ // TODO: When bug 1498553 is resolved, we should be able to
+ // remove the !triggeringPrincipal condition here.
+ if (
+ !params.triggeringPrincipal ||
+ params.triggeringPrincipal.isSystemPrincipal
+ ) {
+ // Reset DOS mitigations for the basic auth prompt.
+ delete browser.authPromptAbuseCounter;
+
+ // Reset temporary permissions on the current tab if the user reloads
+ // the tab via the urlbar.
+ if (
+ openUILinkWhere == "current" &&
+ browser.currentURI &&
+ url === browser.currentURI.spec
+ ) {
+ this.window.SitePermissions.clearTemporaryBlockPermissions(browser);
+ }
+ }
+
+ // Specifies that the URL load was initiated by the URL bar.
+ params.initiatedByURLBar = true;
+ }
+
+ /**
+ * Loads the url in the appropriate place.
+ *
+ * @param {string} url
+ * The URL to open.
+ * @param {Event} event
+ * The event that triggered to load the url.
+ * @param {string} openUILinkWhere
+ * Where we expect the result to be opened.
+ * @param {object} params
+ * The parameters related to how and where the result will be opened.
+ * Further supported parameters are listed in utilityOverlay.js#openUILinkIn.
+ * @param {object} [params.triggeringPrincipal]
+ * The principal that the action was triggered from.
+ * @param {nsIInputStream} [params.postData]
+ * The POST data associated with a search submission.
+ * @param {boolean} [params.allowInheritPrincipal]
+ * Whether the principal can be inherited.
+ * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput]
+ * Whether the search/URL term was without an explicit scheme.
+ * @param {object} [resultDetails]
+ * Details of the selected result, if any.
+ * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type]
+ * Details of the result type, if any.
+ * @param {string} [resultDetails.searchTerm]
+ * Search term of the result source, if any.
+ * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source]
+ * Details of the result source, if any.
+ * @param {object} browser [optional] the browser to use for the load.
+ */
+ _loadURL(
+ url,
+ event,
+ openUILinkWhere,
+ params,
+ resultDetails = null,
+ browser = this.window.gBrowser.selectedBrowser
+ ) {
+ if (this.#isAddressbar) {
+ this.#prepareAddressbarLoad(
+ url,
+ openUILinkWhere,
+ params,
+ resultDetails,
+ browser
+ );
+ }
+
+ params.allowThirdPartyFixup = true;
+
+ if (openUILinkWhere == "current") {
+ params.targetBrowser = browser;
+ params.indicateErrorPageLoad = true;
+ params.allowPinnedTabHostChange = true;
+ params.allowPopups = url.startsWith("javascript:");
+ } else {
+ params.initiatingDoc = this.window.document;
+ }
+
+ if (
+ this._keyDownEnterDeferred &&
+ event?.keyCode === KeyEvent.DOM_VK_RETURN &&
+ openUILinkWhere === "current"
+ ) {
+ // In this case, we move the focus to the browser that loads the content
+ // upon key up the enter key.
+ // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid
+ // focusing on the browser in the function. And also, set loadedContent
+ // flag that whether the content is loaded in the current tab by this enter
+ // key. _keyDownEnterDeferred promise is processed at key up the enter,
+ // focus on the browser passed by _keyDownEnterDeferred.resolve().
+ params.avoidBrowserFocus = true;
+ this._keyDownEnterDeferred.loadedContent = true;
+ this._keyDownEnterDeferred.resolve(browser);
+ }
+
+ // Ensure the window gets the `private` feature if the current window
+ // is private, unless the caller explicitly requested not to.
+ if (this.isPrivate && !("private" in params)) {
+ params.private = true;
+ }
+
+ // Focus the content area before triggering loads, since if the load
+ // occurs in a new tab, we want focus to be restored to the content
+ // area when the current tab is re-selected.
+ if (!params.avoidBrowserFocus) {
+ browser.focus();
+ // Make sure the domain name stays visible for spoof protection and usability.
+ this.inputField.setSelectionRange(0, 0);
+ }
+
+ if (openUILinkWhere != "current") {
+ this.handleRevert();
+ }
+
+ // Notify about the start of navigation.
+ this.#notifyStartNavigation(resultDetails);
+
+ try {
+ this.window.openTrustedLinkIn(url, openUILinkWhere, params);
+ } catch (ex) {
+ // This load can throw an exception in certain cases, which means
+ // we'll want to replace the URL with the loaded URL:
+ if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+ this.handleRevert();
+ }
+ }
+
+ // If we show the focus border after closing the view, it would appear to
+ // flash since this._on_blur would remove it immediately after.
+ this.view.close({ showFocusBorder: false });
+ }
+
+ /**
+ * Determines where a URL/page should be opened.
+ *
+ * @param {Event} event the event triggering the opening.
+ * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
+ */
+ _whereToOpen(event) {
+ let isKeyboardEvent = KeyboardEvent.isInstance(event);
+ let reuseEmpty = isKeyboardEvent;
+ let where = undefined;
+ if (
+ isKeyboardEvent &&
+ (event.altKey || event.getModifierState("AltGraph"))
+ ) {
+ // We support using 'alt' to open in a tab, because ctrl/shift
+ // might be used for canonizing URLs:
+ where = event.shiftKey ? "tabshifted" : "tab";
+ } else if (this.#isCanonizeKeyboardEvent(event)) {
+ // If we're allowing canonization, and this is a canonization key event,
+ // open in current tab to avoid handling as new tab modifier.
+ where = "current";
+ } else {
+ where = lazy.BrowserUtils.whereToOpenLink(event, false, false);
+ }
+ if (lazy.UrlbarPrefs.get("openintab")) {
+ if (where == "current") {
+ where = "tab";
+ } else if (where == "tab") {
+ where = "current";
+ }
+ reuseEmpty = true;
+ }
+ if (
+ where == "tab" &&
+ reuseEmpty &&
+ this.window.gBrowser.selectedTab.isEmpty
+ ) {
+ where = "current";
+ }
+ return where;
+ }
+
+ _initCopyCutController() {
+ if (this._copyCutController) {
+ return;
+ }
+ this._copyCutController = new CopyCutController(this);
+ this.inputField.controllers.insertControllerAt(0, this._copyCutController);
+ }
+
+ /**
+ * Searches the context menu for the location of a specific command.
+ *
+ * @param {string} menuItemCommand
+ * The command to search for.
+ * @returns {HTMLElement}
+ * Html element that matches the command or
+ * the last element if we could not find the command.
+ */
+ #findMenuItemLocation(menuItemCommand) {
+ let inputBox = this.querySelector("moz-input-box");
+ let contextMenu = inputBox.menupopup;
+ let insertLocation = contextMenu.firstElementChild;
+ // find the location of the command
+ while (
+ insertLocation.nextElementSibling &&
+ insertLocation.getAttribute("cmd") != menuItemCommand
+ ) {
+ insertLocation = insertLocation.nextElementSibling;
+ }
+
+ return insertLocation;
+ }
+
+ /**
+ * Strips known tracking query parameters/ link decorators.
+ *
+ * @returns {nsIURI}
+ * The stripped URI or original URI, if nothing can be
+ * stripped
+ */
+ #stripURI() {
+ let copyString = this._getSelectedValueForClipboard();
+ if (!copyString) {
+ return null;
+ }
+ let strippedURI = null;
+
+ // Error check occurs during isClipboardURIValid
+ let uri = Services.io.newURI(copyString);
+ try {
+ strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
+ } catch (e) {
+ console.warn(`stripForCopyOrShare: ${e.message}`);
+ return uri;
+ }
+
+ if (strippedURI) {
+ return this.makeURIReadable(strippedURI);
+ }
+ return uri;
+ }
+
+ /**
+ * Checks if the clipboard contains a valid URI
+ *
+ * @returns {true|false}
+ */
+ #isClipboardURIValid() {
+ let copyString = this._getSelectedValueForClipboard();
+ if (!copyString) {
+ return false;
+ }
+
+ return URL.canParse(copyString);
+ }
+
+ /**
+ * Checks if there is a query parameter that can be stripped
+ *
+ * @returns {true|false}
+ */
+ #canStrip() {
+ let copyString = this._getSelectedValueForClipboard();
+ if (!copyString) {
+ return false;
+ }
+ // throws if the selected string is not a valid URI
+ try {
+ let uri = Services.io.newURI(copyString);
+ return lazy.QueryStringStripper.canStripForShare(uri);
+ } catch (e) {
+ console.warn("canStrip failed!", e);
+ return false;
+ }
+ }
+
+ /**
+ * Restores the untrimmed value in the urlbar.
+ *
+ * @param {object} [options]
+ * Options for untrimming.
+ * @param {boolean} [options.moveCursorToStart]
+ * Whether the cursor should be moved at position 0 after untrimming.
+ * @param {boolean} [options.ignoreSelection]
+ * Whether this should untrim, regardless of the current selection state.
+ */
+ #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) {
+ // Check if we can untrim the current value.
+ if (
+ !lazy.UrlbarPrefs.getScotchBonnetPref(
+ "untrimOnUserInteraction.featureGate"
+ ) ||
+ !this._protocolIsTrimmed ||
+ !this.focused ||
+ (!ignoreSelection && this.#allTextSelected)
+ ) {
+ return;
+ }
+
+ let selectionStart = this.selectionStart;
+ let selectionEnd = this.selectionEnd;
+
+ // Correct the selection taking the trimmed protocol into account.
+ let offset = lazy.BrowserUIUtils.trimURLProtocol.length;
+
+ // In case of autofill, we may have to adjust its boundaries.
+ if (this._autofillPlaceholder) {
+ this._autofillPlaceholder.selectionStart += offset;
+ this._autofillPlaceholder.selectionEnd += offset;
+ }
+
+ if (moveCursorToStart) {
+ this._setValue(this._untrimmedValue, {
+ valueIsTyped: this.valueIsTyped,
+ });
+ this.setSelectionRange(0, 0);
+ return;
+ }
+
+ if (selectionStart == selectionEnd) {
+ // When cursor is at the end of the string, untrimming may
+ // reintroduced a trailing slash and we want to move past it.
+ if (selectionEnd == this.value.length) {
+ offset += 1;
+ }
+ selectionStart = selectionEnd += offset;
+ } else {
+ // There's a selection, so we must calculate both the initial
+ // protocol and the eventual trailing slash.
+ if (selectionStart != 0) {
+ selectionStart += offset;
+ } else {
+ // When selection starts at the beginning, the adjusted selection will
+ // include the protocol only if the selected text includes the host.
+ // The port is left out, as one may want to exclude it from the copy.
+ let prePathMinusPort;
+ try {
+ let uri = Services.io.newURI(this._untrimmedValue);
+ prePathMinusPort = [uri.userPass, uri.displayHost]
+ .filter(Boolean)
+ .join("@");
+ } catch (ex) {
+ lazy.logger.error("Should only try to untrim valid URLs");
+ }
+ if (!this.#selectedText.startsWith(prePathMinusPort)) {
+ selectionStart += offset;
+ }
+ }
+ if (selectionEnd == this.value.length) {
+ offset += 1;
+ }
+ selectionEnd += offset;
+ }
+
+ this._setValue(this._untrimmedValue, {
+ valueIsTyped: this.valueIsTyped,
+ });
+
+ this.setSelectionRange(selectionStart, selectionEnd);
+ }
+
+ // The strip-on-share feature will strip known tracking/decorational
+ // query params from the URI and copy the stripped version to the clipboard.
+ _initStripOnShare() {
+ let contextMenu = this.querySelector("moz-input-box").menupopup;
+ let insertLocation = this.#findMenuItemLocation("cmd_copy");
+ // set up the menu item
+ let stripOnShare = this.document.createXULElement("menuitem");
+ this.document.l10n.setAttributes(
+ stripOnShare,
+ "text-action-copy-clean-link"
+ );
+ stripOnShare.setAttribute("anonid", "strip-on-share");
+ stripOnShare.id = "strip-on-share";
+
+ insertLocation.insertAdjacentElement("afterend", stripOnShare);
+
+ // Register listener that returns the stripped url or falls back
+ // to the original url if nothing can be stripped.
+ stripOnShare.addEventListener("command", () => {
+ let strippedURI = this.#stripURI();
+ lazy.ClipboardHelper.copyString(strippedURI.displaySpec);
+ });
+
+ // Register a listener that hides the menu item if there is nothing to copy.
+ contextMenu.addEventListener("popupshowing", () => {
+ // feature is not enabled
+ if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) {
+ stripOnShare.setAttribute("hidden", true);
+ return;
+ }
+ let controller =
+ this.document.commandDispatcher.getControllerForCommand("cmd_copy");
+ if (
+ !controller.isCommandEnabled("cmd_copy") ||
+ !this.#isClipboardURIValid()
+ ) {
+ stripOnShare.setAttribute("hidden", true);
+ return;
+ }
+ stripOnShare.removeAttribute("hidden");
+ if (!this.#canStrip()) {
+ stripOnShare.setAttribute("disabled", true);
+ return;
+ }
+ stripOnShare.removeAttribute("disabled");
+ });
+ }
+
+ _initPasteAndGo() {
+ let inputBox = this.querySelector("moz-input-box");
+ let contextMenu = inputBox.menupopup;
+ let insertLocation = this.#findMenuItemLocation("cmd_paste");
+ if (!insertLocation) {
+ return;
+ }
+
+ let pasteAndGo = this.document.createXULElement("menuitem");
+ pasteAndGo.id = "paste-and-go";
+ let label = Services.strings
+ .createBundle("chrome://browser/locale/browser.properties")
+ .GetStringFromName("pasteAndGo.label");
+ pasteAndGo.setAttribute("label", label);
+ pasteAndGo.setAttribute("anonid", "paste-and-go");
+ pasteAndGo.addEventListener("command", () => {
+ this._suppressStartQuery = true;
+
+ this.select();
+ this.window.goDoCommand("cmd_paste");
+ this.setResultForCurrentValue(null);
+ this.handleCommand();
+ this.controller.clearLastQueryContextCache();
+
+ this._suppressStartQuery = false;
+ });
+
+ contextMenu.addEventListener("popupshowing", () => {
+ // Close the results pane when the input field contextual menu is open,
+ // because paste and go doesn't want a result selection.
+ this.view.close();
+
+ let controller =
+ this.document.commandDispatcher.getControllerForCommand("cmd_paste");
+ let enabled = controller.isCommandEnabled("cmd_paste");
+ if (enabled) {
+ pasteAndGo.removeAttribute("disabled");
+ } else {
+ pasteAndGo.setAttribute("disabled", "true");
+ }
+ });
+
+ insertLocation.insertAdjacentElement("afterend", pasteAndGo);
+ }
+
+ /**
+ * This notifies observers that the user has entered or selected something in
+ * the URL bar which will cause navigation.
+ *
+ * We use the observer service, so that we don't need to load extra facilities
+ * if they aren't being used, e.g. WebNavigation.
+ *
+ * @param {UrlbarResult} result
+ * Details of the result that was selected, if any.
+ */
+ #notifyStartNavigation(result) {
+ if (this.#isAddressbar) {
+ Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
+ }
+ }
+
+ /**
+ * Returns a search mode object if a result should enter search mode when
+ * selected.
+ *
+ * @param {UrlbarResult} result
+ * The result to check.
+ * @param {string} [entry]
+ * If provided, this will be recorded as the entry point into search mode.
+ * See setSearchMode() documentation for details.
+ * @returns {object} A search mode object. Null if search mode should not be
+ * entered. See setSearchMode documentation for details.
+ */
+ _searchModeForResult(result, entry = null) {
+ // Search mode is determined by the result's keyword or engine.
+ if (!result.payload.keyword && !result.payload.engine) {
+ return null;
+ }
+
+ let searchMode = this.searchModeForToken(result.payload.keyword);
+ // If result.originalEngine is set, then the user is Alt+Tabbing
+ // through the one-offs, so the keyword doesn't match the engine.
+ if (
+ !searchMode &&
+ result.payload.engine &&
+ (!result.payload.originalEngine ||
+ result.payload.engine == result.payload.originalEngine)
+ ) {
+ searchMode = { engineName: result.payload.engine };
+ }
+
+ if (searchMode) {
+ if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) {
+ searchMode.restrictType = "keyword";
+ } else if (
+ lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword)
+ ) {
+ searchMode.restrictType = "symbol";
+ }
+ if (entry) {
+ searchMode.entry = entry;
+ } else {
+ switch (result.providerName) {
+ case "UrlbarProviderTopSites":
+ searchMode.entry = "topsites_urlbar";
+ break;
+ case "UrlbarProviderTabToSearch":
+ if (result.payload.dynamicType) {
+ searchMode.entry = "tabtosearch_onboard";
+ } else {
+ searchMode.entry = "tabtosearch";
+ }
+ break;
+ default:
+ searchMode.entry = "keywordoffer";
+ break;
+ }
+ }
+ }
+
+ return searchMode;
+ }
+
+ /**
+ * Updates the UI so that search mode is either entered or exited.
+ *
+ * @param {object} searchMode
+ * See setSearchMode documentation. If null, then search mode is exited.
+ */
+ _updateSearchModeUI(searchMode) {
+ let { engineName, source, isGeneralPurposeEngine } = searchMode || {};
+
+ // As an optimization, bail if the given search mode is null but search mode
+ // is already inactive. Otherwise, browser_preferences_usage.js fails due to
+ // accessing the browser.urlbar.placeholderName pref (via the call to
+ // initPlaceHolder below) too many times. That test does not enter search mode,
+ // but it triggers many calls to this method with a null search mode, via setURI.
+ if (!engineName && !source && !this.hasAttribute("searchmode")) {
+ return;
+ }
+
+ if (this._searchModeIndicatorTitle) {
+ this._searchModeIndicatorTitle.textContent = "";
+ this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
+ }
+
+ if (!engineName && !source) {
+ this.removeAttribute("searchmode");
+ this.initPlaceHolder(true);
+ return;
+ }
+
+ if (this.#isAddressbar) {
+ if (engineName) {
+ // Set text content for the search mode indicator.
+ this._searchModeIndicatorTitle.textContent = engineName;
+ this.document.l10n.setAttributes(
+ this.inputField,
+ isGeneralPurposeEngine
+ ? "urlbar-placeholder-search-mode-web-2"
+ : "urlbar-placeholder-search-mode-other-engine",
+ { name: engineName }
+ );
+ } else if (source) {
+ const messageIDs = {
+ actions: "urlbar-placeholder-search-mode-other-actions",
+ bookmarks: "urlbar-placeholder-search-mode-other-bookmarks",
+ engine: "urlbar-placeholder-search-mode-other-engine",
+ history: "urlbar-placeholder-search-mode-other-history",
+ tabs: "urlbar-placeholder-search-mode-other-tabs",
+ };
+ let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
+ let l10nID = `urlbar-search-mode-${sourceName}`;
+ this.document.l10n.setAttributes(
+ this._searchModeIndicatorTitle,
+ l10nID
+ );
+ this.document.l10n.setAttributes(
+ this.inputField,
+ messageIDs[sourceName]
+ );
+ }
+ }
+
+ this.toggleAttribute("searchmode", true);
+ // Clear autofill.
+ if (this._autofillPlaceholder && this.userTypedValue) {
+ this.value = this.userTypedValue;
+ }
+ // Search mode should only be active when pageproxystate is invalid.
+ if (this.getAttribute("pageproxystate") == "valid") {
+ this.value = "";
+ this.setPageProxyState("invalid", true);
+ }
+
+ this.searchModeSwitcher?.onSearchModeChanged();
+ }
+
+ /**
+ * Handles persisted search terms logic for the current browser. This manages
+ * state and updates the UI accordingly.
+ *
+ * @param {object} options
+ * @param {object} options.state
+ * The state object for the currently viewed browser.
+ * @param {boolean} options.hideSearchTerms
+ * True if we must hide the search terms and instead show the page URL.
+ * @param {boolean} options.dueToTabSwitch
+ * True if the browser was revealed again due to a tab switch.
+ * @param {boolean} options.isSameDocument
+ * True if the page load was same document.
+ * @param {nsIURI} [options.uri]
+ * The latest URI of the page.
+ * @returns {boolean}
+ * Whether search terms should persist.
+ */
+ #handlePersistedSearchTerms({
+ state,
+ hideSearchTerms,
+ dueToTabSwitch,
+ isSameDocument,
+ uri,
+ }) {
+ if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) {
+ if (state.persist) {
+ this.removeAttribute("persistsearchterms");
+ delete state.persist;
+ }
+ return false;
+ }
+
+ // The first time the browser URI has been loaded to the input. If
+ // persist is not defined, it is likely due to the tab being created in
+ // the background or an existing tab moved to a new window and we have to
+ // do the work for the first time.
+ let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist;
+
+ let cachedUriDidChange =
+ state.persist?.originalURI &&
+ (!this.window.gBrowser.selectedBrowser.originalURI ||
+ !state.persist.originalURI.equals(
+ this.window.gBrowser.selectedBrowser.originalURI
+ ));
+
+ // Capture the shouldPersist property if it exists before
+ // setPersistenceState potentially modifies it.
+ let wasPersisting = state.persist?.shouldPersist ?? false;
+
+ if (firstView || cachedUriDidChange) {
+ lazy.UrlbarSearchTermsPersistence.setPersistenceState(
+ state,
+ this.window.gBrowser.selectedBrowser.originalURI
+ );
+ }
+ let shouldPersist =
+ !hideSearchTerms &&
+ lazy.UrlbarSearchTermsPersistence.shouldPersist(state, {
+ dueToTabSwitch,
+ isSameDocument,
+ uri: uri ?? this.window.gBrowser.currentURI,
+ userTypedValue: this.userTypedValue,
+ firstView,
+ });
+ // When persisting, userTypedValue should have a value consistent with the
+ // search terms to mimic a user typing the search terms.
+ // When turning off persist, check if the userTypedValue needs to be
+ // removed in order for the URL to return to the address bar. Single page
+ // application SERPs will load secondary search pages (e.g. Maps, Images)
+ // with the same document, which won't unset userTypedValue.
+ if (shouldPersist) {
+ this.userTypedValue = state.persist.searchTerms;
+ } else if (wasPersisting && !shouldPersist) {
+ this.userTypedValue = null;
+ }
+
+ state.persist.shouldPersist = shouldPersist;
+ this.toggleAttribute("persistsearchterms", state.persist.shouldPersist);
+
+ if (state.persist.shouldPersist && !isSameDocument) {
+ Glean.urlbarPersistedsearchterms.viewCount.add(1);
+ }
+
+ return shouldPersist;
+ }
+
+ /**
+ * Initializes the urlbar placeholder to the pre-saved engine name. We do this
+ * via a preference, to avoid needing to synchronously init the search service.
+ *
+ * This should be called around the time of DOMContentLoaded, so that it is
+ * initialized quickly before the user sees anything.
+ *
+ * Note: If the preference doesn't exist, we don't do anything as the default
+ * placeholder is a string which doesn't have the engine name; however, this
+ * can be overridden using the `force` parameter.
+ *
+ * @param {boolean} force If true and the preference doesn't exist, the
+ * placeholder will be set to the default version
+ * without an engine name ("Search or enter address").
+ */
+ initPlaceHolder(force = false) {
+ if (!this.#isAddressbar) {
+ return;
+ }
+
+ let prefName =
+ "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : "");
+ let engineName = Services.prefs.getStringPref(prefName, "");
+ if (engineName || force) {
+ // We can do this directly, since we know we're at DOMContentLoaded.
+ this._setPlaceholder(engineName || null);
+ }
+ }
+
+ /**
+ * Asynchronously changes the urlbar placeholder to the name of the default
+ * engine according to the search service when it is initialized.
+ *
+ * This should be called around the time of MozAfterPaint. Since the
+ * placeholder was already initialized to the pre-saved engine name by
+ * initPlaceHolder when this is called, the update is delayed to avoid
+ * confusing the user.
+ */
+ async delayedStartupInit() {
+ // Only delay if requested, and we're not displaying text in the URL bar
+ // currently.
+ if (!this.value) {
+ // Delays changing the URL Bar placeholder and Unified Search Button icon
+ // until the user is not going to be seeing it, e.g. when there is a value
+ // entered in the bar, or if there is a tab switch to a tab which has a url
+ // loaded. We delay the update until the user is out of search mode since
+ // an alternative placeholder is used in search mode.
+ let updateListener = () => {
+ if (this.value && !this.searchMode) {
+ // By the time the user has switched, they may have changed the engine
+ // again, so we need to call this function again but with the
+ // new engine name.
+ // No need to await for this to finish, we're in a listener here anyway.
+ this.searchModeSwitcher.updateSearchIcon();
+ this._updatePlaceholderFromDefaultEngine();
+ this.inputField.removeEventListener("input", updateListener);
+ this.window.gBrowser.tabContainer.removeEventListener(
+ "TabSelect",
+ updateListener
+ );
+ }
+ };
+
+ this.inputField.addEventListener("input", updateListener);
+ this.window.gBrowser.tabContainer.addEventListener(
+ "TabSelect",
+ updateListener
+ );
+ } else {
+ await this._updatePlaceholderFromDefaultEngine();
+ }
+
+ // If we haven't finished initializing, ensure the placeholder
+ // preference is set for the next startup.
+ if (this.#isAddressbar) {
+ lazy.SearchUIUtils.updatePlaceholderNamePreference(
+ await this._getDefaultSearchEngine(),
+ this.isPrivate
+ );
+ }
+ }
+
+ /**
+ * Set Unified Search Button availability.
+ *
+ * @param {boolean} available If true Unified Search Button will be available.
+ */
+ setUnifiedSearchButtonAvailability(available) {
+ this.toggleAttribute("unifiedsearchbutton-available", available);
+ this.getBrowserState(
+ this.window.gBrowser.selectedBrowser
+ ).isUnifiedSearchButtonAvailable = available;
+ }
+
+ /**
+ * Returns a Promise that resolves with default search engine.
+ *
+ * @returns {Promise<nsISearchEngine>}
+ */
+ _getDefaultSearchEngine() {
+ return this.isPrivate
+ ? Services.search.getDefaultPrivate()
+ : Services.search.getDefault();
+ }
+
+ /**
+ * This is a wrapper around '_updatePlaceholder' that uses the appropriate
+ * default engine to get the engine name.
+ */
+ async _updatePlaceholderFromDefaultEngine() {
+ const defaultEngine = await this._getDefaultSearchEngine();
+ this._updatePlaceholder(defaultEngine.name);
+ }
+
+ /**
+ * Updates the URLBar placeholder for the specified engine, delaying the
+ * update if required.
+ *
+ * Note: The engine name will only be displayed for application-provided
+ * engines, as we know they should have short names.
+ *
+ * @param {string} engineName The search engine name to use for the update.
+ */
+ _updatePlaceholder(engineName) {
+ if (!engineName) {
+ throw new Error("Expected an engineName to be specified");
+ }
+
+ if (this.searchMode || !this.#isAddressbar) {
+ return;
+ }
+
+ let engine = Services.search.getEngineByName(engineName);
+ if (engine.isConfigEngine) {
+ this._setPlaceholder(engineName);
+ } else {
+ // Display the default placeholder string.
+ this._setPlaceholder(null);
+ }
+ }
+
+ /**
+ * Sets the URLBar placeholder to either something based on the engine name,
+ * or the default placeholder.
+ *
+ * @param {?string} engineName
+ * The name of the engine or null to use the default placeholder.
+ */
+ _setPlaceholder(engineName) {
+ if (!this.#isAddressbar) {
+ this.document.l10n.setAttributes(this.inputField, "searchbar-input");
+ return;
+ }
+
+ let l10nId;
+ if (lazy.UrlbarPrefs.get("keyword.enabled")) {
+ l10nId = engineName
+ ? "urlbar-placeholder-with-name"
+ : "urlbar-placeholder";
+ } else {
+ l10nId = "urlbar-placeholder-keyword-disabled";
+ }
+
+ this.document.l10n.setAttributes(
+ this.inputField,
+ l10nId,
+ l10nId == "urlbar-placeholder-with-name"
+ ? { name: engineName }
+ : undefined
+ );
+ }
+
+ /**
+ * Determines if we should select all the text in the Urlbar based on the
+ * Urlbar state, and whether the selection is empty.
+ */
+ #maybeSelectAll() {
+ if (
+ !this._preventClickSelectsAll &&
+ this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
+ this.focused &&
+ this.inputField.selectionStart == this.inputField.selectionEnd
+ ) {
+ this.select();
+ }
+ }
+
+ // Event handlers below.
+
+ _on_command(event) {
+ // Something is executing a command, likely causing a focus change. This
+ // should not be recorded as an abandonment. If the user is selecting a
+ // result menu item or entering search mode from a one-off, then they are
+ // in the same engagement and we should not discard.
+ if (
+ !event.target.classList.contains("urlbarView-result-menuitem") &&
+ (!event.target.classList.contains("searchbar-engine-one-off-item") ||
+ this.searchMode?.entry != "oneoff")
+ ) {
+ this.controller.engagementEvent.discard();
+ }
+ }
+
+ _on_blur(event) {
+ lazy.logger.debug("Blur Event");
+ // We cannot count every blur events after a missed engagement as abandoment
+ // because the user may have clicked on some view element that executes
+ // a command causing a focus change. For example opening preferences from
+ // the oneoff settings button.
+ // For now we detect that case by discarding the event on command, but we
+ // may want to figure out a more robust way to detect abandonment.
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ searchSource: this.getSearchSource(event),
+ });
+
+ this.focusedViaMousedown = false;
+ this._handoffSession = undefined;
+ this._isHandoffSession = false;
+ this.removeAttribute("focused");
+
+ if (this._autofillPlaceholder && this.userTypedValue) {
+ // If we were autofilling, remove the autofilled portion, by restoring
+ // the value to the last typed one.
+ this.value = this.userTypedValue;
+ } else if (
+ this.value == this._untrimmedValue &&
+ !this.userTypedValue &&
+ !this.focused
+ ) {
+ // If the value was untrimmed by _on_focus and didn't change, trim it.
+ this.value = this._untrimmedValue;
+ } else {
+ // We're not updating the value, so just format it.
+ this.formatValue();
+ }
+
+ this._resetSearchState();
+
+ // In certain cases, like holding an override key and confirming an entry,
+ // we don't key a keyup event for the override key, thus we make this
+ // additional cleanup on blur.
+ this._clearActionOverride();
+
+ // The extension input sessions depends more on blur than on the fact we
+ // actually cancel a running query, so we do it here.
+ if (lazy.ExtensionSearchHandler.hasActiveInputSession()) {
+ lazy.ExtensionSearchHandler.handleInputCancelled();
+ }
+
+ // Respect the autohide preference for easier inspecting/debugging via
+ // the browser toolbox.
+ if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.view.close();
+ }
+
+ // We may have hidden popup notifications, show them again if necessary.
+ if (
+ this.getAttribute("pageproxystate") != "valid" &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+
+ // If user move the focus to another component while pressing Enter key,
+ // then keyup at that component, as we can't get the event, clear the promise.
+ if (this._keyDownEnterDeferred) {
+ this._keyDownEnterDeferred.resolve();
+ this._keyDownEnterDeferred = null;
+ }
+ this._isKeyDownWithCtrl = false;
+ this._isKeyDownWithMeta = false;
+ this._isKeyDownWithMetaAndLeft = false;
+
+ Services.obs.notifyObservers(null, "urlbar-blur");
+ }
+
+ _on_click(event) {
+ switch (event.target) {
+ case this.inputField:
+ case this._inputContainer:
+ this.#maybeSelectAll();
+ this.#maybeUntrimUrl();
+ break;
+
+ case this._searchModeIndicatorClose:
+ if (event.button != 2) {
+ this.searchMode = null;
+ if (this.view.oneOffSearchButtons) {
+ this.view.oneOffSearchButtons.selectedButton = null;
+ }
+ if (this.view.isOpen) {
+ this.startQuery({
+ event,
+ });
+ }
+ }
+ break;
+
+ case this._revertButton:
+ this.handleRevert();
+ this.select();
+ break;
+
+ case this.goButton:
+ this.handleCommand(event);
+ break;
+ }
+ }
+
+ _on_contextmenu(event) {
+ this.#lazy.addSearchEngineHelper.refreshContextMenu(event);
+
+ // Context menu opened via keyboard shortcut.
+ if (!event.button) {
+ return;
+ }
+
+ this.#maybeSelectAll();
+ }
+
+ _on_focus(event) {
+ lazy.logger.debug("Focus Event");
+ if (!this._hideFocus) {
+ this.toggleAttribute("focused", true);
+ }
+
+ // If the value was trimmed, check whether we should untrim it.
+ // This is necessary when a protocol was typed, but the whole url has
+ // invalid parts, like the origin, then editing and confirming the trimmed
+ // value would execute a search instead of visiting the typed url.
+ if (this._protocolIsTrimmed) {
+ let untrim = false;
+ let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
+ if (fixedURI) {
+ try {
+ let expectedURI = Services.io.newURI(this._untrimmedValue);
+ if (
+ lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") &&
+ this._untrimmedValue.startsWith("https://")
+ ) {
+ untrim =
+ fixedURI.displaySpec.replace("http://", "https://") !=
+ expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI.
+ } else {
+ untrim = fixedURI.displaySpec != expectedURI.displaySpec;
+ }
+ } catch (ex) {
+ untrim = true;
+ }
+ }
+ if (untrim) {
+ this._setValue(this._untrimmedValue);
+ }
+ }
+
+ if (this.focusedViaMousedown) {
+ this.view.autoOpen({ event });
+ } else {
+ if (this._untrimOnFocusAfterKeydown) {
+ // While the mousedown focus has more complex implications due to drag
+ // and double-click select, we can untrim immediately when the urlbar is
+ // focused by a keyboard shortcut.
+ this.#maybeUntrimUrl({ ignoreSelection: true });
+ }
+
+ if (this.inputField.hasAttribute("refocused-by-panel")) {
+ this.#maybeSelectAll();
+ }
+ }
+
+ this._updateUrlTooltip();
+ this.formatValue();
+
+ // Hide popup notifications, to reduce visual noise.
+ if (
+ this.getAttribute("pageproxystate") != "valid" &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+
+ Services.obs.notifyObservers(null, "urlbar-focus");
+ }
+
+ _on_mouseover() {
+ this._updateUrlTooltip();
+ }
+
+ _on_draggableregionleftmousedown() {
+ if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.view.close();
+ }
+ }
+
+ _on_mousedown(event) {
+ switch (event.currentTarget) {
+ case this: {
+ this._mousedownOnUrlbarDescendant = true;
+ if (
+ event.composedTarget != this.inputField &&
+ event.composedTarget != this._inputContainer
+ ) {
+ break;
+ }
+
+ this.focusedViaMousedown = !this.focused;
+ this._preventClickSelectsAll = this.focused;
+
+ // Keep the focus status, since the attribute may be changed
+ // upon calling this.focus().
+ const hasFocus = this.hasAttribute("focused");
+ if (event.composedTarget != this.inputField) {
+ this.focus();
+ }
+
+ // The rest of this case only cares about left clicks.
+ if (event.button != 0) {
+ break;
+ }
+
+ // Clear any previous selection unless we are focused, to ensure it
+ // doesn't affect drag selection.
+ if (this.focusedViaMousedown) {
+ this.inputField.setSelectionRange(0, 0);
+ }
+
+ // Do not suppress the focus border if we are already focused. If we
+ // did, we'd hide the focus border briefly then show it again if the
+ // user has Top Sites disabled, creating a flashing effect.
+ this.view.autoOpen({
+ event,
+ suppressFocusBorder: !hasFocus,
+ });
+ break;
+ }
+ case this.window:
+ if (this._mousedownOnUrlbarDescendant) {
+ this._mousedownOnUrlbarDescendant = false;
+ break;
+ }
+ // Don't close the view when clicking on a tab; we may want to keep the
+ // view open on tab switch, and the TabSelect event arrived earlier.
+ if (event.target.closest("tab")) {
+ break;
+ }
+
+ // Close the view when clicking on toolbars and other UI pieces that
+ // might not automatically remove focus from the input.
+ // Respect the autohide preference for easier inspecting/debugging via
+ // the browser toolbox.
+ if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ if (this.view.isOpen && !this.hasAttribute("focused")) {
+ // In this case, as blur event never happen from the inputField, we
+ // record abandonment event explicitly.
+ let blurEvent = new FocusEvent("blur", {
+ relatedTarget: this.inputField,
+ });
+ this.controller.engagementEvent.record(blurEvent, {
+ searchString: this._lastSearchString,
+ searchSource: this.getSearchSource(blurEvent),
+ });
+ }
+
+ this.view.close();
+ }
+ break;
+ }
+ }
+
+ _on_input(event) {
+ if (
+ this._autofillPlaceholder &&
+ this.value === this.userTypedValue &&
+ (event.inputType === "deleteContentBackward" ||
+ event.inputType === "deleteContentForward")
+ ) {
+ // Take a telemetry if user deleted whole autofilled value.
+ Glean.urlbar.autofillDeletion.add(1);
+ }
+
+ let value = this.value;
+ this.valueIsTyped = true;
+ this._untrimmedValue = value;
+ this._protocolIsTrimmed = false;
+ this._resultForCurrentValue = null;
+
+ this.userTypedValue = value;
+ // Unset userSelectionBehavior because the user is modifying the search
+ // string, thus there's no valid selection. This is also used by the view
+ // to set "aria-activedescendant", thus it should never get stale.
+ this.controller.userSelectionBehavior = "none";
+
+ let compositionState = this.#compositionState;
+ let compositionClosedPopup = this.#compositionClosedPopup;
+
+ // Clear composition values if we're no more composing.
+ if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
+ this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
+ this.#compositionClosedPopup = false;
+ }
+
+ this.toggleAttribute("usertyping", value);
+ this.removeAttribute("actiontype");
+
+ if (
+ this.getAttribute("pageproxystate") == "valid" &&
+ this.value != this._lastValidURLStr
+ ) {
+ this.setPageProxyState("invalid", true);
+ }
+
+ let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
+ if (
+ state.persist?.shouldPersist &&
+ this.value !== state.persist.searchTerms
+ ) {
+ state.persist.shouldPersist = false;
+ this.removeAttribute("persistsearchterms");
+ }
+
+ if (this.view.isOpen) {
+ if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) {
+ // UrlbarView rolls up all popups when it opens, but we should
+ // do the same for UrlbarInput when it's already open in case
+ // a tab preview was opened
+ this.window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow)
+ .rollupAllPopups();
+ }
+ if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) {
+ this.view.clear();
+ if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) {
+ this.view.close();
+ return;
+ }
+ }
+ } else {
+ this.view.clear();
+ }
+
+ this.view.removeAccessibleFocus();
+
+ // During composition with an IME, the following events happen in order:
+ // 1. a compositionstart event
+ // 2. some input events
+ // 3. a compositionend event
+ // 4. an input event
+
+ // We should do nothing during composition or if composition was canceled
+ // and we didn't close the popup on composition start.
+ if (
+ !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") &&
+ (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING ||
+ (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED &&
+ !compositionClosedPopup))
+ ) {
+ return;
+ }
+
+ // Autofill only when text is inserted (i.e., event.data is not empty) and
+ // it's not due to pasting.
+ const allowAutofill =
+ (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") ||
+ compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) &&
+ !!event.data &&
+ !lazy.UrlbarUtils.isPasteEvent(event) &&
+ this._maybeAutofillPlaceholder(value);
+
+ this.startQuery({
+ searchString: value,
+ allowAutofill,
+ resetSearchState: false,
+ event,
+ });
+ }
+
+ _on_selectionchange() {
+ // Confirm placeholder as user text if it gets explicitly deselected. This
+ // happens when the user wants to modify the autofilled text by either
+ // clicking on it, or pressing HOME, END, RIGHT, …
+ if (
+ this._autofillPlaceholder &&
+ this._autofillPlaceholder.value == this.value &&
+ (this._autofillPlaceholder.selectionStart != this.selectionStart ||
+ this._autofillPlaceholder.selectionEnd != this.selectionEnd)
+ ) {
+ this._autofillPlaceholder = null;
+ this.userTypedValue = this.value;
+ }
+ }
+
+ _on_select() {
+ // On certain user input, AutoCopyListener::OnSelectionChange() updates
+ // the primary selection with user-selected text (when supported).
+ // Selection::NotifySelectionListeners() then dispatches a "select" event
+ // under similar conditions via TextInputListener::OnSelectionChange().
+ // This event is received here in order to replace the primary selection
+ // from the editor with text having the adjustments of
+ // _getSelectedValueForClipboard(), such as adding the scheme for the url.
+ //
+ // Other "select" events are also received, however, and must be excluded.
+ if (
+ // _suppressPrimaryAdjustment is set during select(). Don't update
+ // the primary selection because that is not the intent of user input,
+ // which may be new tab or urlbar focus.
+ this._suppressPrimaryAdjustment ||
+ // The check on isHandlingUserInput filters out async "select" events
+ // from setSelectionRange(), which occur when autofill text is selected.
+ !this.window.windowUtils.isHandlingUserInput ||
+ !Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kSelectionClipboard
+ )
+ ) {
+ return;
+ }
+
+ let val = this._getSelectedValueForClipboard();
+ if (!val) {
+ return;
+ }
+
+ lazy.ClipboardHelper.copyStringToClipboard(
+ val,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+
+ _on_overflow(event) {
+ const targetIsPlaceholder =
+ event.originalTarget.implementedPseudoElement == "::placeholder";
+ // We only care about the non-placeholder text.
+ // This shouldn't be needed, see bug 1487036.
+ if (targetIsPlaceholder) {
+ return;
+ }
+ this._overflowing = true;
+ this.updateTextOverflow();
+ }
+
+ _on_underflow(event) {
+ const targetIsPlaceholder =
+ event.originalTarget.implementedPseudoElement == "::placeholder";
+ // We only care about the non-placeholder text.
+ // This shouldn't be needed, see bug 1487036.
+ if (targetIsPlaceholder) {
+ return;
+ }
+ this._overflowing = false;
+
+ this.updateTextOverflow();
+
+ this._updateUrlTooltip();
+ }
+
+ _on_paste(event) {
+ let originalPasteData = event.clipboardData.getData("text/plain");
+ if (!originalPasteData) {
+ return;
+ }
+
+ let oldValue = this.value;
+ let oldStart = oldValue.substring(0, this.selectionStart);
+ // If there is already non-whitespace content in the URL bar
+ // preceding the pasted content, it's not necessary to check
+ // protocols used by the pasted content:
+ if (oldStart.trim()) {
+ return;
+ }
+ let oldEnd = oldValue.substring(this.selectionEnd);
+
+ const pasteData = this.sanitizeTextFromClipboard(originalPasteData);
+
+ if (originalPasteData != pasteData) {
+ // Unfortunately we're not allowed to set the bits being pasted
+ // so cancel this event:
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ const value = oldStart + pasteData + oldEnd;
+ this._setValue(value, { valueIsTyped: true });
+ this.userTypedValue = value;
+
+ // Since we prevent the default paste event, we have to ensure the
+ // pageproxystate is updated. The paste event replaces the actual current
+ // page's URL with user-typed content, so we should set pageproxystate to
+ // invalid.
+ if (this.getAttribute("pageproxystate") == "valid") {
+ this.setPageProxyState("invalid");
+ }
+ this.toggleAttribute("usertyping", this._untrimmedValue);
+
+ // Fix up cursor/selection:
+ let newCursorPos = oldStart.length + pasteData.length;
+ this.inputField.setSelectionRange(newCursorPos, newCursorPos);
+
+ this.startQuery({
+ searchString: this.value,
+ allowAutofill: false,
+ resetSearchState: false,
+ event,
+ });
+ }
+ }
+
+ /**
+ * Sanitize and process data retrieved from the clipboard
+ *
+ * @param {string} clipboardData
+ * The original data retrieved from the clipboard.
+ * @returns {string}
+ * The sanitized paste data, ready to use.
+ */
+ sanitizeTextFromClipboard(clipboardData) {
+ let fixedURI, keywordAsSent;
+ try {
+ ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo(
+ clipboardData,
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ ));
+ } catch (e) {}
+
+ let pasteData;
+ if (keywordAsSent) {
+ // For performance reasons, we don't want to beautify a long string.
+ if (clipboardData.length < 500) {
+ // For only keywords, replace any white spaces including line break
+ // with white space.
+ pasteData = clipboardData.replace(/\s/g, " ");
+ } else {
+ pasteData = clipboardData;
+ }
+ } else if (
+ fixedURI?.scheme == "data" &&
+ !fixedURI.spec.match(/^data:.+;base64,/)
+ ) {
+ // For data url without base64, replace line break with white space.
+ pasteData = clipboardData.replace(/[\r\n]/g, " ");
+ } else {
+ // For normal url or data url having basic64, or if fixup failed, just
+ // remove line breaks.
+ pasteData = clipboardData.replace(/[\r\n]/g, "");
+ }
+
+ return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData);
+ }
+
+ /**
+ * Generate a UrlbarQueryContext from the current context.
+ *
+ * @param {object} [options]
+ * Optional params
+ * @param {boolean} [options.allowAutofill]
+ * Whether autofill is enabled.
+ * @param {string} [options.searchString]
+ * The string being searched.
+ * @param {object} [options.event]
+ * The event triggering the query.
+ * @returns {UrlbarQueryContext}
+ * The queryContext object.
+ */
+ #makeQueryContext({
+ allowAutofill = true,
+ searchString = null,
+ event = null,
+ } = {}) {
+ // When we are in actions search mode we can show more results so
+ // increase the limit.
+ let maxResults =
+ this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS
+ ? lazy.UrlbarPrefs.get("maxRichResults")
+ : UNLIMITED_MAX_RESULTS;
+ let options = {
+ allowAutofill,
+ isPrivate: this.isPrivate,
+ sapName: this.sapName,
+ maxResults,
+ searchString,
+ userContextId: parseInt(
+ this.window.gBrowser.selectedBrowser.getAttribute("usercontextid") || 0
+ ),
+ tabGroup: this.window.gBrowser.selectedTab.group?.id ?? null,
+ currentPage: this.window.gBrowser.currentURI.spec,
+ prohibitRemoteResults:
+ event &&
+ lazy.UrlbarUtils.isPasteEvent(event) &&
+ lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") <
+ event.data?.length,
+ };
+
+ if (this.searchMode) {
+ options.searchMode = this.searchMode;
+ if (this.searchMode.source) {
+ options.sources = [this.searchMode.source];
+ }
+ }
+
+ return new lazy.UrlbarQueryContext(options);
+ }
+
+ _on_scrollend() {
+ this.updateTextOverflow();
+ }
+
+ _on_TabSelect() {
+ // TabSelect may be activated by a keyboard shortcut and cause the urlbar
+ // to take focus, in this case we should not untrim.
+ this._untrimOnFocusAfterKeydown = false;
+ this._gotTabSelect = true;
+ this._afterTabSelectAndFocusChange();
+ }
+
+ _on_TabClose(event) {
+ this.controller.engagementEvent.handleBounceEventTrigger(
+ event.target.linkedBrowser
+ );
+
+ if (this.view.isOpen) {
+ // Refresh results when a tab is closed while the results view is open.
+ // This prevents switch-to-tab results from remaining in the results
+ // list after their tab is closed.
+ this.startQuery();
+ }
+ }
+
+ _on_beforeinput(event) {
+ if (event.data && this._keyDownEnterDeferred) {
+ // Ignore char key input while processing enter key.
+ event.preventDefault();
+ }
+ }
+
+ _on_keydown(event) {
+ if (event.currentTarget == this.window) {
+ // It would be great if we could more easily detect the user focusing the
+ // address bar through a keyboard shortcut, but F6 and TAB bypass are
+ // not going through commands handling.
+ // Also note we'll unset this on TabSelect, as it can focus the address
+ // bar but we should not untrim in that case.
+ this._untrimOnFocusAfterKeydown = !this.focused;
+ return;
+ }
+
+ // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if
+ // not properly handled, so let's first handle things that should not be
+ // evaluated repeatedly.
+ if (!event.repeat) {
+ this.#allTextSelectedOnKeyDown = this.#allTextSelected;
+
+ if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
+ if (this._keyDownEnterDeferred) {
+ this._keyDownEnterDeferred.reject();
+ }
+ this._keyDownEnterDeferred = Promise.withResolvers();
+ event._disableCanonization =
+ AppConstants.platform == "macosx"
+ ? this._isKeyDownWithMeta
+ : this._isKeyDownWithCtrl;
+ }
+
+ // Now set the keydown trackers for the current event, anything that wants
+ // to check the previous events should have happened before this point.
+ // The previously value is persisted until keyup, as we check if the
+ // modifiers were down, even if other keys are pressed in the meanwhile.
+ if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) {
+ this._isKeyDownWithCtrl = true;
+ }
+ if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) {
+ this._isKeyDownWithMeta = true;
+ }
+ // This is used in keyup, so it can be set every time.
+ this._isKeyDownWithMetaAndLeft =
+ this._isKeyDownWithMeta &&
+ !event.shiftKey &&
+ event.keyCode == KeyEvent.DOM_VK_LEFT;
+
+ this._toggleActionOverride(event);
+ }
+
+ // Due to event deferring, it's possible preventDefault() won't be invoked
+ // soon enough to actually prevent some of the default behaviors, thus we
+ // have to handle the event "twice". This first immediate call passes false
+ // as second argument so that handleKeyNavigation will only simulate the
+ // event handling, without actually executing actions.
+ // TODO (Bug 1541806): improve this handling, maybe by delaying actions
+ // instead of events.
+ if (this.eventBufferer.shouldDeferEvent(event)) {
+ this.controller.handleKeyNavigation(event, false);
+ }
+ this.eventBufferer.maybeDeferEvent(event, () => {
+ this.controller.handleKeyNavigation(event);
+ });
+ }
+
+ async _on_keyup(event) {
+ if (event.currentTarget == this.window) {
+ this._untrimOnFocusAfterKeydown = false;
+ return;
+ }
+
+ if (this.#allTextSelectedOnKeyDown) {
+ let moveCursorToStart = this.#isHomeKeyUpEvent(event);
+ // We must set the selection immediately because:
+ // - on Mac Fn + Left is not handled properly as Home
+ // - untrim depends on text not being fully selected.
+ if (moveCursorToStart) {
+ this.selectionStart = this.selectionEnd = 0;
+ }
+ this.#maybeUntrimUrl({ moveCursorToStart });
+ }
+ if (event.keyCode === KeyEvent.DOM_VK_META) {
+ this._isKeyDownWithMeta = false;
+ this._isKeyDownWithMetaAndLeft = false;
+ }
+ if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
+ this._isKeyDownWithCtrl = false;
+ }
+
+ this._toggleActionOverride(event);
+
+ // Pressing Enter key while pressing Meta key, and next, even when releasing
+ // Enter key before releasing Meta key, the keyup event is not fired.
+ // Therefore, if Enter keydown is detecting, continue the post processing
+ // for Enter key when any keyup event is detected.
+ if (this._keyDownEnterDeferred) {
+ if (this._keyDownEnterDeferred.loadedContent) {
+ try {
+ const loadingBrowser = await this._keyDownEnterDeferred.promise;
+ // Ensure the selected browser didn't change in the meanwhile.
+ if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
+ loadingBrowser.focus();
+ // Make sure the domain name stays visible for spoof protection and usability.
+ this.inputField.setSelectionRange(0, 0);
+ }
+ } catch (ex) {
+ // Not all the Enter actions in the urlbar will cause a navigation, then it
+ // is normal for this to be rejected.
+ // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
+ // to ensure not overwriting the new value created by keydown.
+ }
+ } else {
+ // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately.
+ this._keyDownEnterDeferred.resolve();
+ }
+
+ this._keyDownEnterDeferred = null;
+ }
+ }
+
+ _on_compositionstart() {
+ if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
+ throw new Error("Trying to start a nested composition?");
+ }
+ this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING;
+
+ if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
+ return;
+ }
+
+ // Close the view. This will also stop searching.
+ if (this.view.isOpen) {
+ // We're closing the view, but we want to retain search mode if the
+ // selected result was previewing it.
+ if (this.searchMode) {
+ // If we entered search mode with an empty string, clear userTypedValue,
+ // otherwise confirmSearchMode may try to set it as value.
+ // This can happen for example if we entered search mode typing a
+ // a partial engine domain and selecting a tab-to-search result.
+ if (!this.value) {
+ this.userTypedValue = null;
+ }
+ this.confirmSearchMode();
+ }
+ this.#compositionClosedPopup = true;
+ this.view.close();
+ } else {
+ this.#compositionClosedPopup = false;
+ }
+ }
+
+ _on_compositionend(event) {
+ if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
+ throw new Error("Trying to stop a non existing composition?");
+ }
+
+ if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
+ // Clear the selection and the cached result, since they refer to the
+ // state before this composition. A new input even will be generated
+ // after this.
+ this.view.clearSelection();
+ this._resultForCurrentValue = null;
+ }
+
+ // We can't yet retrieve the committed value from the editor, since it isn't
+ // completely committed yet. We'll handle it at the next input event.
+ this.#compositionState = event.data
+ ? lazy.UrlbarUtils.COMPOSITION.COMMIT
+ : lazy.UrlbarUtils.COMPOSITION.CANCELED;
+ }
+
+ _on_dragstart(event) {
+ // Drag only if the gesture starts from the input field.
+ let nodePosition = this.inputField.compareDocumentPosition(
+ event.originalTarget
+ );
+ if (
+ event.target != this.inputField &&
+ !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
+ ) {
+ return;
+ }
+
+ // Don't cover potential drop targets on the toolbars or in content.
+ this.view.close();
+
+ // Only customize the drag data if the entire value is selected and it's a
+ // loaded URI. Use default behavior otherwise.
+ if (
+ !this.#allTextSelected ||
+ this.getAttribute("pageproxystate") != "valid"
+ ) {
+ return;
+ }
+
+ let uri = this.makeURIReadable(this.window.gBrowser.currentURI);
+ let href = uri.displaySpec;
+ let title = this.window.gBrowser.contentTitle || href;
+
+ event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
+ event.dataTransfer.setData("text/plain", href);
+ event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
+ event.dataTransfer.effectAllowed = "copyLink";
+ event.stopPropagation();
+ }
+
+ /**
+ * Handles dragover events for the input.
+ *
+ * @param {DragEvent} event
+ */
+ _on_dragover(event) {
+ if (!getDroppableData(event)) {
+ event.dataTransfer.dropEffect = "none";
+ }
+ }
+
+ /**
+ * Handles dropping of data on the input.
+ *
+ * @param {DragEvent} event
+ */
+ _on_drop(event) {
+ let droppedItem = getDroppableData(event);
+ let droppedURL = URL.isInstance(droppedItem)
+ ? droppedItem.href
+ : droppedItem;
+ if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) {
+ let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event);
+ this.value = droppedURL;
+ this.setPageProxyState("invalid");
+ this.focus();
+ // To simplify tracking of events, register an initial event for event
+ // telemetry, to replace the missing input event.
+ let queryContext = this.#makeQueryContext({ searchString: droppedURL });
+ this.controller.setLastQueryContextCache(queryContext);
+ this.controller.engagementEvent.start(event, queryContext);
+ this.handleNavigation({ triggeringPrincipal: principal });
+ if (this.#isAddressbar) {
+ // For safety reasons, in the drop case we don't want to immediately show
+ // the dropped value, instead we want to keep showing the current page
+ // url until an onLocationChange happens.
+ // See the handling in `setURI` for further details.
+ this.userTypedValue = null;
+ this.setURI({ dueToTabSwitch: true });
+ }
+ }
+ }
+
+ _on_customizationstarting() {
+ this.incrementBreakoutBlockerCount();
+ this.blur();
+ }
+
+ _on_aftercustomization() {
+ this.decrementBreakoutBlockerCount();
+ this.#updateLayoutBreakout();
+ }
+
+ uiDensityChanged() {
+ if (this.#breakoutBlockerCount) {
+ return;
+ }
+ this.#updateLayoutBreakout();
+ }
+
+ _on_toolbarvisibilitychange() {
+ this.#updateTextboxPositionNextFrame();
+ }
+
+ _on_DOMMenuBarActive() {
+ this.#updateTextboxPositionNextFrame();
+ }
+
+ _on_DOMMenuBarInactive() {
+ this.#updateTextboxPositionNextFrame();
+ }
+
+ #allTextSelectedOnKeyDown = false;
+ get #allTextSelected() {
+ return this.selectionStart == 0 && this.selectionEnd == this.value.length;
+ }
+
+ /**
+ * @param {string} value
+ * A untrimmed address bar input.
+ * @returns {nsILoadInfo.SchemelessInputType}
+ * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input
+ * doesn't start with a scheme relevant for schemeless HTTPS-First
+ * (http://, https:// and file://).
+ * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme.
+ */
+ #getSchemelessInput(value) {
+ return ["http://", "https://", "file://"].every(
+ scheme => !value.trim().startsWith(scheme)
+ )
+ ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless
+ : Ci.nsILoadInfo.SchemelessInputTypeSchemeful;
+ }
+
+ get #isOpenedPageInBlankTargetLoading() {
+ return (
+ this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
+ ?.count === 0 &&
+ this.window.gBrowser.selectedBrowser.browsingContext
+ .nonWebControlledBlankURI
+ );
+ }
+
+ // Search modes are per browser and are stored in the `searchModes` property of this map.
+ // For a browser, search mode can be in preview mode, confirmed, or both.
+ // Typically, search mode is entered in preview mode with a particular
+ // source and is confirmed with the same source once a query starts. It's
+ // also possible for a confirmed search mode to be replaced with a preview
+ // mode with a different source, and in those cases, we need to re-confirm
+ // search mode when preview mode is exited. In addition, only confirmed
+ // search modes should be restored across sessions. We therefore need to
+ // keep track of both the current confirmed and preview modes, per browser.
+ //
+ // For each browser with a search mode, this maps the browser to an object
+ // like this: { preview, confirmed }. Both `preview` and `confirmed` are
+ // search mode objects; see the setSearchMode documentation. Either one may
+ // be undefined if that particular mode is not active for the browser.
+
+ /**
+ * Tracks a state object per browser.
+ */
+ #browserStates = new WeakMap();
+
+ get #selectedText() {
+ return this.editor.selection.toStringWithFormat(
+ "text/plain",
+ Ci.nsIDocumentEncoder.OutputPreformatted |
+ Ci.nsIDocumentEncoder.OutputRaw,
+ 0
+ );
+ }
+
+ /**
+ * Check whether a key event has a similar effect as the Home key.
+ *
+ * @param {KeyboardEvent} event A Keyboard event
+ * @returns {boolean} Whether the even will act like the Home key.
+ */
+ #isHomeKeyUpEvent(event) {
+ let isMac = AppConstants.platform === "macosx";
+ return (
+ // On MacOS this can be generated with Fn + Left.
+ event.keyCode == KeyEvent.DOM_VK_HOME ||
+ // Windows and Linux also support Ctrl + Left.
+ (!isMac &&
+ event.keyCode == KeyboardEvent.DOM_VK_LEFT &&
+ event.ctrlKey &&
+ !event.shiftKey) ||
+ // MacOS supports other combos to move cursor at the start of the line.
+ // For example Ctrl + A.
+ (isMac &&
+ event.keyCode == KeyboardEvent.DOM_VK_A &&
+ event.ctrlKey &&
+ !event.shiftKey) ||
+ // And also Cmd (Meta) + Left.
+ // Unfortunately on MacOS it's not possible to detect combos with the meta
+ // key during the keyup event, due to how the OS handles events. Thus we
+ // record the combo on keydown, and check for it here.
+ (isMac &&
+ event.keyCode == KeyEvent.DOM_VK_META &&
+ this._isKeyDownWithMetaAndLeft)
+ );
+ }
+}
+
+/**
+ * Tries to extract droppable data from a DND event.
+ *
+ * @param {DragEvent} event The DND event to examine.
+ * @returns {URL|string|null}
+ * null if there's a security reason for which we should do nothing.
+ * A URL object if it's a value we can load.
+ * A string value otherwise.
+ */
+function getDroppableData(event) {
+ let links;
+ try {
+ links = Services.droppedLinkHandler.dropLinks(event);
+ } catch (ex) {
+ // This is either an unexpected failure or a security exception; in either
+ // case we should always return null.
+ return null;
+ }
+ // The URL bar automatically handles inputs with newline characters,
+ // so we can get away with treating text/x-moz-url flavours as text/plain.
+ if (links[0]?.url) {
+ event.preventDefault();
+ let href = links[0].url;
+ if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
+ // We may have stripped an unsafe protocol like javascript: and if so
+ // there's no point in handling a partial drop.
+ event.stopImmediatePropagation();
+ return null;
+ }
+
+ // If this fails, checkLoadURIStrWithPrincipal would also fail,
+ // as that's what it does with things that don't pass the IO
+ // service's newURI constructor without fixup. It's conceivable we
+ // may want to relax this check in the future (so e.g. www.foo.com
+ // gets fixed up), but not right now.
+ let url = URL.parse(href);
+ if (url) {
+ // If we succeed, try to pass security checks. If this works, return the
+ // URL object. If the *security checks* fail, return null.
+ try {
+ let principal =
+ Services.droppedLinkHandler.getTriggeringPrincipal(event);
+ Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+ principal,
+ url.href,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
+ );
+ return url;
+ } catch (ex) {
+ return null;
+ }
+ }
+ // We couldn't make a URL out of this. Continue on, and return text below.
+ }
+ // Handle as text.
+ return event.dataTransfer.getData("text/plain");
+}
+
+/**
+ * Decodes the given URI for displaying it in the address bar without losing
+ * information, such that hitting Enter again will load the same URI.
+ *
+ * @param {nsIURI} aURI
+ * The URI to decode
+ * @returns {string}
+ * The decoded URI
+ */
+function losslessDecodeURI(aURI) {
+ let scheme = aURI.scheme;
+ let value = aURI.displaySpec;
+
+ // Try to decode as UTF-8 if there's no encoding sequence that we would break.
+ if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
+ let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
+ if (decodeASCIIOnly) {
+ // This only decodes ascii characters (hex) 20-7e, except 25 (%).
+ // This avoids both cases stipulated below (%-related issues, and \r, \n
+ // and \t, which would be %0d, %0a and %09, respectively) as well as any
+ // non-US-ascii characters.
+ value = value.replace(
+ /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
+ decodeURI
+ );
+ } else {
+ try {
+ value = decodeURI(value)
+ // decodeURI decodes %25 to %, which creates unintended encoding
+ // sequences. Re-encode it, unless it's part of a sequence that
+ // survived decodeURI, i.e. one for:
+ // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
+ // (RFC 3987 section 3.2)
+ .replace(
+ /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
+ encodeURIComponent
+ );
+ } catch (e) {}
+ }
+ }
+
+ // IMPORTANT: The following regular expressions are Unicode-aware due to /v.
+ // Avoid matching high or low surrogate pairs directly, always work with
+ // full Unicode scalar values.
+
+ // Encode potentially invisible characters:
+ // U+0000-001F: C0/C1 control characters
+ // U+007F-009F: commands
+ // U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
+ // U+2028-2029: line and paragraph separators
+ // U+2800: braille empty pattern
+ // U+FFFC: object replacement character
+ // Encode any trailing whitespace that may be part of a pasted URL, so that it
+ // doesn't get eaten away by the location bar (bug 410726).
+ // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
+ // where they would push part of the URL to overflow the location bar
+ // (bug 1395508). A single space, or the last space if the are many, is
+ // preserved to maintain readability of certain urls if it's not followed by a
+ // control or separator character. We only do this for the common space,
+ // because others may be eaten when copied to the clipboard,so it's safer to
+ // preserve them encoded.
+ value = value.replace(
+ // eslint-disable-next-line no-control-regex
+ /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv,
+ encodeURIComponent
+ );
+
+ // Encode characters that are ignorable, can't be rendered usefully, or may
+ // confuse users.
+ //
+ // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
+ // per bug 582186:
+ // U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
+ // U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
+ // U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF
+ // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
+ // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
+ // Other format characters in the Cf category that are unlikely to be rendered
+ // usefully:
+ // U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3
+ // Mimicking UI parts:
+ // U+1F50F-1F513, U+1F6E1
+ // Unassigned codepoints, sometimes shown as empty glyphs.
+ value = value.replace(
+ // eslint-disable-next-line no-misleading-character-class
+ /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv,
+ encodeURIComponent
+ );
+ return value;
+}
+
+/**
+ * Handles copy and cut commands for the urlbar.
+ */
+class CopyCutController {
+ /**
+ * @param {UrlbarInput} urlbar
+ * The UrlbarInput instance to use this controller for.
+ */
+ constructor(urlbar) {
+ this.urlbar = urlbar;
+ }
+
+ /**
+ * @param {string} command
+ * The name of the command to handle.
+ */
+ doCommand(command) {
+ let urlbar = this.urlbar;
+ let val = urlbar._getSelectedValueForClipboard();
+ if (!val) {
+ return;
+ }
+
+ if (command == "cmd_cut" && this.isCommandEnabled(command)) {
+ let start = urlbar.selectionStart;
+ let end = urlbar.selectionEnd;
+ urlbar.inputField.value =
+ urlbar.inputField.value.substring(0, start) +
+ urlbar.inputField.value.substring(end);
+ urlbar.inputField.setSelectionRange(start, start);
+
+ let event = new UIEvent("input", {
+ bubbles: true,
+ cancelable: false,
+ view: urlbar.window,
+ detail: 0,
+ });
+ urlbar.inputField.dispatchEvent(event);
+ }
+
+ lazy.ClipboardHelper.copyString(val);
+ }
+
+ /**
+ * @param {string} command
+ * The name of the command to check.
+ * @returns {boolean}
+ * Whether the command is handled by this controller.
+ */
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_copy":
+ case "cmd_cut":
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param {string} command
+ * The name of the command to check.
+ * @returns {boolean}
+ * Whether the command should be enabled.
+ */
+ isCommandEnabled(command) {
+ return (
+ this.supportsCommand(command) &&
+ (command != "cmd_cut" || !this.urlbar.readOnly) &&
+ this.urlbar.selectionStart < this.urlbar.selectionEnd
+ );
+ }
+
+ onEvent() {}
+}
+
+/**
+ * Manages the Add Search Engine contextual menu entries.
+ *
+ * Note: setEnginesFromBrowser must be invoked from the outside when the
+ * page provided engines list changes.
+ * refreshContextMenu must be invoked when the context menu is opened.
+ */
+class AddSearchEngineHelper {
+ /**
+ * @type {UrlbarSearchOneOffs}
+ */
+ shortcutButtons;
+
+ /**
+ * @param {UrlbarInput} input The parent UrlbarInput.
+ */
+ constructor(input) {
+ this.input = input;
+ this.shortcutButtons = input.view.oneOffSearchButtons;
+ }
+
+ /**
+ * If there's more than this number of engines, the context menu offers
+ * them in a submenu.
+ *
+ * @returns {number}
+ */
+ get maxInlineEngines() {
+ return this.shortcutButtons._maxInlineAddEngines;
+ }
+
+ /**
+ * Invoked by OpenSearchManager when the list of available engines changes.
+ *
+ * @param {object} browser The current browser.
+ * @param {object} engines The updated list of available engines.
+ */
+ setEnginesFromBrowser(browser, engines) {
+ this.browsingContext = browser.browsingContext;
+ // Make a copy of the array for state comparison.
+ engines = engines.slice();
+ if (!this._sameEngines(this.engines, engines)) {
+ this.engines = engines;
+ this.shortcutButtons?.updateWebEngines();
+ }
+ }
+
+ _sameEngines(engines1, engines2) {
+ if (engines1?.length != engines2?.length) {
+ return false;
+ }
+ return lazy.ObjectUtils.deepEqual(
+ engines1.map(e => e.title),
+ engines2.map(e => e.title)
+ );
+ }
+
+ _createMenuitem(engine, index) {
+ let elt = this.input.document.createXULElement("menuitem");
+ elt.setAttribute("anonid", `add-engine-${index}`);
+ elt.classList.add("menuitem-iconic");
+ elt.classList.add("context-menu-add-engine");
+ this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", {
+ engineName: engine.title,
+ });
+ elt.setAttribute("uri", engine.uri);
+ if (engine.icon) {
+ elt.setAttribute("image", engine.icon);
+ } else {
+ elt.removeAttribute("image");
+ }
+ elt.addEventListener("command", this._onCommand.bind(this));
+ return elt;
+ }
+
+ _createMenu(engine) {
+ let elt = this.input.document.createXULElement("menu");
+ elt.setAttribute("anonid", "add-engine-menu");
+ elt.classList.add("menu-iconic");
+ elt.classList.add("context-menu-add-engine");
+ this.input.document.l10n.setAttributes(
+ elt,
+ "search-one-offs-add-engine-menu"
+ );
+ if (engine.icon) {
+ elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon));
+ }
+ let popup = this.input.document.createXULElement("menupopup");
+ elt.appendChild(popup);
+ return elt;
+ }
+
+ refreshContextMenu() {
+ let engines = this.engines;
+ let contextMenu = this.input.querySelector("moz-input-box").menupopup;
+
+ // Certain operations, like customization, destroy and recreate widgets,
+ // so we cannot rely on cached elements.
+ if (!contextMenu.querySelector(".menuseparator-add-engine")) {
+ this.contextSeparator =
+ this.input.document.createXULElement("menuseparator");
+ this.contextSeparator.setAttribute("anonid", "add-engine-separator");
+ this.contextSeparator.classList.add("menuseparator-add-engine");
+ this.contextSeparator.collapsed = true;
+ contextMenu.appendChild(this.contextSeparator);
+ }
+
+ this.contextSeparator.collapsed = !engines.length;
+ let curElt = this.contextSeparator;
+ // Remove the previous items, if any.
+ for (let elt = curElt.nextElementSibling; elt; ) {
+ let nextElementSibling = elt.nextElementSibling;
+ elt.remove();
+ elt = nextElementSibling;
+ }
+
+ // If the page provides too many engines, we only show a single menu entry
+ // with engines in a submenu.
+ if (engines.length > this.maxInlineEngines) {
+ // Set the menu button's image to the image of the first engine. The
+ // offered engines may have differing images, so there's no perfect
+ // choice here.
+ let elt = this._createMenu(engines[0]);
+ this.contextSeparator.insertAdjacentElement("afterend", elt);
+ curElt = elt.lastElementChild;
+ }
+
+ // Insert the engines, either in the contextual menu or the sub menu.
+ for (let i = 0; i < engines.length; ++i) {
+ let elt = this._createMenuitem(engines[i], i);
+ if (curElt.localName == "menupopup") {
+ curElt.appendChild(elt);
+ } else {
+ curElt.insertAdjacentElement("afterend", elt);
+ }
+ curElt = elt;
+ }
+ }
+
+ async _onCommand(event) {
+ let added = await lazy.SearchUIUtils.addOpenSearchEngine(
+ event.target.getAttribute("uri"),
+ event.target.getAttribute("image"),
+ this.browsingContext
+ ).catch(console.error);
+ if (added) {
+ // Remove the offered engine from the list. The browser updated the
+ // engines list at this point, so we just have to refresh the menu.)
+ this.refreshContextMenu();
+ }
+ }
+}
+
+customElements.define("moz-urlbar", UrlbarInput);
diff --git a/browser/components/urlbar/jar.mn b/browser/components/urlbar/jar.mn
@@ -3,4 +3,5 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
+ content/browser/urlbar/SmartbarInput.mjs (content/SmartbarInput.mjs)
content/browser/urlbar/UrlbarInput.mjs (content/UrlbarInput.mjs)
diff --git a/browser/components/urlbar/tsconfig.json b/browser/components/urlbar/tsconfig.json
@@ -1,6 +1,7 @@
{
"include": ["**/*.mjs", "types/*.ts"],
"exclude": [
+ "content/SmartbarInput.mjs",
"content/UrlbarInput.mjs",
"UrlbarProviderGlobalActions.sys.mjs",
"UrlbarView.sys.mjs",