tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SmartbarInput.mjs (209753B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { createEditor } from "chrome://browser/content/urlbar/SmartbarInputUtils.mjs";
      6 
      7 const { XPCOMUtils } = ChromeUtils.importESModule(
      8  "resource://gre/modules/XPCOMUtils.sys.mjs"
      9 );
     10 
     11 const { AppConstants } = ChromeUtils.importESModule(
     12  "resource://gre/modules/AppConstants.sys.mjs"
     13 );
     14 
     15 /**
     16 * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs"
     17 */
     18 
     19 const lazy = XPCOMUtils.declareLazy({
     20  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     21  BrowserSearchTelemetry:
     22    "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     23  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     24  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     25  ExtensionSearchHandler:
     26    "resource://gre/modules/ExtensionSearchHandler.sys.mjs",
     27  ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
     28  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     29  PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
     30  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     31  ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
     32  SearchModeSwitcher:
     33    "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs",
     34  SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
     35  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
     36  SmartbarInputController:
     37    "chrome://browser/content/urlbar/SmartbarInputController.mjs",
     38  UrlbarController:
     39    "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs",
     40  UrlbarEventBufferer:
     41    "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs",
     42  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     43  UrlbarQueryContext:
     44    "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     45  UrlbarProviderGlobalActions:
     46    "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs",
     47  UrlbarProviderOpenTabs:
     48    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     49  UrlbarSearchUtils:
     50    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     51  UrlbarTokenizer:
     52    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     53  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     54  UrlbarValueFormatter:
     55    "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs",
     56  UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
     57  UrlbarSearchTermsPersistence:
     58    "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs",
     59  UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
     60  ClipboardHelper: {
     61    service: "@mozilla.org/widget/clipboardhelper;1",
     62    iid: Ci.nsIClipboardHelper,
     63  },
     64  QueryStringStripper: {
     65    service: "@mozilla.org/url-query-string-stripper;1",
     66    iid: Ci.nsIURLQueryStringStripper,
     67  },
     68  QUERY_STRIPPING_STRIP_ON_SHARE: {
     69    pref: "privacy.query_stripping.strip_on_share.enabled",
     70    default: false,
     71  },
     72  logger: () => lazy.UrlbarUtils.getLogger({ prefix: "SmartbarInput" }),
     73 });
     74 
     75 const UNLIMITED_MAX_RESULTS = 99;
     76 
     77 let getBoundsWithoutFlushing = element =>
     78  element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
     79 let px = number => number.toFixed(2) + "px";
     80 
     81 /**
     82 * Implements the text input part of the address bar UI.
     83 */
     84 export class SmartbarInput extends HTMLElement {
     85  static get #markup() {
     86    return `
     87      <hbox class="urlbar-background"/>
     88      <hbox class="urlbar-input-container"
     89            flex="1"
     90            pageproxystate="invalid">
     91        <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot>
     92        <toolbarbutton id="urlbar-searchmode-switcher"
     93                       class="searchmode-switcher chromeclass-toolbar-additional"
     94                       align="center"
     95                       aria-expanded="false"
     96                       aria-haspopup="menu"
     97                       tooltip="dynamic-shortcut-tooltip"
     98                       data-l10n-id="urlbar-searchmode-default"
     99                       type="menu">
    100          <image class="searchmode-switcher-icon toolbarbutton-icon"/>
    101          <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker"
    102                 data-l10n-id="urlbar-searchmode-dropmarker" />
    103          <menupopup class="searchmode-switcher-popup toolbar-menupopup"
    104                     consumeoutsideclicks="false">
    105            <label class="searchmode-switcher-popup-description"
    106                   data-l10n-id="urlbar-searchmode-popup-description"
    107                   role="heading" />
    108            <menuseparator/>
    109            <menuseparator class="searchmode-switcher-popup-footer-separator"/>
    110            <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic"
    111                      data-action="openpreferences"
    112                      image="chrome://global/skin/icons/settings.svg"
    113                      data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/>
    114          </menupopup>
    115        </toolbarbutton>
    116        <box class="searchmode-switcher-chicklet">
    117          <label class="searchmode-switcher-title" />
    118          <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button"
    119                         data-action="exitsearchmode"
    120                         role="button"
    121                         data-l10n-id="urlbar-searchmode-exit-button" />
    122        </box>
    123        <moz-urlbar-slot name="site-info"> </moz-urlbar-slot>
    124        <moz-input-box tooltip="aHTMLTooltip"
    125                       class="urlbar-input-box"
    126                       flex="1"
    127                       role="combobox"
    128                       aria-owns="urlbar-results">
    129          <html:input id="urlbar-scheme"
    130                      required="required"/>
    131          <html:input id="urlbar-input"
    132                      class="urlbar-input textbox-input"
    133                      aria-controls="urlbar-results"
    134                      aria-autocomplete="both"
    135                      inputmode="mozAwesomebar"
    136                      data-l10n-id="smartbar-placeholder"/>
    137        </moz-input-box>
    138        <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot>
    139        <image class="urlbar-icon urlbar-go-button"
    140               role="button"
    141               data-l10n-id="urlbar-go-button"/>
    142        <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot>
    143      </hbox>
    144      <vbox class="urlbarView"
    145            context=""
    146            role="group"
    147            tooltip="aHTMLTooltip">
    148        <html:div class="urlbarView-body-outer">
    149          <html:div class="urlbarView-body-inner">
    150            <html:div id="urlbar-results"
    151                      class="urlbarView-results"
    152                      role="listbox"/>
    153          </html:div>
    154        </html:div>
    155        <menupopup class="urlbarView-result-menu"
    156                   consumeoutsideclicks="false"/>
    157        <hbox class="search-one-offs"
    158              includecurrentengine="true"
    159              disabletab="true"/>
    160      </vbox>`;
    161  }
    162 
    163  /**
    164   * @type {DocumentFragment=}
    165   *
    166   * The cached fragment.
    167   */
    168  static #fragment;
    169 
    170  /** @type {DocumentFragment} */
    171  static get fragment() {
    172    if (!this.#fragment) {
    173      this.#fragment = window.MozXULElement.parseXULToFragment(this.#markup);
    174    }
    175    // @ts-ignore
    176    return document.importNode(this.#fragment, true);
    177  }
    178 
    179  static #inputFieldEvents = [
    180    "compositionstart",
    181    "compositionend",
    182    "contextmenu",
    183    "dragover",
    184    "dragstart",
    185    "drop",
    186    "focus",
    187    "blur",
    188    "input",
    189    "beforeinput",
    190    "keydown",
    191    "keyup",
    192    "mouseover",
    193    "overflow",
    194    "underflow",
    195    "paste",
    196    "scrollend",
    197    "select",
    198    "selectionchange",
    199  ];
    200 
    201  static #validSmartbarModes = ["chat", "search", "navigate"];
    202 
    203  #allowBreakout = false;
    204  #gBrowserListenersAdded = false;
    205  #breakoutBlockerCount = 0;
    206  #isAddressbar = false;
    207  /**
    208   * `True` if this instance is in `smartbar` mode.
    209   *
    210   * Smartbar mode is enabled by adding the attribute `sap-name="smartbar"`.
    211   * Both `#isSmartbarMode` and `#isAddressbar` are `false` if `sap-name` is neither
    212   * `smartbar` nor `urlbar`.
    213   */
    214  #isSmartbarMode = false;
    215  #sapName = "";
    216  #smartbarEditor = null;
    217  #smartbarInputController = null;
    218  #smartbarMode = "search";
    219  _userTypedValue = "";
    220  _actionOverrideKeyCount = 0;
    221  _lastValidURLStr = "";
    222  _valueOnLastSearch = "";
    223  _suppressStartQuery = false;
    224  _suppressPrimaryAdjustment = false;
    225  _lastSearchString = "";
    226  // Tracks IME composition.
    227  #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
    228  #compositionClosedPopup = false;
    229 
    230  valueIsTyped = false;
    231 
    232  // Properties accessed in tests.
    233  lastQueryContextPromise = Promise.resolve();
    234  _autofillPlaceholder = null;
    235  _resultForCurrentValue = null;
    236  _untrimmedValue = "";
    237  _enableAutofillPlaceholder = true;
    238 
    239  constructor() {
    240    super();
    241 
    242    this.window = this.ownerGlobal;
    243    this.document = this.window.document;
    244    this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window);
    245 
    246    lazy.UrlbarPrefs.addObserver(this);
    247    window.addEventListener("unload", () => {
    248      // Stop listening to pref changes to make sure we don't init the new
    249      // searchbar in closed windows that have not been gc'd yet.
    250      lazy.UrlbarPrefs.removeObserver(this);
    251    });
    252  }
    253 
    254  /**
    255   * Populates moz-urlbar-slots by moving all children with a urlbar-slot
    256   * attribute into their moz-urlbar-slots and removing the slots.
    257   *
    258   * Should only be called once all children have been parsed.
    259   */
    260  #populateSlots() {
    261    let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]");
    262    for (let slot of urlbarSlots) {
    263      let slotName = slot.getAttribute("name");
    264      let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`);
    265 
    266      for (let node of nodes) {
    267        slot.parentNode.insertBefore(node, slot);
    268      }
    269 
    270      slot.remove();
    271    }
    272 
    273    // Slotted elements only used by the addressbar.
    274    // Will be null for searchbar and others.
    275    this._identityBox = this.querySelector(".identity-box");
    276    this._revertButton = this.querySelector(".urlbar-revert-button");
    277    // Pre scotch bonnet search mode indicator (addressbar only).
    278    this._searchModeIndicator = this.querySelector(
    279      "#urlbar-search-mode-indicator"
    280    );
    281    this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector(
    282      "#urlbar-search-mode-indicator-title"
    283    );
    284    this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector(
    285      "#urlbar-search-mode-indicator-close"
    286    );
    287  }
    288 
    289  /**
    290   * Initialization that happens once on the first connect.
    291   */
    292  #initOnce() {
    293    this.#sapName = this.getAttribute("sap-name");
    294    this.#isAddressbar = this.#sapName == "urlbar";
    295    this.#isSmartbarMode = this.#sapName == "smartbar";
    296 
    297    // This listener must be added before connecting the fragment
    298    // because the event could fire while or after connecting it.
    299    this.addEventListener(
    300      "moz-input-box-rebuilt",
    301      this.#onContextMenuRebuilt.bind(this)
    302    );
    303 
    304    this.appendChild(SmartbarInput.fragment);
    305 
    306    // Make sure all children have been parsed before calling #populateSlots.
    307    if (document.readyState === "loading") {
    308      document.addEventListener(
    309        "DOMContentLoaded",
    310        () => this.#populateSlots(),
    311        { once: true }
    312      );
    313    } else {
    314      this.#populateSlots();
    315    }
    316 
    317    this.panel = this.querySelector(".urlbarView");
    318    this.inputField = /** @type {HTMLInputElement} */ (
    319      this.querySelector(".urlbar-input")
    320    );
    321    if (this.#isSmartbarMode) {
    322      this.#ensureSmartbarEditor();
    323    }
    324    this._inputContainer = this.querySelector(".urlbar-input-container");
    325 
    326    this.controller = new lazy.UrlbarController({ input: this });
    327    this.view = new lazy.UrlbarView(this);
    328    this.searchModeSwitcher = new lazy.SearchModeSwitcher(this);
    329 
    330    // The event bufferer can be used to defer events that may affect users
    331    // muscle memory; for example quickly pressing DOWN+ENTER should end up
    332    // on a predictable result, regardless of the search status. The event
    333    // bufferer will invoke the handling code at the right time.
    334    this.eventBufferer = new lazy.UrlbarEventBufferer(this);
    335 
    336    // Forward certain properties.
    337    // Note if you are extending these, you'll also need to extend the inline
    338    // type definitions.
    339    const READ_WRITE_PROPERTIES = [
    340      "placeholder",
    341      "readOnly",
    342      "selectionStart",
    343      "selectionEnd",
    344    ];
    345 
    346    for (let property of READ_WRITE_PROPERTIES) {
    347      Object.defineProperty(this, property, {
    348        enumerable: true,
    349        get() {
    350          return this.inputField[property];
    351        },
    352        set(val) {
    353          this.inputField[property] = val;
    354        },
    355      });
    356    }
    357 
    358    // The engine name is not known yet, but update placeholder anyway to
    359    // reflect value of keyword.enabled or set the searchbar placeholder.
    360    this._setPlaceholder(null);
    361  }
    362 
    363  connectedCallback() {
    364    if (
    365      this.getAttribute("sap-name") == "searchbar" &&
    366      !lazy.UrlbarPrefs.get("browser.search.widget.new")
    367    ) {
    368      return;
    369    }
    370 
    371    this.#init();
    372  }
    373 
    374  #init() {
    375    if (!this.controller) {
    376      this.#initOnce();
    377    }
    378 
    379    if (this.sapName == "searchbar") {
    380      this.parentNode.setAttribute("overflows", "false");
    381    }
    382 
    383    // Don't attach event listeners if the toolbar is not visible
    384    // in this window or the urlbar is readonly.
    385    if (
    386      !this.window.toolbar.visible ||
    387      this.window.document.documentElement.hasAttribute("taskbartab") ||
    388      this.readOnly
    389    ) {
    390      return;
    391    }
    392 
    393    this._initCopyCutController();
    394 
    395    for (let event of SmartbarInput.#inputFieldEvents) {
    396      this.inputField.addEventListener(event, this);
    397    }
    398 
    399    // These are on the window to detect focusing shortcuts like F6.
    400    this.window.addEventListener("keydown", this);
    401    this.window.addEventListener("keyup", this);
    402 
    403    this.window.addEventListener("mousedown", this);
    404    if (AppConstants.platform == "win") {
    405      this.window.addEventListener("draggableregionleftmousedown", this);
    406    }
    407    this.addEventListener("mousedown", this);
    408 
    409    // This listener handles clicks from our children too, included the search mode
    410    // indicator close button.
    411    this._inputContainer.addEventListener("click", this);
    412 
    413    // This is used to detect commands launched from the panel, to avoid
    414    // recording abandonment events when the command causes a blur event.
    415    this.view.panel.addEventListener("command", this, true);
    416 
    417    this.window.addEventListener("customizationstarting", this);
    418    this.window.addEventListener("aftercustomization", this);
    419    this.window.addEventListener("toolbarvisibilitychange", this);
    420    let menuToolbar = this.window.document.getElementById("toolbar-menubar");
    421    if (menuToolbar) {
    422      menuToolbar.addEventListener("DOMMenuBarInactive", this);
    423      menuToolbar.addEventListener("DOMMenuBarActive", this);
    424    }
    425 
    426    if (this.window.gBrowser) {
    427      // On startup, this will be called again by browser-init.js
    428      // once gBrowser has been initialized.
    429      this.addGBrowserListeners();
    430    }
    431 
    432    // If the search service is not initialized yet, the placeholder
    433    // and icon will be updated in delayedStartupInit.
    434    if (
    435      Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") &&
    436      Services.search.isInitialized
    437    ) {
    438      this.searchModeSwitcher.updateSearchIcon();
    439      this._updatePlaceholderFromDefaultEngine();
    440    }
    441 
    442    // Expanding requires a parent toolbar, and us not being read-only.
    443    this.#allowBreakout = !!this.closest("toolbar");
    444    if (this.#allowBreakout) {
    445      // TODO(emilio): This could use CSS anchor positioning rather than this
    446      // ResizeObserver, eventually.
    447      this._resizeObserver = new this.window.ResizeObserver(([entry]) => {
    448        this.style.setProperty(
    449          "--urlbar-width",
    450          px(entry.borderBoxSize[0].inlineSize)
    451        );
    452      });
    453      this._resizeObserver.observe(this.parentNode);
    454    }
    455 
    456    this.#updateLayoutBreakout();
    457 
    458    this._addObservers();
    459  }
    460 
    461  disconnectedCallback() {
    462    if (
    463      this.getAttribute("sap-name") == "searchbar" &&
    464      !lazy.UrlbarPrefs.get("browser.search.widget.new")
    465    ) {
    466      return;
    467    }
    468 
    469    this.#uninit();
    470  }
    471 
    472  #uninit() {
    473    if (this.sapName == "searchbar") {
    474      this.parentNode.removeAttribute("overflows");
    475    }
    476 
    477    if (this._copyCutController) {
    478      this.inputField.controllers.removeController(this._copyCutController);
    479      delete this._copyCutController;
    480    }
    481 
    482    for (let event of SmartbarInput.#inputFieldEvents) {
    483      this.inputField.removeEventListener(event, this);
    484    }
    485 
    486    // These are on the window to detect focusing shortcuts like F6.
    487    this.window.removeEventListener("keydown", this);
    488    this.window.removeEventListener("keyup", this);
    489 
    490    this.window.removeEventListener("mousedown", this);
    491    if (AppConstants.platform == "win") {
    492      this.window.removeEventListener("draggableregionleftmousedown", this);
    493    }
    494    this.removeEventListener("mousedown", this);
    495 
    496    // This listener handles clicks from our children too, included the search mode
    497    // indicator close button.
    498    this._inputContainer.removeEventListener("click", this);
    499 
    500    // This is used to detect commands launched from the panel, to avoid
    501    // recording abandonment events when the command causes a blur event.
    502    this.view.panel.removeEventListener("command", this, true);
    503 
    504    this.window.removeEventListener("customizationstarting", this);
    505    this.window.removeEventListener("aftercustomization", this);
    506    this.window.removeEventListener("toolbarvisibilitychange", this);
    507    let menuToolbar = this.window.document.getElementById("toolbar-menubar");
    508    if (menuToolbar) {
    509      menuToolbar.removeEventListener("DOMMenuBarInactive", this);
    510      menuToolbar.removeEventListener("DOMMenuBarActive", this);
    511    }
    512    if (this.#gBrowserListenersAdded) {
    513      this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this);
    514      this.window.gBrowser.tabContainer.removeEventListener("TabClose", this);
    515      this.window.gBrowser.removeTabsProgressListener(this);
    516      this.#gBrowserListenersAdded = false;
    517    }
    518 
    519    this._resizeObserver?.disconnect();
    520 
    521    this._removeObservers();
    522  }
    523 
    524  /**
    525   * This method is used to attach new context menu options to the urlbar
    526   * context menu, i.e. the context menu of the moz-input-box.
    527   * It is called when the moz-input-box rebuilds its context menu.
    528   *
    529   * Note that it might be called before #init has finished.
    530   */
    531  #onContextMenuRebuilt() {
    532    this._initStripOnShare();
    533    this._initPasteAndGo();
    534  }
    535 
    536  addGBrowserListeners() {
    537    // The following listeners are only used for the address bar.
    538    if (!this.#isAddressbar) {
    539      return;
    540    }
    541    if (this.window.gBrowser && !this.#gBrowserListenersAdded) {
    542      this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
    543      this.window.gBrowser.tabContainer.addEventListener("TabClose", this);
    544      this.window.gBrowser.addTabsProgressListener(this);
    545      this.#gBrowserListenersAdded = true;
    546    }
    547  }
    548 
    549  #initSmartbarEditor() {
    550    const adapter = createEditor(this.inputField);
    551    this.#smartbarInputController = new lazy.SmartbarInputController(adapter);
    552    this.inputField = adapter.input;
    553    this.#smartbarEditor = adapter.editor;
    554  }
    555 
    556  #ensureSmartbarEditor() {
    557    if (!this.#smartbarInputController) {
    558      this.#initSmartbarEditor();
    559    }
    560    return this.#smartbarEditor;
    561  }
    562 
    563  #setInputValue(val) {
    564    if (this.#smartbarInputController) {
    565      this.#smartbarInputController.setValue(val);
    566    } else {
    567      this.inputField.value = val;
    568    }
    569  }
    570 
    571  #lazy = XPCOMUtils.declareLazy({
    572    valueFormatter: () => new lazy.UrlbarValueFormatter(this),
    573    addSearchEngineHelper: () => new AddSearchEngineHelper(this),
    574  });
    575 
    576  /**
    577   * Manages the Add Search Engine contextual menu entries.
    578   */
    579  get addSearchEngineHelper() {
    580    return this.#lazy.addSearchEngineHelper;
    581  }
    582 
    583  /**
    584   * The search access point name of the SmartbarInput for use with telemetry or
    585   * logging, e.g. `urlbar`, `searchbar`.
    586   */
    587  get sapName() {
    588    return this.#sapName;
    589  }
    590 
    591  get smartbarMode() {
    592    const mode = this.getAttribute("smartbar-mode") || this.#smartbarMode;
    593    return SmartbarInput.#validSmartbarModes.includes(mode)
    594      ? mode
    595      : SmartbarInput.#validSmartbarModes[0];
    596  }
    597 
    598  set smartbarMode(mode) {
    599    if (!SmartbarInput.#validSmartbarModes.includes(mode)) {
    600      return;
    601    }
    602    this.#smartbarMode = mode;
    603    this.setAttribute("smartbar-mode", mode);
    604  }
    605 
    606  blur() {
    607    if (this.#smartbarInputController) {
    608      this.#smartbarInputController.blur();
    609    } else {
    610      this.inputField.blur();
    611    }
    612  }
    613 
    614  /**
    615   * @type {typeof HTMLInputElement.prototype.placeholder}
    616   */
    617  get placeholder() {
    618    return (
    619      this.#smartbarInputController?.placeholder ?? this.inputField?.placeholder
    620    );
    621  }
    622 
    623  set placeholder(val) {
    624    if (this.#smartbarInputController) {
    625      this.#smartbarInputController.placeholder = val;
    626      return;
    627    }
    628    if (this.inputField) {
    629      this.inputField.placeholder = val;
    630    }
    631  }
    632 
    633  /**
    634   * @type {typeof HTMLInputElement.prototype.readOnly}
    635   */
    636  get readOnly() {
    637    return this.#smartbarInputController?.readOnly ?? this.inputField?.readOnly;
    638  }
    639 
    640  set readOnly(val) {
    641    if (this.#smartbarInputController) {
    642      this.#smartbarInputController.readOnly = val;
    643      return;
    644    }
    645    if (this.inputField) {
    646      this.inputField.readOnly = val;
    647    }
    648  }
    649 
    650  /**
    651   * @type {typeof HTMLInputElement.prototype.selectionStart}
    652   */
    653  get selectionStart() {
    654    return (
    655      this.#smartbarInputController?.selectionStart ??
    656      this.inputField?.selectionStart ??
    657      0
    658    );
    659  }
    660 
    661  set selectionStart(val) {
    662    if (this.#smartbarInputController) {
    663      this.#smartbarInputController.selectionStart = val;
    664      return;
    665    }
    666    if (this.inputField) {
    667      this.inputField.selectionStart = val;
    668    }
    669  }
    670 
    671  /**
    672   * @type {typeof HTMLInputElement.prototype.selectionEnd}
    673   */
    674  get selectionEnd() {
    675    return (
    676      this.#smartbarInputController?.selectionEnd ??
    677      this.inputField?.selectionEnd ??
    678      0
    679    );
    680  }
    681 
    682  set selectionEnd(val) {
    683    if (this.#smartbarInputController) {
    684      this.#smartbarInputController.selectionEnd = val;
    685      return;
    686    }
    687    if (this.inputField) {
    688      this.inputField.selectionEnd = val;
    689    }
    690  }
    691 
    692  /**
    693   * Called when a urlbar or urlbar related pref changes.
    694   *
    695   * @param {string} pref
    696   *   The name of the pref. Relative to `browser.urlbar` for urlbar prefs.
    697   */
    698  onPrefChanged(pref) {
    699    switch (pref) {
    700      case "keyword.enabled":
    701        this._updatePlaceholderFromDefaultEngine().catch(e =>
    702          // This can happen if the search service failed.
    703          console.warn("Falied to update urlbar placeholder:", e)
    704        );
    705        break;
    706      case "browser.search.widget.new": {
    707        if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) {
    708          if (lazy.UrlbarPrefs.get("browser.search.widget.new")) {
    709            // The connectedCallback was skipped. Init now.
    710            this.#init();
    711          } else {
    712            // Uninit now, the disconnectedCallback will be skipped.
    713            this.#uninit();
    714          }
    715        }
    716      }
    717    }
    718  }
    719 
    720  /**
    721   * Applies styling to the text in the urlbar input, depending on the text.
    722   */
    723  formatValue() {
    724    // The editor may not exist if the toolbar is not visible.
    725    if (this.#isAddressbar && this.editor) {
    726      this.#lazy.valueFormatter.update();
    727    }
    728  }
    729 
    730  focus() {
    731    let beforeFocus = new CustomEvent("beforefocus", {
    732      bubbles: true,
    733      cancelable: true,
    734    });
    735    this.inputField.dispatchEvent(beforeFocus);
    736    if (beforeFocus.defaultPrevented) {
    737      return;
    738    }
    739 
    740    if (this.#smartbarInputController) {
    741      this.#smartbarInputController.focus();
    742    } else {
    743      this.inputField.focus();
    744    }
    745  }
    746 
    747  select() {
    748    let beforeSelect = new CustomEvent("beforeselect", {
    749      bubbles: true,
    750      cancelable: true,
    751    });
    752    this.inputField.dispatchEvent(beforeSelect);
    753    if (beforeSelect.defaultPrevented) {
    754      return;
    755    }
    756 
    757    // See _on_select().  HTMLInputElement.select() dispatches a "select"
    758    // event but does not set the primary selection.
    759    this._suppressPrimaryAdjustment = true;
    760    if (this.#smartbarInputController) {
    761      this.#smartbarInputController?.select();
    762    } else {
    763      this.inputField.select();
    764    }
    765    this._suppressPrimaryAdjustment = false;
    766  }
    767 
    768  setSelectionRange(selectionStart, selectionEnd) {
    769    let beforeSelect = new CustomEvent("beforeselect", {
    770      bubbles: true,
    771      cancelable: true,
    772    });
    773    this.inputField.dispatchEvent(beforeSelect);
    774    if (beforeSelect.defaultPrevented) {
    775      return;
    776    }
    777 
    778    // See _on_select().  HTMLInputElement.select() dispatches a "select"
    779    // event but does not set the primary selection.
    780    this._suppressPrimaryAdjustment = true;
    781    if (this.#smartbarInputController) {
    782      this.#smartbarInputController.setSelectionRange(
    783        selectionStart,
    784        selectionEnd
    785      );
    786    } else {
    787      this.inputField.setSelectionRange(selectionStart, selectionEnd);
    788    }
    789    this._suppressPrimaryAdjustment = false;
    790  }
    791 
    792  saveSelectionStateForBrowser(browser) {
    793    let state = this.getBrowserState(browser);
    794    state.selection = {
    795      // When the value is empty, we're either on a blank page, or the whole
    796      // text has been edited away. In the latter case we'll restore value to
    797      // the current URI, and we want to fully select it.
    798      start: this.value ? this.selectionStart : 0,
    799      end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER,
    800      // When restoring a URI from an empty value, we don't want to untrim it.
    801      shouldUntrim: this.value && !this._protocolIsTrimmed,
    802    };
    803  }
    804 
    805  restoreSelectionStateForBrowser(browser) {
    806    // Address bar must be focused to untrim and for selection to make sense.
    807    this.focus();
    808    let state = this.getBrowserState(browser);
    809    if (state.selection) {
    810      if (state.selection.shouldUntrim) {
    811        this.#maybeUntrimUrl();
    812      }
    813      this.setSelectionRange(
    814        state.selection.start,
    815        // When selecting all the end value may be larger than the actual value.
    816        Math.min(state.selection.end, this.value.length)
    817      );
    818    }
    819  }
    820 
    821  /**
    822   * Sets the URI to display in the location bar.
    823   *
    824   * @param {object} [options]
    825   * @param {?nsIURI} [options.uri]
    826   *        If this is unspecified, the current URI will be used.
    827   * @param {boolean} [options.dueToTabSwitch=false]
    828   *        Whether this is being called due to switching tabs.
    829   * @param {boolean} [options.dueToSessionRestore=false]
    830   *        Whether this is being called due to session restore.
    831   * @param {boolean} [options.hideSearchTerms=false]
    832   *        True if userTypedValue should not be overidden by search terms
    833   *        and false otherwise.
    834   * @param {boolean} [options.isSameDocument=false]
    835   *        Whether the caller loaded a new document or not (e.g. location
    836   *        change from an anchor scroll or a pushState event).
    837   */
    838  setURI({
    839    uri = null,
    840    dueToTabSwitch = false,
    841    dueToSessionRestore = false,
    842    hideSearchTerms = false,
    843    isSameDocument = false,
    844  } = {}) {
    845    if (!this.#isAddressbar) {
    846      throw new Error(
    847        "Cannot set URI for SmartbarInput that is not an address bar"
    848      );
    849    }
    850    // We only need to update the searchModeUI on tab switch conditionally
    851    // as we only persist searchMode with ScotchBonnet enabled.
    852    if (
    853      dueToTabSwitch &&
    854      lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode")
    855    ) {
    856      this._updateSearchModeUI(this.searchMode);
    857    }
    858 
    859    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
    860    this.#handlePersistedSearchTerms({
    861      state,
    862      uri,
    863      dueToTabSwitch,
    864      hideSearchTerms,
    865      isSameDocument,
    866    });
    867 
    868    let value = this.userTypedValue;
    869    let valid = false;
    870    let isReverting = !uri;
    871 
    872    // If `value` is null or if it's an empty string and we're switching tabs
    873    // set value to the browser's current URI. When a user empties the input,
    874    // switches tabs, and switches back, we want the URI to become visible again
    875    // so the user knows what URI they're viewing.
    876    // An exception to this is made in case of an auth request from a different
    877    // base domain. To avoid auth prompt spoofing we already display the url of
    878    // the cross domain resource, although the page is not loaded yet.
    879    // This url will be set/unset by PromptParent. See bug 791594 for reference.
    880    if (value === null || (!value && dueToTabSwitch)) {
    881      uri =
    882        this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
    883        uri ||
    884        this.#isOpenedPageInBlankTargetLoading ||
    885        this.window.gBrowser.currentURI;
    886      // Strip off usernames and passwords for the location bar
    887      try {
    888        uri = Services.io.createExposableURI(uri);
    889      } catch (e) {}
    890 
    891      let isInitialPageControlledByWebContent = false;
    892 
    893      // Replace initial page URIs with an empty string
    894      // only if there's no opener (bug 370555).
    895      if (
    896        this.window.isInitialPage(uri) &&
    897        lazy.BrowserUIUtils.checkEmptyPageOrigin(
    898          this.window.gBrowser.selectedBrowser,
    899          uri
    900        )
    901      ) {
    902        value = "";
    903      } else {
    904        isInitialPageControlledByWebContent = true;
    905 
    906        // We should deal with losslessDecodeURI throwing for exotic URIs
    907        try {
    908          value = losslessDecodeURI(uri);
    909        } catch (ex) {
    910          value = "about:blank";
    911        }
    912      }
    913      // If we update the URI while restoring a session, set the proxyState to
    914      // invalid, because we don't have a valid security state to show via site
    915      // identity yet. See Bug 1746383.
    916      valid =
    917        !dueToSessionRestore &&
    918        (!this.window.isBlankPageURL(uri.spec) ||
    919          lazy.ExtensionUtils.isExtensionUrl(uri) ||
    920          isInitialPageControlledByWebContent);
    921    } else if (
    922      this.window.isInitialPage(value) &&
    923      lazy.BrowserUIUtils.checkEmptyPageOrigin(
    924        this.window.gBrowser.selectedBrowser
    925      )
    926    ) {
    927      value = "";
    928      valid = true;
    929    }
    930 
    931    const previousUntrimmedValue = this.untrimmedValue;
    932    // When calculating the selection indices we must take into account a
    933    // trimmed protocol.
    934    let offset = this._protocolIsTrimmed
    935      ? lazy.BrowserUIUtils.trimURLProtocol.length
    936      : 0;
    937    const previousSelectionStart = this.selectionStart + offset;
    938    const previousSelectionEnd = this.selectionEnd + offset;
    939 
    940    this._setValue(value, { allowTrim: true, valueIsTyped: !valid });
    941    this.toggleAttribute("usertyping", !valid && value);
    942 
    943    if (this.focused && value != previousUntrimmedValue) {
    944      if (
    945        previousSelectionStart != previousSelectionEnd &&
    946        value.substring(previousSelectionStart, previousSelectionEnd) ===
    947          previousUntrimmedValue.substring(
    948            previousSelectionStart,
    949            previousSelectionEnd
    950          )
    951      ) {
    952        // If the same text is in the same place as the previously selected text,
    953        // the selection is kept.
    954        this.setSelectionRange(
    955          previousSelectionStart - offset,
    956          previousSelectionEnd - offset
    957        );
    958      } else if (
    959        previousSelectionEnd &&
    960        (previousUntrimmedValue.length === previousSelectionEnd ||
    961          value.length <= previousSelectionEnd)
    962      ) {
    963        // If the previous end caret is not 0 and the caret is at the end of the
    964        // input or its position is beyond the end of the new value, keep the
    965        // position at the end.
    966        this.setSelectionRange(value.length, value.length);
    967      } else {
    968        // Otherwise clear selection and set the caret position to the previous
    969        // caret end position.
    970        this.setSelectionRange(
    971          previousSelectionEnd - offset,
    972          previousSelectionEnd - offset
    973        );
    974      }
    975    }
    976 
    977    // The proxystate must be set before setting search mode below because
    978    // search mode depends on it.
    979    this.setPageProxyState(
    980      valid ? "valid" : "invalid",
    981      dueToTabSwitch,
    982      !isReverting &&
    983        dueToTabSwitch &&
    984        this.getBrowserState(this.window.gBrowser.selectedBrowser)
    985          .isUnifiedSearchButtonAvailable
    986    );
    987 
    988    if (
    989      state.persist?.shouldPersist &&
    990      !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState(
    991        this.searchMode,
    992        state
    993      )
    994    ) {
    995      // When search terms persist, on non-default engine search result pages
    996      // the address bar should show the same search mode. For default engines,
    997      // search mode should not persist.
    998      if (state.persist.isDefaultEngine) {
    999        this.searchMode = null;
   1000      } else {
   1001        this.searchMode = {
   1002          engineName: state.persist.originalEngineName,
   1003          source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
   1004          isPreview: false,
   1005        };
   1006      }
   1007    } else if (dueToTabSwitch && !valid) {
   1008      // If we're switching tabs, restore the tab's search mode.
   1009      this.restoreSearchModeState();
   1010    } else if (valid) {
   1011      // If the URI is valid, exit search mode.  This must happen
   1012      // after setting proxystate above because search mode depends on it.
   1013      this.searchMode = null;
   1014    }
   1015 
   1016    // Dispatch URIUpdate event to synchronize the tab status when switching.
   1017    let event = new CustomEvent("SetURI", { bubbles: true });
   1018    this.inputField.dispatchEvent(event);
   1019  }
   1020 
   1021  /**
   1022   * Converts an internal URI (e.g. a URI with a username or password) into one
   1023   * which we can expose to the user.
   1024   *
   1025   * @param {nsIURI} uri
   1026   *   The URI to be converted
   1027   * @returns {nsIURI}
   1028   *   The converted, exposable URI
   1029   */
   1030  makeURIReadable(uri) {
   1031    // Avoid copying 'about:reader?url=', and always provide the original URI:
   1032    // Reader mode ensures we call createExposableURI itself.
   1033    let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay(
   1034      uri.displaySpec
   1035    );
   1036    if (readerStrippedURI) {
   1037      return readerStrippedURI;
   1038    }
   1039 
   1040    try {
   1041      return Services.io.createExposableURI(uri);
   1042    } catch (ex) {}
   1043 
   1044    return uri;
   1045  }
   1046 
   1047  /**
   1048   * Function for tabs progress listener.
   1049   *
   1050   * @param {nsIBrowser} browser
   1051   * @param {nsIWebProgress} webProgress
   1052   *   The nsIWebProgress instance that fired the notification.
   1053   * @param {nsIRequest} request
   1054   *   The associated nsIRequest.  This may be null in some cases.
   1055   * @param {nsIURI} locationURI
   1056   *   The URI of the location that is being loaded.
   1057   */
   1058  onLocationChange(browser, webProgress, request, locationURI) {
   1059    if (!webProgress.isTopLevel) {
   1060      return;
   1061    }
   1062 
   1063    if (
   1064      browser != this.window.gBrowser.selectedBrowser &&
   1065      !this.window.isBlankPageURL(locationURI.spec)
   1066    ) {
   1067      // If the page is loaded on background tab, make Unified Search Button
   1068      // unavailable when back to the tab.
   1069      this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false;
   1070    }
   1071 
   1072    // Using browser navigation buttons should potentially trigger a bounce
   1073    // telemetry event.
   1074    if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
   1075      this.controller.engagementEvent.handleBounceEventTrigger(browser);
   1076    }
   1077  }
   1078 
   1079  /**
   1080   * Passes DOM events to the _on_<event type> methods.
   1081   *
   1082   * @param {Event} event The event to handle.
   1083   */
   1084  handleEvent(event) {
   1085    let methodName = "_on_" + event.type;
   1086    if (methodName in this) {
   1087      try {
   1088        this[methodName](event);
   1089      } catch (e) {
   1090        console.error(`Error calling SmartbarInput::${methodName}:`, e);
   1091      }
   1092    } else {
   1093      throw new Error("Unrecognized SmartbarInput event: " + event.type);
   1094    }
   1095  }
   1096 
   1097  /**
   1098   * Handles an event which might open text or a URL. If the event requires
   1099   * doing so, handleCommand forwards it to handleNavigation.
   1100   *
   1101   * @param {Event} [event] The event triggering the open.
   1102   */
   1103  handleCommand(event = null) {
   1104    let isMouseEvent = MouseEvent.isInstance(event);
   1105    if (isMouseEvent && event.button == 2) {
   1106      // Do nothing for right clicks.
   1107      return;
   1108    }
   1109 
   1110    // Determine whether to use the selected one-off search button.  In
   1111    // one-off search buttons parlance, "selected" means that the button
   1112    // has been navigated to via the keyboard.  So we want to use it if
   1113    // the triggering event is not a mouse click -- i.e., it's a Return
   1114    // key -- or if the one-off was mouse-clicked.
   1115    if (this.view.isOpen) {
   1116      let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton;
   1117      if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
   1118        this.view.oneOffSearchButtons.handleSearchCommand(event, {
   1119          engineName: selectedOneOff.engine?.name,
   1120          source: selectedOneOff.source,
   1121          entry: "oneoff",
   1122        });
   1123        return;
   1124      }
   1125    }
   1126 
   1127    this.handleNavigation({ event });
   1128  }
   1129 
   1130  /**
   1131   * @typedef {object} HandleNavigationOneOffParams
   1132   *
   1133   * @property {string} openWhere
   1134   *   Where we expect the result to be opened.
   1135   * @property {object} openParams
   1136   *   The parameters related to where the result will be opened.
   1137   * @property {nsISearchEngine} engine
   1138   *   The selected one-off's engine.
   1139   */
   1140 
   1141  /**
   1142   * Handles an event which would cause a URL or text to be opened.
   1143   *
   1144   * @param {object} options
   1145   *   Options for the navigation.
   1146   * @param {Event} [options.event]
   1147   *   The event triggering the open.
   1148   * @param {HandleNavigationOneOffParams} [options.oneOffParams]
   1149   *   Optional. Pass if this navigation was triggered by a one-off. Practically
   1150   *   speaking, UrlbarSearchOneOffs passes this when the user holds certain key
   1151   *   modifiers while picking a one-off. In those cases, we do an immediate
   1152   *   search using the one-off's engine instead of entering search mode.
   1153   * @param {object} [options.triggeringPrincipal]
   1154   *   The principal that the action was triggered from.
   1155   */
   1156  handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
   1157    if (this.#isSmartbarMode) {
   1158      const committedValue = this.untrimmedValue;
   1159      const mode = this.smartbarMode;
   1160      lazy.logger.debug(`commit (${mode}): ${committedValue}`);
   1161      this.#clearSmartbarInput();
   1162      this.dispatchEvent(
   1163        new CustomEvent("smartbar-commit", {
   1164          detail: { value: committedValue, event, mode },
   1165        })
   1166      );
   1167      if (!this.window.gBrowser) {
   1168        return;
   1169      }
   1170      // Fall through to default navigation behaviour.
   1171    }
   1172    let element = this.view.selectedElement;
   1173    let result = this.view.getResultFromElement(element);
   1174    let openParams = oneOffParams?.openParams || { triggeringPrincipal };
   1175 
   1176    // If the value was submitted during composition, the result may not have
   1177    // been updated yet, because the input event happens after composition end.
   1178    // We can't trust element nor _resultForCurrentValue targets in that case,
   1179    // so we always generate a new heuristic to load.
   1180    let isComposing = this.editor.composing;
   1181 
   1182    // Use the selected element if we have one; this is usually the case
   1183    // when the view is open.
   1184    let selectedPrivateResult =
   1185      result &&
   1186      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   1187      result.payload.inPrivateWindow;
   1188    let selectedPrivateEngineResult =
   1189      selectedPrivateResult && result.payload.isPrivateEngine;
   1190    // Whether the user has been editing the value in the URL bar after selecting
   1191    // the result. However, if the result type is tip, pick as it is. The result
   1192    // heuristic is also kept the behavior as is for safety.
   1193    let safeToPickResult =
   1194      result &&
   1195      (result.heuristic ||
   1196        !this.valueIsTyped ||
   1197        result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP ||
   1198        this.value == this.#getValueFromResult(result));
   1199    if (
   1200      !isComposing &&
   1201      element &&
   1202      (!oneOffParams?.engine || selectedPrivateEngineResult) &&
   1203      safeToPickResult
   1204    ) {
   1205      this.pickElement(element, event);
   1206      return;
   1207    }
   1208 
   1209    // Use the hidden heuristic if it exists and there's no selection.
   1210    if (
   1211      lazy.UrlbarPrefs.get("experimental.hideHeuristic") &&
   1212      !element &&
   1213      !isComposing &&
   1214      !oneOffParams?.engine &&
   1215      this._resultForCurrentValue?.heuristic
   1216    ) {
   1217      this.pickResult(this._resultForCurrentValue, event);
   1218      return;
   1219    }
   1220 
   1221    // We don't select a heuristic result when we're autofilling a token alias,
   1222    // but we want pressing Enter to behave like the first result was selected.
   1223    if (!result && this.value.startsWith("@")) {
   1224      let tokenAliasResult = this.view.getResultAtIndex(0);
   1225      if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
   1226        this.pickResult(tokenAliasResult, event);
   1227        return;
   1228      }
   1229    }
   1230 
   1231    let url;
   1232    let selType = this.controller.engagementEvent.typeFromElement(
   1233      result,
   1234      element
   1235    );
   1236    let typedValue = this.value;
   1237    if (oneOffParams?.engine) {
   1238      selType = "oneoff";
   1239      typedValue = this._lastSearchString;
   1240      // If there's a selected one-off button then load a search using
   1241      // the button's engine.
   1242      result = this._resultForCurrentValue;
   1243 
   1244      let searchString =
   1245        (result && (result.payload.suggestion || result.payload.query)) ||
   1246        this._lastSearchString;
   1247      [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl(
   1248        oneOffParams.engine,
   1249        searchString
   1250      );
   1251      if (oneOffParams.openWhere == "tab") {
   1252        this.window.gBrowser.tabContainer.addEventListener(
   1253          "TabOpen",
   1254          tabEvent =>
   1255            this._recordSearch(
   1256              oneOffParams.engine,
   1257              event,
   1258              {},
   1259              tabEvent.target.linkedBrowser
   1260            ),
   1261          { once: true }
   1262        );
   1263      } else {
   1264        this._recordSearch(oneOffParams.engine, event);
   1265      }
   1266 
   1267      lazy.UrlbarUtils.addToFormHistory(
   1268        this,
   1269        searchString,
   1270        oneOffParams.engine.name
   1271      ).catch(console.error);
   1272    } else {
   1273      // Use the current value if we don't have a UrlbarResult e.g. because the
   1274      // view is closed.
   1275      url = this.untrimmedValue;
   1276      openParams.postData = null;
   1277    }
   1278 
   1279    if (!url) {
   1280      return;
   1281    }
   1282 
   1283    // When the user hits enter in a local search mode and there's no selected
   1284    // result or one-off, don't do anything.
   1285    if (
   1286      this.searchMode &&
   1287      !this.searchMode.engineName &&
   1288      !result &&
   1289      !oneOffParams
   1290    ) {
   1291      return;
   1292    }
   1293 
   1294    let where = oneOffParams?.openWhere || this._whereToOpen(event);
   1295    if (selectedPrivateResult) {
   1296      where = "window";
   1297      openParams.private = true;
   1298    }
   1299    openParams.allowInheritPrincipal = false;
   1300    url = this._maybeCanonizeURL(event, url) || url.trim();
   1301 
   1302    let selectedResult = result || this.view.selectedResult;
   1303    this.controller.engagementEvent.record(event, {
   1304      element,
   1305      selType,
   1306      searchString: typedValue,
   1307      result: selectedResult || this._resultForCurrentValue || null,
   1308    });
   1309 
   1310    if (URL.canParse(url)) {
   1311      // Annotate if the untrimmed value contained a scheme, to later potentially
   1312      // be upgraded by schemeless HTTPS-First.
   1313      openParams.schemelessInput = this.#getSchemelessInput(
   1314        this.untrimmedValue
   1315      );
   1316      this._loadURL(url, event, where, openParams);
   1317      return;
   1318    }
   1319 
   1320    // This is not a URL and there's no selected element, because likely the
   1321    // view is closed, or paste&go was used.
   1322    // We must act consistently here, having or not an open view should not
   1323    // make a difference if the search string is the same.
   1324 
   1325    // If we have a result for the current value, we can just use it.
   1326    if (!isComposing && this._resultForCurrentValue) {
   1327      this.pickResult(this._resultForCurrentValue, event);
   1328      return;
   1329    }
   1330 
   1331    // Otherwise, we must fetch the heuristic result for the current value.
   1332    // TODO (Bug 1604927): If the urlbar results are restricted to a specific
   1333    // engine, here we must search with that specific engine; indeed the
   1334    // docshell wouldn't know about our engine restriction.
   1335    // Also remember to invoke this._recordSearch, after replacing url with
   1336    // the appropriate engine submission url.
   1337    let browser = this.window.gBrowser.selectedBrowser;
   1338    let lastLocationChange = browser.lastLocationChange;
   1339 
   1340    // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit.
   1341    Glean.urlbar.heuristicResultMissing.addToDenominator(1);
   1342 
   1343    lazy.UrlbarUtils.getHeuristicResultFor(url, this)
   1344      .then(newResult => {
   1345        // Because this happens asynchronously, we must verify that the browser
   1346        // location did not change in the meanwhile.
   1347        if (
   1348          where != "current" ||
   1349          browser.lastLocationChange == lastLocationChange
   1350        ) {
   1351          this.pickResult(newResult, event, null, browser);
   1352        }
   1353      })
   1354      .catch(() => {
   1355        if (url) {
   1356          // Something went wrong, we should always have a heuristic result,
   1357          // otherwise it means we're not able to search at all, maybe because
   1358          // some parts of the profile are corrupt.
   1359          // The urlbar should still allow to search or visit the typed string,
   1360          // so that the user can look for help to resolve the problem.
   1361 
   1362          // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit.
   1363          Glean.urlbar.heuristicResultMissing.addToNumerator(1);
   1364 
   1365          let flags =
   1366            Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   1367            Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   1368          if (this.isPrivate) {
   1369            flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   1370          }
   1371          let {
   1372            preferredURI: uri,
   1373            postData,
   1374            keywordAsSent,
   1375          } = Services.uriFixup.getFixupURIInfo(url, flags);
   1376          if (
   1377            where != "current" ||
   1378            browser.lastLocationChange == lastLocationChange
   1379          ) {
   1380            openParams.postData = postData;
   1381            if (!keywordAsSent) {
   1382              // `uri` is not a search engine url, so we annotate if the untrimmed
   1383              // value contained a scheme, to potentially be later upgraded by
   1384              // schemeless HTTPS-First.
   1385              openParams.schemelessInput = this.#getSchemelessInput(
   1386                this.untrimmedValue
   1387              );
   1388            }
   1389            this._loadURL(uri.spec, event, where, openParams, null, browser);
   1390          }
   1391        }
   1392      });
   1393    // Don't add further handling here, the catch above is our last resort.
   1394  }
   1395 
   1396  handleRevert() {
   1397    this.userTypedValue = null;
   1398    // Nullify search mode before setURI so it won't try to restore it.
   1399    this.searchMode = null;
   1400    if (this.#isAddressbar) {
   1401      this.setURI({
   1402        dueToTabSwitch: true,
   1403        hideSearchTerms: true,
   1404      });
   1405    } else {
   1406      this.value = "";
   1407    }
   1408    if (this.value && this.focused) {
   1409      this.select();
   1410    }
   1411  }
   1412 
   1413  maybeHandleRevertFromPopup(anchorElement) {
   1414    if (!this.#isAddressbar) {
   1415      return;
   1416    }
   1417    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   1418    if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) {
   1419      this.handleRevert();
   1420      Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1);
   1421    }
   1422  }
   1423 
   1424  /**
   1425   * Called by inputs that resemble search boxes, but actually hand input off
   1426   * to the Urlbar. We use these fake inputs on the new tab page and
   1427   * about:privatebrowsing.
   1428   *
   1429   * @param {string} searchString
   1430   *   The search string to use.
   1431   * @param {nsISearchEngine} [searchEngine]
   1432   *   Optional. If included and the right prefs are set, we will enter search
   1433   *   mode when handing `searchString` from the fake input to the Urlbar.
   1434   * @param {string} [newtabSessionId]
   1435   *   Optional. The id of the newtab session that handed off this search.
   1436   */
   1437  handoff(searchString, searchEngine, newtabSessionId) {
   1438    this._isHandoffSession = true;
   1439    this._handoffSession = newtabSessionId;
   1440    if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) {
   1441      this.search(searchString, {
   1442        searchEngine,
   1443        searchModeEntry: "handoff",
   1444      });
   1445    } else {
   1446      this.search(searchString);
   1447    }
   1448  }
   1449 
   1450  /**
   1451   * Called when an element of the view is picked.
   1452   *
   1453   * @param {HTMLElement} element The element that was picked.
   1454   * @param {Event} event The event that picked the element.
   1455   */
   1456  pickElement(element, event) {
   1457    let result = this.view.getResultFromElement(element);
   1458    lazy.logger.debug(
   1459      `pickElement ${element} with event ${event?.type}, result: ${result}`
   1460    );
   1461    if (!result) {
   1462      return;
   1463    }
   1464    this.pickResult(result, event, element);
   1465  }
   1466 
   1467  /**
   1468   * Called when a result is picked.
   1469   *
   1470   * @param {UrlbarResult} result The result that was picked.
   1471   * @param {Event} event The event that picked the result.
   1472   * @param {HTMLElement} element the picked view element, if available.
   1473   * @param {object} browser The browser to use for the load.
   1474   */
   1475  // eslint-disable-next-line complexity
   1476  pickResult(
   1477    result,
   1478    event,
   1479    element = null,
   1480    browser = this.window.gBrowser.selectedBrowser
   1481  ) {
   1482    if (element?.classList.contains("urlbarView-button-menu")) {
   1483      this.view.openResultMenu(result, element);
   1484      return;
   1485    }
   1486 
   1487    if (element?.dataset.command) {
   1488      this.#pickMenuResult(result, event, element, browser);
   1489      return;
   1490    }
   1491 
   1492    if (
   1493      result.providerName == lazy.UrlbarProviderGlobalActions.name &&
   1494      this.#providesSearchMode(result)
   1495    ) {
   1496      this.maybeConfirmSearchModeFromResult({
   1497        result,
   1498        checkValue: false,
   1499      });
   1500      return;
   1501    }
   1502 
   1503    // When a one-off is selected, we restyle heuristic results to look like
   1504    // search results. In the unlikely event that they are clicked, instead of
   1505    // picking the results as usual, we confirm search mode, same as if the user
   1506    // had selected them and pressed the enter key. Restyling results in this
   1507    // manner was agreed on as a compromise between consistent UX and
   1508    // engineering effort. See review discussion at bug 1667766.
   1509    if (
   1510      (this.searchMode?.isPreview &&
   1511        result.providerName == lazy.UrlbarProviderGlobalActions.name) ||
   1512      (result.heuristic &&
   1513        this.searchMode?.isPreview &&
   1514        this.view.oneOffSearchButtons?.selectedButton)
   1515    ) {
   1516      this.confirmSearchMode();
   1517      this.search(this.value);
   1518      return;
   1519    }
   1520 
   1521    if (
   1522      result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP &&
   1523      result.payload.type == "dismissalAcknowledgment"
   1524    ) {
   1525      // The user clicked the "Got it" button inside the dismissal
   1526      // acknowledgment tip. Dismiss the tip.
   1527      this.controller.engagementEvent.record(event, {
   1528        result,
   1529        element,
   1530        searchString: this._lastSearchString,
   1531        selType: "dismiss",
   1532      });
   1533      this.view.onQueryResultRemoved(result.rowIndex);
   1534      return;
   1535    }
   1536 
   1537    let resultUrl = element?.dataset.url;
   1538    let originalUntrimmedValue = this.untrimmedValue;
   1539    let isCanonized = this.setValueFromResult({
   1540      result,
   1541      event,
   1542      element,
   1543      urlOverride: resultUrl,
   1544    });
   1545    let where = this._whereToOpen(event);
   1546    let openParams = {
   1547      allowInheritPrincipal: false,
   1548      globalHistoryOptions: {
   1549        triggeringSource: this.#sapName,
   1550        triggeringSearchEngine: result.payload?.engine,
   1551        triggeringSponsoredURL: result.payload?.isSponsored
   1552          ? result.payload.url
   1553          : undefined,
   1554      },
   1555      private: this.isPrivate,
   1556    };
   1557 
   1558    if (resultUrl && where == "current") {
   1559      // Open help links in a new tab.
   1560      where = "tab";
   1561    }
   1562 
   1563    if (!this.#providesSearchMode(result)) {
   1564      this.view.close({ elementPicked: true });
   1565    }
   1566 
   1567    if (isCanonized) {
   1568      this.controller.engagementEvent.record(event, {
   1569        result,
   1570        element,
   1571        selType: "canonized",
   1572        searchString: this._lastSearchString,
   1573      });
   1574      this._loadURL(this._untrimmedValue, event, where, openParams, browser);
   1575      return;
   1576    }
   1577 
   1578    let { url, postData } = resultUrl
   1579      ? { url: resultUrl, postData: null }
   1580      : lazy.UrlbarUtils.getUrlFromResult(result, { element });
   1581    openParams.postData = postData;
   1582 
   1583    switch (result.type) {
   1584      case lazy.UrlbarUtils.RESULT_TYPE.URL: {
   1585        if (result.heuristic) {
   1586          // Bug 1578856: both the provider and the docshell run heuristics to
   1587          // decide how to handle a non-url string, either fixing it to a url, or
   1588          // searching for it.
   1589          // Some preferences can control the docshell behavior, for example
   1590          // if dns_first_for_single_words is true, the docshell looks up the word
   1591          // against the dns server, and either loads it as an url or searches for
   1592          // it, depending on the lookup result. The provider instead will always
   1593          // return a fixed url in this case, because URIFixup is synchronous and
   1594          // can't do a synchronous dns lookup. A possible long term solution
   1595          // would involve sharing the docshell logic with the provider, along
   1596          // with the dns lookup.
   1597          // For now, in this specific case, we'll override the result's url
   1598          // with the input value, and let it pass through to _loadURL(), and
   1599          // finally to the docshell.
   1600          // This also means that in some cases the heuristic result will show a
   1601          // Visit entry, but the docshell will instead execute a search. It's a
   1602          // rare case anyway, most likely to happen for enterprises customizing
   1603          // the urifixup prefs.
   1604          if (
   1605            lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
   1606            lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
   1607          ) {
   1608            url = originalUntrimmedValue;
   1609          }
   1610          // Annotate if the untrimmed value contained a scheme, to later potentially
   1611          // be upgraded by schemeless HTTPS-First.
   1612          openParams.schemelessInput = this.#getSchemelessInput(
   1613            originalUntrimmedValue
   1614          );
   1615        }
   1616        break;
   1617      }
   1618      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: {
   1619        // If this result comes from a bookmark keyword, let it inherit the
   1620        // current document's principal, otherwise bookmarklets would break.
   1621        openParams.allowInheritPrincipal = true;
   1622        break;
   1623      }
   1624      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
   1625        // Behaviour is reversed with SecondaryActions, default behaviour is to navigate
   1626        // and button is provided to switch to tab.
   1627        if (
   1628          this.hasAttribute("action-override") ||
   1629          (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") &&
   1630            element?.dataset.action !== "tabswitch")
   1631        ) {
   1632          where = "current";
   1633          break;
   1634        }
   1635 
   1636        // Keep the searchMode for telemetry since handleRevert sets it to null.
   1637        const searchMode = this.searchMode;
   1638        this.handleRevert();
   1639        let prevTab = this.window.gBrowser.selectedTab;
   1640        let loadOpts = {
   1641          adoptIntoActiveWindow: lazy.UrlbarPrefs.get(
   1642            "switchTabs.adoptIntoActiveWindow"
   1643          ),
   1644        };
   1645 
   1646        // We cache the search string because switching tab may clear it.
   1647        let searchString = this._lastSearchString;
   1648        this.controller.engagementEvent.record(event, {
   1649          result,
   1650          element,
   1651          searchString,
   1652          searchMode,
   1653          selType: this.controller.engagementEvent.typeFromElement(
   1654            result,
   1655            element
   1656          ),
   1657        });
   1658 
   1659        let switched = this.window.switchToTabHavingURI(
   1660          Services.io.newURI(url),
   1661          true,
   1662          loadOpts,
   1663          lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
   1664            lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(
   1665              result.payload.userContextId
   1666            )
   1667            ? result.payload.userContextId
   1668            : null
   1669        );
   1670        if (switched && prevTab.isEmpty) {
   1671          this.window.gBrowser.removeTab(prevTab);
   1672        }
   1673 
   1674        if (switched && !this.isPrivate && !result.heuristic) {
   1675          // We don't await for this, because a rejection should not interrupt
   1676          // the load. Just reportError it.
   1677          lazy.UrlbarUtils.addToInputHistory(url, searchString).catch(
   1678            console.error
   1679          );
   1680        }
   1681 
   1682        // TODO (Bug 1865757): We should not show a "switchtotab" result for
   1683        // tabs that are not currently open. Find out why tabs are not being
   1684        // properly unregistered when they are being closed.
   1685        if (!switched) {
   1686          console.error(`Tried to switch to non-existent tab: ${url}`);
   1687          lazy.UrlbarProviderOpenTabs.unregisterOpenTab(
   1688            url,
   1689            result.payload.userContextId,
   1690            result.payload.tabGroup,
   1691            this.isPrivate
   1692          );
   1693        }
   1694 
   1695        return;
   1696      }
   1697      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
   1698        if (result.payload.providesSearchMode) {
   1699          this.controller.engagementEvent.record(event, {
   1700            result,
   1701            element,
   1702            searchString: this._lastSearchString,
   1703            selType: this.controller.engagementEvent.typeFromElement(
   1704              result,
   1705              element
   1706            ),
   1707          });
   1708          this.maybeConfirmSearchModeFromResult({
   1709            result,
   1710            checkValue: false,
   1711          });
   1712          return;
   1713        }
   1714 
   1715        if (
   1716          !this.searchMode &&
   1717          result.heuristic &&
   1718          // If we asked the DNS earlier, avoid the post-facto check.
   1719          !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
   1720          // TODO (bug 1642623): for now there is no smart heuristic to skip the
   1721          // DNS lookup, so any value above 0 will run it.
   1722          lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
   1723          this.window.gKeywordURIFixup &&
   1724          lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
   1725        ) {
   1726          // When fixing a single word to a search, the docShell would also
   1727          // query the DNS and if resolved ask the user whether they would
   1728          // rather visit that as a host. On a positive answer, it adds the host
   1729          // to the list that we use to make decisions.
   1730          // Because we are directly asking for a search here, bypassing the
   1731          // docShell, we need to do the same ourselves.
   1732          // See also URIFixupChild.sys.mjs and keyword-uri-fixup.
   1733          let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
   1734          if (fixupInfo) {
   1735            this.window.gKeywordURIFixup.check(
   1736              this.window.gBrowser.selectedBrowser,
   1737              fixupInfo
   1738            );
   1739          }
   1740        }
   1741 
   1742        if (result.payload.inPrivateWindow) {
   1743          where = "window";
   1744          openParams.private = true;
   1745        }
   1746 
   1747        const actionDetails = {
   1748          isSuggestion: !!result.payload.suggestion,
   1749          isFormHistory:
   1750            result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY,
   1751          alias: result.payload.keyword,
   1752        };
   1753        const engine = Services.search.getEngineByName(result.payload.engine);
   1754 
   1755        if (where == "tab") {
   1756          // The TabOpen event is fired synchronously so tabEvent.target
   1757          // is guaranteed to be our new search tab.
   1758          this.window.gBrowser.tabContainer.addEventListener(
   1759            "TabOpen",
   1760            tabEvent =>
   1761              this._recordSearch(
   1762                engine,
   1763                event,
   1764                actionDetails,
   1765                tabEvent.target.linkedBrowser
   1766              ),
   1767            { once: true }
   1768          );
   1769        } else {
   1770          this._recordSearch(engine, event, actionDetails);
   1771        }
   1772 
   1773        if (!result.payload.inPrivateWindow) {
   1774          lazy.UrlbarUtils.addToFormHistory(
   1775            this,
   1776            result.payload.suggestion || result.payload.query,
   1777            engine.name
   1778          ).catch(console.error);
   1779        }
   1780        break;
   1781      }
   1782      case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
   1783        if (url) {
   1784          break;
   1785        }
   1786        this.handleRevert();
   1787        this.controller.engagementEvent.record(event, {
   1788          result,
   1789          element,
   1790          selType: "tip",
   1791          searchString: this._lastSearchString,
   1792        });
   1793        return;
   1794      }
   1795      case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
   1796        if (!url) {
   1797          // If we're not loading a URL, the engagement is done. First revert
   1798          // and then record the engagement since providers expect the urlbar to
   1799          // be reverted when they're notified of the engagement, but before
   1800          // reverting, copy the search mode since it's nulled on revert.
   1801          const { searchMode } = this;
   1802          this.handleRevert();
   1803          this.controller.engagementEvent.record(event, {
   1804            result,
   1805            element,
   1806            searchMode,
   1807            searchString: this._lastSearchString,
   1808            selType: this.controller.engagementEvent.typeFromElement(
   1809              result,
   1810              element
   1811            ),
   1812          });
   1813          return;
   1814        }
   1815        break;
   1816      }
   1817      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: {
   1818        this.controller.engagementEvent.record(event, {
   1819          result,
   1820          element,
   1821          selType: "extension",
   1822          searchString: this._lastSearchString,
   1823        });
   1824 
   1825        // The urlbar needs to revert to the loaded url when a command is
   1826        // handled by the extension.
   1827        this.handleRevert();
   1828        // We don't directly handle a load when an Omnibox API result is picked,
   1829        // instead we forward the request to the WebExtension itself, because
   1830        // the value may not even be a url.
   1831        // We pass the keyword and content, that actually is the retrieved value
   1832        // prefixed by the keyword. ExtensionSearchHandler uses this keyword
   1833        // redundancy as a sanity check.
   1834        lazy.ExtensionSearchHandler.handleInputEntered(
   1835          result.payload.keyword,
   1836          result.payload.content,
   1837          where
   1838        );
   1839        return;
   1840      }
   1841      case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: {
   1842        this.handleRevert();
   1843        this.controller.engagementEvent.record(event, {
   1844          result,
   1845          element,
   1846          searchString: this._lastSearchString,
   1847          selType: this.controller.engagementEvent.typeFromElement(
   1848            result,
   1849            element
   1850          ),
   1851        });
   1852        this.maybeConfirmSearchModeFromResult({
   1853          result,
   1854          checkValue: false,
   1855        });
   1856 
   1857        return;
   1858      }
   1859    }
   1860 
   1861    if (!url) {
   1862      throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
   1863    }
   1864 
   1865    // Record input history but only in non-private windows.
   1866    if (!this.isPrivate) {
   1867      let input;
   1868      if (!result.heuristic) {
   1869        input = this._lastSearchString;
   1870      } else if (result.autofill?.type == "adaptive") {
   1871        input = result.autofill.adaptiveHistoryInput;
   1872      }
   1873      // `input` may be an empty string, so do a strict comparison here.
   1874      if (input !== undefined) {
   1875        // We don't await for this, because a rejection should not interrupt
   1876        // the load. Just reportError it.
   1877        lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error);
   1878      }
   1879    }
   1880 
   1881    this.controller.engagementEvent.startTrackingBounceEvent(browser, event, {
   1882      result,
   1883      element,
   1884      searchString: this._lastSearchString,
   1885      selType: this.controller.engagementEvent.typeFromElement(result, element),
   1886      searchSource: this.getSearchSource(event),
   1887    });
   1888 
   1889    this.controller.engagementEvent.record(event, {
   1890      result,
   1891      element,
   1892      searchString: this._lastSearchString,
   1893      selType: this.controller.engagementEvent.typeFromElement(result, element),
   1894      searchSource: this.getSearchSource(event),
   1895    });
   1896 
   1897    if (result.payload.sendAttributionRequest) {
   1898      lazy.PartnerLinkAttribution.makeRequest({
   1899        targetURL: result.payload.url,
   1900        source: this.#sapName,
   1901        campaignID: Services.prefs.getStringPref(
   1902          "browser.partnerlink.campaign.topsites"
   1903        ),
   1904      });
   1905      if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") {
   1906        // The position is 1-based for telemetry
   1907        const position = result.rowIndex + 1;
   1908        Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1);
   1909      }
   1910    }
   1911 
   1912    this._loadURL(
   1913      url,
   1914      event,
   1915      where,
   1916      openParams,
   1917      {
   1918        source: result.source,
   1919        type: result.type,
   1920        searchTerm: result.payload.suggestion ?? result.payload.query,
   1921      },
   1922      browser
   1923    );
   1924  }
   1925 
   1926  #clearSmartbarInput() {
   1927    this.value = "";
   1928    this.userTypedValue = "";
   1929    this._lastSearchString = "";
   1930    this._autofillPlaceholder = null;
   1931    this._resultForCurrentValue = null;
   1932    this.setSelectionRange(0, 0);
   1933  }
   1934 
   1935  /**
   1936   * Called by the view when moving through results with the keyboard, and when
   1937   * picking a result.  This sets the input value to the value of the result and
   1938   * invalidates the pageproxystate.  It also sets the result that is associated
   1939   * with the current input value.  If you need to set this result but don't
   1940   * want to also set the input value, then use setResultForCurrentValue.
   1941   *
   1942   * @param {object} options
   1943   *   Options.
   1944   * @param {UrlbarResult} [options.result]
   1945   *   The result that was selected or picked, null if no result was selected.
   1946   * @param {Event} [options.event]
   1947   *   The event that picked the result.
   1948   * @param {string} [options.urlOverride]
   1949   *   Normally the URL is taken from `result.payload.url`, but if `urlOverride`
   1950   *   is specified, it's used instead. See `#getValueFromResult()`.
   1951   * @param {Element} [options.element]
   1952   *   The element that was selected or picked, if available. For results that
   1953   *   have multiple selectable children, the value may be taken from a child
   1954   *   element rather than the result. See `#getValueFromResult()`.
   1955   * @returns {boolean}
   1956   *   Whether the value has been canonized
   1957   */
   1958  setValueFromResult({
   1959    result = null,
   1960    event = null,
   1961    urlOverride = null,
   1962    element = null,
   1963  } = {}) {
   1964    // Usually this is set by a previous input event, but in certain cases, like
   1965    // when opening Top Sites on a loaded page, it wouldn't happen. To avoid
   1966    // confusing the user, we always enforce it when a result changes our value.
   1967    this.setPageProxyState("invalid", true);
   1968 
   1969    // A previous result may have previewed search mode. If we don't expect that
   1970    // we might stay in a search mode of some kind, exit it now.
   1971    if (
   1972      this.searchMode?.isPreview &&
   1973      !this.#providesSearchMode(result) &&
   1974      !this.view.oneOffSearchButtons?.selectedButton
   1975    ) {
   1976      this.searchMode = null;
   1977    }
   1978 
   1979    if (!result) {
   1980      // This happens when there's no selection, for example when moving to the
   1981      // one-offs search settings button, or to the input field when Top Sites
   1982      // are shown; then we must reset the input value.
   1983      // Note that for Top Sites the last search string would be empty, thus we
   1984      // must restore the last text value.
   1985      // Note that unselected autofill results will still arrive in this
   1986      // function with a non-null `result`. They are handled below.
   1987      this.value = this._lastSearchString || this._valueOnLastSearch;
   1988      this.setResultForCurrentValue(result);
   1989      return false;
   1990    }
   1991 
   1992    // We won't allow trimming when calling _setValue, since it makes too easy
   1993    // for the user to wrongly transform `https` into `http`, for example by
   1994    // picking a https://site/path_1 result and editing the path to path_2,
   1995    // then we'd end up visiting http://site/path_2.
   1996    // Trimming `http` would be ok, but there's other cases where it's unsafe,
   1997    // like transforming a url into a search.
   1998    // This choice also makes it easier to copy the full url of a result.
   1999 
   2000    // We are supporting canonization of any result, in particular this allows
   2001    // for single word search suggestions to be converted to a .com URL.
   2002    // For autofilled results, the value to canonize is the user typed string,
   2003    // not the autofilled value.
   2004    let canonizedUrl = this._maybeCanonizeURL(
   2005      event,
   2006      result.autofill ? this._lastSearchString : this.value
   2007    );
   2008    if (canonizedUrl) {
   2009      this._setValue(canonizedUrl);
   2010 
   2011      this.setResultForCurrentValue(result);
   2012      return true;
   2013    }
   2014 
   2015    if (result.autofill) {
   2016      this._autofillValue(result.autofill);
   2017    }
   2018 
   2019    if (this.#providesSearchMode(result)) {
   2020      let enteredSearchMode;
   2021      // Only preview search mode if the result is selected.
   2022      if (this.view.resultIsSelected(result)) {
   2023        // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used
   2024        // for selection of the urlbar results, keep the search mode as preview
   2025        // mode if there are multiple results.
   2026        // If ScotchBonnet is disabled, not starting a query means we will only
   2027        // preview search mode.
   2028        enteredSearchMode = this.maybeConfirmSearchModeFromResult({
   2029          result,
   2030          checkValue: false,
   2031          startQuery:
   2032            lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") &&
   2033            this.view.visibleResults.length == 1,
   2034        });
   2035      }
   2036      if (!enteredSearchMode) {
   2037        this._setValue(this.#getValueFromResult(result), {
   2038          actionType: this.#getActionTypeFromResult(result),
   2039        });
   2040        this.searchMode = null;
   2041      }
   2042      this.setResultForCurrentValue(result);
   2043      return false;
   2044    }
   2045 
   2046    if (!result.autofill) {
   2047      let value = this.#getValueFromResult(result, { urlOverride, element });
   2048      this._setValue(value, {
   2049        actionType: this.#getActionTypeFromResult(result),
   2050      });
   2051    }
   2052 
   2053    this.setResultForCurrentValue(result);
   2054 
   2055    // Update placeholder selection and value to the current selected result to
   2056    // prevent the on_selectionchange event to detect a "accent-character"
   2057    // insertion.
   2058    if (!result.autofill && this._autofillPlaceholder) {
   2059      this._autofillPlaceholder.value = this.value;
   2060      this._autofillPlaceholder.selectionStart = this.value.length;
   2061      this._autofillPlaceholder.selectionEnd = this.value.length;
   2062    }
   2063    return false;
   2064  }
   2065 
   2066  /**
   2067   * The input keeps track of the result associated with the current input
   2068   * value.  This result can be set by calling either setValueFromResult or this
   2069   * method.  Use this method when you need to set the result without also
   2070   * setting the input value.  This can be the case when either the selection is
   2071   * cleared and no other result becomes selected, or when the result is the
   2072   * heuristic and we don't want to modify the value the user is typing.
   2073   *
   2074   * @param {UrlbarResult} result
   2075   *   The result to associate with the current input value.
   2076   */
   2077  setResultForCurrentValue(result) {
   2078    this._resultForCurrentValue = result;
   2079  }
   2080 
   2081  /**
   2082   * Called by the controller when the first result of a new search is received.
   2083   * If it's an autofill result, then it may need to be autofilled, subject to a
   2084   * few restrictions.
   2085   *
   2086   * @param {UrlbarResult} result
   2087   *   The first result.
   2088   */
   2089  _autofillFirstResult(result) {
   2090    if (!result.autofill) {
   2091      return;
   2092    }
   2093 
   2094    let isPlaceholderSelected =
   2095      this._autofillPlaceholder &&
   2096      this.selectionEnd == this._autofillPlaceholder.value.length &&
   2097      this.selectionStart == this._lastSearchString.length &&
   2098      this._autofillPlaceholder.value
   2099        .toLocaleLowerCase()
   2100        .startsWith(this._lastSearchString.toLocaleLowerCase());
   2101 
   2102    // Don't autofill if there's already a selection (with one caveat described
   2103    // next) or the cursor isn't at the end of the input.  But if there is a
   2104    // selection and it's the autofill placeholder value, then do autofill.
   2105    if (
   2106      !isPlaceholderSelected &&
   2107      !this._autofillIgnoresSelection &&
   2108      (this.selectionStart != this.selectionEnd ||
   2109        this.selectionEnd != this._lastSearchString.length)
   2110    ) {
   2111      return;
   2112    }
   2113 
   2114    this.setValueFromResult({ result });
   2115  }
   2116  /**
   2117   * Clears displayed autofill values and unsets the autofill placeholder.
   2118   */
   2119  #clearAutofill() {
   2120    if (!this._autofillPlaceholder) {
   2121      return;
   2122    }
   2123    let currentSelectionStart = this.selectionStart;
   2124    let currentSelectionEnd = this.selectionEnd;
   2125 
   2126    // Overriding this value clears the selection.
   2127    this.#setInputValue(
   2128      this.value.substring(0, this._autofillPlaceholder.selectionStart)
   2129    );
   2130    this._autofillPlaceholder = null;
   2131    // Restore selection
   2132    this.setSelectionRange(currentSelectionStart, currentSelectionEnd);
   2133  }
   2134 
   2135  /**
   2136   * Invoked by the controller when the first result is received.
   2137   *
   2138   * @param {UrlbarResult} firstResult
   2139   *   The first result received.
   2140   * @returns {boolean}
   2141   *   True if this method canceled the query and started a new one.  False
   2142   *   otherwise.
   2143   */
   2144  onFirstResult(firstResult) {
   2145    // If the heuristic result has a keyword but isn't a keyword offer, we may
   2146    // need to enter search mode.
   2147    if (
   2148      firstResult.heuristic &&
   2149      firstResult.payload.keyword &&
   2150      !this.#providesSearchMode(firstResult) &&
   2151      this.maybeConfirmSearchModeFromResult({
   2152        result: firstResult,
   2153        entry: "typed",
   2154        checkValue: false,
   2155      })
   2156    ) {
   2157      return true;
   2158    }
   2159 
   2160    // To prevent selection flickering, we apply autofill on input through a
   2161    // placeholder, without waiting for results. But, if the first result is
   2162    // not an autofill one, the autofill prediction was wrong and we should
   2163    // restore the original user typed string.
   2164    if (firstResult.autofill) {
   2165      this._autofillFirstResult(firstResult);
   2166    } else if (
   2167      this._autofillPlaceholder &&
   2168      // Avoid clobbering added spaces (for token aliases, for example).
   2169      !this.value.endsWith(" ")
   2170    ) {
   2171      this._autofillPlaceholder = null;
   2172      this._setValue(this.userTypedValue);
   2173    }
   2174 
   2175    return false;
   2176  }
   2177 
   2178  /**
   2179   * Starts a query based on the current input value.
   2180   *
   2181   * @param {object} [options]
   2182   *   Object options
   2183   * @param {boolean} [options.allowAutofill]
   2184   *   Whether or not to allow providers to include autofill results.
   2185   * @param {boolean} [options.autofillIgnoresSelection]
   2186   *   Normally we autofill only if the cursor is at the end of the string,
   2187   *   if this is set we'll autofill regardless of selection.
   2188   * @param {string} [options.searchString]
   2189   *   The search string.  If not given, the current input value is used.
   2190   *   Otherwise, the current input value must start with this value.
   2191   * @param {boolean} [options.resetSearchState]
   2192   *   If this is the first search of a user interaction with the input, set
   2193   *   this to true (the default) so that search-related state from the previous
   2194   *   interaction doesn't interfere with the new interaction.  Otherwise set it
   2195   *   to false so that state is maintained during a single interaction.  The
   2196   *   intended use for this parameter is that it should be set to false when
   2197   *   this method is called due to input events.
   2198   * @param {event} [options.event]
   2199   *   The user-generated event that triggered the query, if any.  If given, we
   2200   *   will record engagement event telemetry for the query.
   2201   */
   2202  startQuery({
   2203    allowAutofill,
   2204    autofillIgnoresSelection = false,
   2205    searchString,
   2206    resetSearchState = true,
   2207    event,
   2208  } = {}) {
   2209    if (!searchString) {
   2210      searchString =
   2211        this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
   2212    } else if (!this.value.startsWith(searchString)) {
   2213      throw new Error("The current value doesn't start with the search string");
   2214    }
   2215 
   2216    let queryContext = this.#makeQueryContext({
   2217      allowAutofill,
   2218      event,
   2219      searchString,
   2220    });
   2221 
   2222    if (event) {
   2223      this.controller.engagementEvent.start(event, queryContext, searchString);
   2224    }
   2225 
   2226    if (this._suppressStartQuery) {
   2227      return;
   2228    }
   2229 
   2230    this._autofillIgnoresSelection = autofillIgnoresSelection;
   2231    if (resetSearchState) {
   2232      this._resetSearchState();
   2233    }
   2234 
   2235    if (this.searchMode) {
   2236      this.confirmSearchMode();
   2237    }
   2238 
   2239    this._lastSearchString = searchString;
   2240    this._valueOnLastSearch = this.value;
   2241 
   2242    // TODO (Bug 1522902): This promise is necessary for tests, because some
   2243    // tests are not listening for completion when starting a query through
   2244    // other methods than startQuery (input events for example).
   2245    this.lastQueryContextPromise = this.controller.startQuery(queryContext);
   2246  }
   2247 
   2248  /**
   2249   * Sets the input's value, starts a search, and opens the view.
   2250   *
   2251   * @param {string} value
   2252   *   The input's value will be set to this value, and the search will
   2253   *   use it as its query.
   2254   * @param {object} [options]
   2255   *   Object options
   2256   * @param {nsISearchEngine} [options.searchEngine]
   2257   *   Search engine to use when the search is using a known alias.
   2258   * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
   2259   *   If provided, we will record this parameter as the search mode entry point
   2260   *   in Telemetry. Consumers should provide this if they expect their call
   2261   *   to enter search mode.
   2262   * @param {boolean} [options.focus]
   2263   *   If true, the urlbar will be focused.  If false, the focus will remain
   2264   *   unchanged.
   2265   * @param {boolean} [options.startQuery]
   2266   *   If true, start query to show urlbar result by fireing input event. If
   2267   *   false, not fire the event.
   2268   */
   2269  search(value, options = {}) {
   2270    let { searchEngine, searchModeEntry, startQuery = true } = options;
   2271    if (options.focus ?? true) {
   2272      this.focus();
   2273    }
   2274    let trimmedValue = value.trim();
   2275    let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES);
   2276    let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end);
   2277    // Enter search mode if the string starts with a restriction token.
   2278    let searchMode = this.searchModeForToken(firstToken);
   2279    let firstTokenIsRestriction = !!searchMode;
   2280    if (!searchMode && searchEngine) {
   2281      searchMode = { engineName: searchEngine.name };
   2282      firstTokenIsRestriction = searchEngine.aliases.includes(firstToken);
   2283    }
   2284 
   2285    if (searchMode) {
   2286      searchMode.entry = searchModeEntry;
   2287      this.searchMode = searchMode;
   2288      if (firstTokenIsRestriction) {
   2289        // Remove the restriction token/alias from the string to be searched for
   2290        // in search mode.
   2291        value = value.replace(firstToken, "");
   2292      }
   2293      if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) {
   2294        // If there was a trailing space after the restriction token/alias,
   2295        // remove it.
   2296        value = value.slice(1);
   2297      }
   2298    } else if (
   2299      Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken)
   2300    ) {
   2301      this.searchMode = null;
   2302      // If the entire value is a restricted token, append a space.
   2303      if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) {
   2304        value += " ";
   2305      }
   2306    }
   2307    this.#setInputValue(value);
   2308    // Avoid selecting the text if this method is called twice in a row.
   2309    this.selectionStart = -1;
   2310 
   2311    if (startQuery) {
   2312      // Note: proper IME Composition handling depends on the fact this generates
   2313      // an input event, rather than directly invoking the controller; everything
   2314      // goes through _on_input, that will properly skip the search until the
   2315      // composition is committed. _on_input also skips the search when it's the
   2316      // same as the previous search, but we want to allow consecutive searches
   2317      // with the same string. So clear _lastSearchString first.
   2318      this._lastSearchString = "";
   2319      let event = new UIEvent("input", {
   2320        bubbles: true,
   2321        cancelable: false,
   2322        view: this.window,
   2323        detail: 0,
   2324      });
   2325      this.inputField.dispatchEvent(event);
   2326    }
   2327  }
   2328 
   2329  /**
   2330   * Returns a search mode object if a token should enter search mode when
   2331   * typed. This does not handle engine aliases.
   2332   *
   2333   * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token
   2334   *   A restriction token to convert to search mode.
   2335   * @returns {?object}
   2336   *   A search mode object. Null if search mode should not be entered. See
   2337   *   setSearchMode documentation for details.
   2338   */
   2339  searchModeForToken(token) {
   2340    if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) {
   2341      return {
   2342        engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)
   2343          ?.name,
   2344      };
   2345    }
   2346 
   2347    let mode =
   2348      this.#isAddressbar &&
   2349      lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
   2350    if (mode) {
   2351      // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
   2352      return { ...mode };
   2353    }
   2354 
   2355    return null;
   2356  }
   2357 
   2358  /**
   2359   * Opens a search page if the value is non-empty, otherwise opens the
   2360   * search engine homepage (searchform).
   2361   *
   2362   * @param {string} value
   2363   * @param {object} options
   2364   * @param {nsISearchEngine} options.searchEngine
   2365   */
   2366  openEngineHomePage(value, { searchEngine }) {
   2367    if (!searchEngine) {
   2368      console.warn("No searchEngine parameter");
   2369      return;
   2370    }
   2371 
   2372    let trimmedValue = value.trim();
   2373    let url;
   2374    if (trimmedValue) {
   2375      url = searchEngine.getSubmission(trimmedValue, null).uri.spec;
   2376      // TODO: record SAP telemetry, see Bug 1961789.
   2377    } else {
   2378      url = searchEngine.searchForm;
   2379      lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName);
   2380    }
   2381 
   2382    this._lastSearchString = "";
   2383    if (this.#isAddressbar) {
   2384      this.#setInputValue(url);
   2385    }
   2386    this.selectionStart = -1;
   2387 
   2388    this.window.openTrustedLinkIn(url, "current");
   2389  }
   2390 
   2391  /**
   2392   * Focus without the focus styles.
   2393   * This is used by Activity Stream and about:privatebrowsing for search hand-off.
   2394   */
   2395  setHiddenFocus() {
   2396    this._hideFocus = true;
   2397    if (this.focused) {
   2398      this.removeAttribute("focused");
   2399    } else {
   2400      this.focus();
   2401    }
   2402  }
   2403 
   2404  /**
   2405   * Restore focus styles.
   2406   * This is used by Activity Stream and about:privatebrowsing for search hand-off.
   2407   *
   2408   * @param {boolean} forceSuppressFocusBorder
   2409   *   Set true to suppress-focus-border attribute if this flag is true.
   2410   */
   2411  removeHiddenFocus(forceSuppressFocusBorder = false) {
   2412    this._hideFocus = false;
   2413    if (this.focused) {
   2414      this.toggleAttribute("focused", true);
   2415 
   2416      if (forceSuppressFocusBorder) {
   2417        this.toggleAttribute("suppress-focus-border", true);
   2418      }
   2419    }
   2420  }
   2421 
   2422  /**
   2423   * Addressbar: Gets the search mode for a specific browser instance.
   2424   * Searchbar: Gets the window-global search mode.
   2425   *
   2426   * @param {MozBrowser} browser
   2427   *   The search mode for this browser will be returned.
   2428   * @param {boolean} [confirmedOnly]
   2429   *   Normally, if the browser has both preview and confirmed modes, preview
   2430   *   mode will be returned since it takes precedence.  If this argument is
   2431   *   true, then only confirmed search mode will be returned, or null if
   2432   *   search mode hasn't been confirmed.
   2433   * @returns {?object}
   2434   *   A search mode object or null if the browser/window is not in search mode.
   2435   *   See setSearchMode documentation.
   2436   */
   2437  getSearchMode(browser, confirmedOnly = false) {
   2438    let modes = this.#getSearchModesObject(browser);
   2439 
   2440    // Return copies so that callers don't modify the stored values.
   2441    if (!confirmedOnly && modes.preview) {
   2442      return { ...modes.preview };
   2443    }
   2444    if (modes.confirmed) {
   2445      return { ...modes.confirmed };
   2446    }
   2447    return null;
   2448  }
   2449 
   2450  /**
   2451   * Addressbar: Sets the search mode for a specific browser instance.
   2452   * Searchbar: Sets the window-global search mode.
   2453   * If the given browser is selected, then this will also enter search mode.
   2454   *
   2455   * @param {object} searchMode
   2456   *   A search mode object.
   2457   * @param {string} searchMode.engineName
   2458   *   The name of the search engine to restrict to.
   2459   * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
   2460   *   A result source to restrict to.
   2461   * @param {string} searchMode.entry
   2462   *   How search mode was entered. This is recorded in event telemetry. One of
   2463   *   the values in UrlbarUtils.SEARCH_MODE_ENTRY.
   2464   * @param {boolean} [searchMode.isPreview]
   2465   *   If true, we will preview search mode. Search mode preview does not record
   2466   *   telemetry and has slighly different UI behavior. The preview is exited in
   2467   *   favor of full search mode when a query is executed. False should be
   2468   *   passed if the caller needs to enter search mode but expects it will not
   2469   *   be interacted with right away. Defaults to true.
   2470   * @param {MozBrowser} browser
   2471   *   The browser for which to set search mode.
   2472   *   Pass the selected browser for the searchbar.
   2473   */
   2474  async setSearchMode(searchMode, browser) {
   2475    if (this.#isSmartbarMode) {
   2476      return;
   2477    }
   2478    let currentSearchMode = this.getSearchMode(browser);
   2479    let areSearchModesSame =
   2480      (!currentSearchMode && !searchMode) ||
   2481      lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode);
   2482 
   2483    // Exit search mode if the passed-in engine is invalid or hidden.
   2484    let engine;
   2485    if (searchMode?.engineName) {
   2486      if (!Services.search.isInitialized) {
   2487        await Services.search.init();
   2488      }
   2489      engine = Services.search.getEngineByName(searchMode.engineName);
   2490      if (!engine || engine.hidden) {
   2491        searchMode = null;
   2492      }
   2493    }
   2494 
   2495    let {
   2496      engineName,
   2497      source,
   2498      entry,
   2499      restrictType,
   2500      isPreview = true,
   2501    } = searchMode || {};
   2502 
   2503    searchMode = null;
   2504 
   2505    if (engineName) {
   2506      searchMode = {
   2507        engineName,
   2508        isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
   2509      };
   2510      if (source) {
   2511        searchMode.source = source;
   2512      } else if (searchMode.isGeneralPurposeEngine) {
   2513        // History results for general-purpose search engines are often not
   2514        // useful, so we hide them in search mode. See bug 1658646 for
   2515        // discussion.
   2516        searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
   2517      }
   2518    } else if (source) {
   2519      let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
   2520      if (sourceName) {
   2521        searchMode = { source };
   2522      } else {
   2523        console.error(`Unrecognized source: ${source}`);
   2524      }
   2525    }
   2526 
   2527    let modes = this.#getSearchModesObject(browser);
   2528 
   2529    if (searchMode) {
   2530      searchMode.isPreview = isPreview;
   2531      if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
   2532        searchMode.entry = entry;
   2533      } else {
   2534        // If we see this value showing up in telemetry, we should review
   2535        // search mode's entry points.
   2536        searchMode.entry = "other";
   2537      }
   2538 
   2539      if (!searchMode.isPreview) {
   2540        modes.confirmed = searchMode;
   2541        delete modes.preview;
   2542      } else {
   2543        modes.preview = searchMode;
   2544      }
   2545    } else {
   2546      delete modes.preview;
   2547      delete modes.confirmed;
   2548    }
   2549 
   2550    if (restrictType) {
   2551      searchMode.restrictType = restrictType;
   2552    }
   2553 
   2554    // Enter search mode if the browser is selected.
   2555    if (browser == this.window.gBrowser.selectedBrowser) {
   2556      this._updateSearchModeUI(searchMode);
   2557      if (searchMode) {
   2558        // Set userTypedValue to the query string so that it's properly restored
   2559        // when switching back to the current tab and across sessions.
   2560        this.userTypedValue = this.untrimmedValue;
   2561        this.valueIsTyped = true;
   2562        if (!searchMode.isPreview && !areSearchModesSame) {
   2563          try {
   2564            lazy.BrowserSearchTelemetry.recordSearchMode(searchMode);
   2565          } catch (ex) {
   2566            console.error(ex);
   2567          }
   2568        }
   2569      }
   2570    }
   2571  }
   2572 
   2573  /**
   2574   * @typedef {object} SearchModesObject
   2575   *
   2576   * @property {object} [preview] preview search mode
   2577   * @property {object} [confirmed] confirmed search mode
   2578   */
   2579 
   2580  /**
   2581   * @type {SearchModesObject|undefined}
   2582   *
   2583   * The (lazily initialized) search mode object for the searchbar.
   2584   * This is needed because the searchbar has one search mode per window that
   2585   * shouldn't change when switching tabs. For the address bar, the search mode
   2586   * is stored per browser in #browserStates and this is always undefined.
   2587   */
   2588  #searchbarSearchModes;
   2589 
   2590  /**
   2591   * Addressbar: Gets the search modes object for a specific browser instance.
   2592   * Searchbar: Gets the window-global search modes object.
   2593   *
   2594   * @param {MozBrowser} browser
   2595   *   The browser to get the search modes object for.
   2596   *   Pass the selected browser for the searchbar.
   2597   * @returns {SearchModesObject}
   2598   */
   2599  #getSearchModesObject(browser) {
   2600    if (!this.#isAddressbar) {
   2601      // The passed browser doesn't matter here, but it does in setSearchMode.
   2602      this.#searchbarSearchModes ??= {};
   2603      return this.#searchbarSearchModes;
   2604    }
   2605 
   2606    let state = this.getBrowserState(browser);
   2607    state.searchModes ??= {};
   2608    return state.searchModes;
   2609  }
   2610 
   2611  /**
   2612   * Restores the current browser search mode from a previously stored state.
   2613   */
   2614  restoreSearchModeState() {
   2615    if (this.#isSmartbarMode) {
   2616      return;
   2617    }
   2618    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   2619    this.searchMode = state.searchModes?.confirmed;
   2620  }
   2621 
   2622  /**
   2623   * Enters search mode with the default engine.
   2624   */
   2625  searchModeShortcut() {
   2626    // We restrict to search results when entering search mode from this
   2627    // shortcut to honor historical behaviour.
   2628    this.searchMode = {
   2629      source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
   2630      engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name,
   2631      entry: "shortcut",
   2632    };
   2633    // The searchMode setter clears the input if pageproxystate is valid, so
   2634    // we know at this point this.value will either be blank or the user's
   2635    // typed string.
   2636    this.search(this.value);
   2637    this.select();
   2638  }
   2639 
   2640  /**
   2641   * Confirms the current search mode.
   2642   */
   2643  confirmSearchMode() {
   2644    let searchMode = this.searchMode;
   2645    if (searchMode?.isPreview) {
   2646      searchMode.isPreview = false;
   2647      this.searchMode = searchMode;
   2648 
   2649      // Unselect the one-off search button to ensure UI consistency.
   2650      if (this.view.oneOffSearchButtons) {
   2651        this.view.oneOffSearchButtons.selectedButton = null;
   2652      }
   2653    }
   2654  }
   2655 
   2656  // Getters and Setters below.
   2657 
   2658  get editor() {
   2659    if (this.#isSmartbarMode) {
   2660      return this.#ensureSmartbarEditor();
   2661    }
   2662    return this.inputField.editor;
   2663  }
   2664 
   2665  get focused() {
   2666    return (
   2667      this.document.activeElement ==
   2668      (this.#smartbarInputController?.input ?? this.inputField)
   2669    );
   2670  }
   2671 
   2672  get goButton() {
   2673    return this.querySelector(".urlbar-go-button");
   2674  }
   2675 
   2676  get value() {
   2677    return this.#smartbarInputController?.value ?? this.inputField.value;
   2678  }
   2679 
   2680  set value(val) {
   2681    this._setValue(val, { allowTrim: true });
   2682  }
   2683 
   2684  get untrimmedValue() {
   2685    return this._untrimmedValue;
   2686  }
   2687 
   2688  get userTypedValue() {
   2689    return this.#isAddressbar
   2690      ? this.window.gBrowser.userTypedValue
   2691      : this._userTypedValue;
   2692  }
   2693 
   2694  set userTypedValue(val) {
   2695    if (this.#isAddressbar) {
   2696      this.window.gBrowser.userTypedValue = val;
   2697    } else {
   2698      this._userTypedValue = val;
   2699    }
   2700  }
   2701 
   2702  get lastSearchString() {
   2703    return this._lastSearchString;
   2704  }
   2705 
   2706  get searchMode() {
   2707    if (this.#isSmartbarMode) {
   2708      return null;
   2709    }
   2710    if (!this.window.gBrowser) {
   2711      // This only happens before DOMContentLoaded.
   2712      return null;
   2713    }
   2714    return this.getSearchMode(this.window.gBrowser.selectedBrowser);
   2715  }
   2716 
   2717  set searchMode(searchMode) {
   2718    if (this.#isSmartbarMode) {
   2719      return;
   2720    }
   2721    this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
   2722    this.searchModeSwitcher?.onSearchModeChanged();
   2723    lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window);
   2724  }
   2725 
   2726  getBrowserState(browser) {
   2727    let state = this.#browserStates.get(browser);
   2728    if (!state) {
   2729      state = {};
   2730      this.#browserStates.set(browser, state);
   2731    }
   2732    return state;
   2733  }
   2734 
   2735  async #updateLayoutBreakout() {
   2736    if (!this.#allowBreakout) {
   2737      return;
   2738    }
   2739    if (this.document.fullscreenElement) {
   2740      // Toolbars are hidden in DOM fullscreen mode, so we can't get proper
   2741      // layout information and need to retry after leaving that mode.
   2742      this.window.addEventListener(
   2743        "fullscreen",
   2744        () => {
   2745          this.#updateLayoutBreakout();
   2746        },
   2747        { once: true }
   2748      );
   2749      return;
   2750    }
   2751    await this.#updateLayoutBreakoutDimensions();
   2752  }
   2753 
   2754  startLayoutExtend() {
   2755    if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
   2756      // Do not expand if the Urlbar does not support being expanded or it is
   2757      // already expanded.
   2758      return;
   2759    }
   2760    if (!this.view.isOpen) {
   2761      return;
   2762    }
   2763 
   2764    this.#updateTextboxPosition();
   2765 
   2766    this.setAttribute("breakout-extend", "true");
   2767 
   2768    // Enable the animation only after the first extend call to ensure it
   2769    // doesn't run when opening a new window.
   2770    if (!this.hasAttribute("breakout-extend-animate")) {
   2771      this.window.promiseDocumentFlushed(() => {
   2772        this.window.requestAnimationFrame(() => {
   2773          this.setAttribute("breakout-extend-animate", "true");
   2774        });
   2775      });
   2776    }
   2777  }
   2778 
   2779  endLayoutExtend() {
   2780    // If reduce motion is enabled, we want to collapse the Urlbar here so the
   2781    // user sees only sees two states: not expanded, and expanded with the view
   2782    // open.
   2783    if (!this.hasAttribute("breakout-extend") || this.view.isOpen) {
   2784      return;
   2785    }
   2786 
   2787    this.removeAttribute("breakout-extend");
   2788    this.#updateTextboxPosition();
   2789  }
   2790 
   2791  /**
   2792   * Updates the user interface to indicate whether the URI in the address bar
   2793   * is different than the loaded page, because it's being edited or because a
   2794   * search result is currently selected and is displayed in the location bar.
   2795   *
   2796   * @param {string} state
   2797   *        The string "valid" indicates that the security indicators and other
   2798   *        related user interface elments should be shown because the URI in
   2799   *        the location bar matches the loaded page. The string "invalid"
   2800   *        indicates that the URI in the location bar is different than the
   2801   *        loaded page.
   2802   * @param {boolean} [updatePopupNotifications]
   2803   *        Indicates whether we should update the PopupNotifications
   2804   *        visibility due to this change, otherwise avoid doing so as it is
   2805   *        being handled somewhere else.
   2806   * @param {boolean} [forceUnifiedSearchButtonAvailable]
   2807   *        If this parameter is true, force to make Unified Search Button available.
   2808   *        Otherwise, the availability will be depedent on the proxy state.
   2809   *        Default value is false.
   2810   */
   2811  setPageProxyState(
   2812    state,
   2813    updatePopupNotifications,
   2814    forceUnifiedSearchButtonAvailable = false
   2815  ) {
   2816    if (!this.#isAddressbar) {
   2817      return;
   2818    }
   2819    let prevState = this.getAttribute("pageproxystate");
   2820 
   2821    this.setAttribute("pageproxystate", state);
   2822    this._inputContainer.setAttribute("pageproxystate", state);
   2823    this._identityBox?.setAttribute("pageproxystate", state);
   2824    this.setUnifiedSearchButtonAvailability(
   2825      forceUnifiedSearchButtonAvailable || state == "invalid"
   2826    );
   2827 
   2828    if (state == "valid") {
   2829      this._lastValidURLStr = this.value;
   2830    }
   2831 
   2832    if (
   2833      updatePopupNotifications &&
   2834      prevState != state &&
   2835      this.window.UpdatePopupNotificationsVisibility
   2836    ) {
   2837      this.window.UpdatePopupNotificationsVisibility();
   2838    }
   2839  }
   2840 
   2841  /**
   2842   * When switching tabs quickly, TabSelect sometimes happens before
   2843   * _adjustFocusAfterTabSwitch and due to the focus still being on the old
   2844   * tab, we end up flickering the results pane briefly.
   2845   */
   2846  afterTabSwitchFocusChange() {
   2847    this._gotFocusChange = true;
   2848    this._afterTabSelectAndFocusChange();
   2849  }
   2850 
   2851  /**
   2852   * Confirms search mode and starts a new search if appropriate for the given
   2853   * result.  See also _searchModeForResult.
   2854   *
   2855   * @param {object} options
   2856   *   Options object.
   2857   * @param {string} [options.entry]
   2858   *   If provided, this will be recorded as the entry point into search mode.
   2859   *   See setSearchMode documentation for details.
   2860   * @param {UrlbarResult} [options.result]
   2861   *   The result to confirm. Defaults to the currently selected result.
   2862   * @param {boolean} [options.checkValue]
   2863   *   If true, the trimmed input value must equal the result's keyword in order
   2864   *   to enter search mode.
   2865   * @param {boolean} [options.startQuery]
   2866   *   If true, start a query after entering search mode. Defaults to true.
   2867   * @returns {boolean}
   2868   *   True if we entered search mode and false if not.
   2869   */
   2870  maybeConfirmSearchModeFromResult({
   2871    entry,
   2872    result = this._resultForCurrentValue,
   2873    checkValue = true,
   2874    startQuery = true,
   2875  }) {
   2876    if (
   2877      !result ||
   2878      (checkValue &&
   2879        this.value.trim() != result.payload.keyword?.trim() &&
   2880        this.value.trim() != result.payload.autofillKeyword?.trim())
   2881    ) {
   2882      return false;
   2883    }
   2884 
   2885    let searchMode = this._searchModeForResult(result, entry);
   2886    if (!searchMode) {
   2887      return false;
   2888    }
   2889 
   2890    this.searchMode = searchMode;
   2891 
   2892    let value = result.payload.query?.trimStart() || "";
   2893    this._setValue(value);
   2894 
   2895    if (startQuery) {
   2896      this.startQuery({ allowAutofill: false });
   2897    }
   2898 
   2899    return true;
   2900  }
   2901 
   2902  observe(subject, topic, data) {
   2903    switch (topic) {
   2904      case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: {
   2905        let engine = subject.QueryInterface(Ci.nsISearchEngine);
   2906        switch (data) {
   2907          case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
   2908          case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: {
   2909            let searchMode = this.searchMode;
   2910            if (searchMode?.engineName == engine.name) {
   2911              // Exit search mode if the current search mode engine was removed.
   2912              this.searchMode = searchMode;
   2913            }
   2914            break;
   2915          }
   2916          case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT:
   2917            if (!this.isPrivate) {
   2918              this._updatePlaceholder(engine.name);
   2919            }
   2920            break;
   2921          case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE:
   2922            if (this.isPrivate) {
   2923              this._updatePlaceholder(engine.name);
   2924            }
   2925            break;
   2926        }
   2927        break;
   2928      }
   2929    }
   2930  }
   2931 
   2932  /**
   2933   * Get search source.
   2934   *
   2935   * @param {Event} event
   2936   *   The event that triggered this query.
   2937   * @returns {string}
   2938   *   The source name.
   2939   */
   2940  getSearchSource(event) {
   2941    if (this.#isAddressbar) {
   2942      if (this._isHandoffSession) {
   2943        return "urlbar-handoff";
   2944      }
   2945 
   2946      const isOneOff =
   2947        this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
   2948      if (this.searchMode && !isOneOff) {
   2949        // Without checking !isOneOff, we might record the string
   2950        // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
   2951        // oneoff_urlbar and oneoff_searchbar). The extra information is not
   2952        // necessary; the intent is the same regardless of whether the user is
   2953        // in search mode when they do a key-modified click/enter on a one-off.
   2954        return "urlbar-searchmode";
   2955      }
   2956 
   2957      let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   2958      if (state.persist?.searchTerms && !isOneOff) {
   2959        // Normally, we use state.persist.shouldPersist to check if search terms
   2960        // persisted. However when the user modifies the search term, the boolean
   2961        // will become false. Thus, we check the presence of the search terms to
   2962        // know whether or not search terms ever persisted in the address bar.
   2963        return "urlbar-persisted";
   2964      }
   2965    }
   2966    return this.#sapName;
   2967  }
   2968 
   2969  // Private methods below.
   2970 
   2971  /*
   2972   * Actions can have several buttons in the same result where not all
   2973   * will provide a searchMode so check the currently selected button
   2974   * in that case.
   2975   */
   2976  #providesSearchMode(result) {
   2977    if (!result) {
   2978      return false;
   2979    }
   2980    if (
   2981      this.view.selectedElement &&
   2982      result.providerName == lazy.UrlbarProviderGlobalActions.name
   2983    ) {
   2984      return this.view.selectedElement.dataset.providesSearchmode == "true";
   2985    }
   2986    return result.payload.providesSearchMode;
   2987  }
   2988 
   2989  _addObservers() {
   2990    this._observer ??= {
   2991      observe: this.observe.bind(this),
   2992      QueryInterface: ChromeUtils.generateQI([
   2993        "nsIObserver",
   2994        "nsISupportsWeakReference",
   2995      ]),
   2996    };
   2997    Services.obs.addObserver(
   2998      this._observer,
   2999      lazy.SearchUtils.TOPIC_ENGINE_MODIFIED,
   3000      true
   3001    );
   3002  }
   3003 
   3004  _removeObservers() {
   3005    if (this._observer) {
   3006      Services.obs.removeObserver(
   3007        this._observer,
   3008        lazy.SearchUtils.TOPIC_ENGINE_MODIFIED
   3009      );
   3010      this._observer = null;
   3011    }
   3012  }
   3013 
   3014  _getURIFixupInfo(searchString) {
   3015    let flags =
   3016      Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   3017      Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   3018    if (this.isPrivate) {
   3019      flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   3020    }
   3021    try {
   3022      return Services.uriFixup.getFixupURIInfo(searchString, flags);
   3023    } catch (ex) {
   3024      console.error(
   3025        `An error occured while trying to fixup "${searchString}"`,
   3026        ex
   3027      );
   3028    }
   3029    return null;
   3030  }
   3031 
   3032  _afterTabSelectAndFocusChange() {
   3033    // We must have seen both events to proceed safely.
   3034    if (!this._gotFocusChange || !this._gotTabSelect) {
   3035      return;
   3036    }
   3037    this._gotFocusChange = this._gotTabSelect = false;
   3038 
   3039    this.formatValue();
   3040    this._resetSearchState();
   3041 
   3042    // We don't use the original TabSelect event because caching it causes
   3043    // leaks on MacOS.
   3044    const event = new CustomEvent("tabswitch");
   3045    // If the urlbar is focused after a tab switch, record a potential
   3046    // engagement event. When switching from a focused to a non-focused urlbar,
   3047    // the blur event would record the abandonment. When switching from an
   3048    // unfocused to a focused urlbar, there should be no search session ongoing,
   3049    // so this will be a no-op.
   3050    if (this.focused) {
   3051      this.controller.engagementEvent.record(event, {
   3052        searchString: this._lastSearchString,
   3053        searchSource: this.getSearchSource(event),
   3054      });
   3055    }
   3056 
   3057    // Switching tabs doesn't always change urlbar focus, so we must try to
   3058    // reopen here too, not just on focus.
   3059    if (this.view.autoOpen({ event })) {
   3060      return;
   3061    }
   3062    // The input may retain focus when switching tabs in which case we
   3063    // need to close the view and search mode switcher popup explicitly.
   3064    this.searchModeSwitcher.closePanel();
   3065    this.view.close();
   3066  }
   3067 
   3068  #updateTextboxPosition() {
   3069    if (!this.view.isOpen) {
   3070      this.style.top = "";
   3071      return;
   3072    }
   3073    this.style.top = px(
   3074      this.parentNode.getBoxQuads({
   3075        ignoreTransforms: true,
   3076        flush: false,
   3077      })[0].p1.y
   3078    );
   3079  }
   3080 
   3081  #updateTextboxPositionNextFrame() {
   3082    if (!this.hasAttribute("breakout")) {
   3083      return;
   3084    }
   3085    // Allow for any layout changes to take place (e.g. when the menubar becomes
   3086    // inactive) before re-measuring to position the textbox
   3087    this.window.requestAnimationFrame(() => {
   3088      this.window.requestAnimationFrame(() => {
   3089        this.#updateTextboxPosition();
   3090      });
   3091    });
   3092  }
   3093 
   3094  #stopBreakout() {
   3095    this.removeAttribute("breakout");
   3096    this.parentNode.removeAttribute("breakout");
   3097    this.style.top = "";
   3098    try {
   3099      this.hidePopover();
   3100    } catch (ex) {
   3101      // No big deal if not a popover already.
   3102    }
   3103    this._layoutBreakoutUpdateKey = {};
   3104  }
   3105 
   3106  incrementBreakoutBlockerCount() {
   3107    this.#breakoutBlockerCount++;
   3108    if (this.#breakoutBlockerCount == 1) {
   3109      this.#stopBreakout();
   3110    }
   3111  }
   3112 
   3113  decrementBreakoutBlockerCount() {
   3114    if (this.#breakoutBlockerCount > 0) {
   3115      this.#breakoutBlockerCount--;
   3116    }
   3117    if (this.#breakoutBlockerCount === 0) {
   3118      this.#updateLayoutBreakout();
   3119    }
   3120  }
   3121 
   3122  async #updateLayoutBreakoutDimensions() {
   3123    this.#stopBreakout();
   3124 
   3125    // When this method gets called a second time before the first call
   3126    // finishes, we need to disregard the first one.
   3127    let updateKey = {};
   3128    this._layoutBreakoutUpdateKey = updateKey;
   3129    await this.window.promiseDocumentFlushed(() => {});
   3130    await new Promise(resolve => {
   3131      this.window.requestAnimationFrame(() => {
   3132        if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) {
   3133          return;
   3134        }
   3135 
   3136        this.parentNode.style.setProperty(
   3137          "--urlbar-container-height",
   3138          px(getBoundsWithoutFlushing(this.parentNode).height)
   3139        );
   3140        this.style.setProperty(
   3141          "--urlbar-height",
   3142          px(getBoundsWithoutFlushing(this).height)
   3143        );
   3144 
   3145        if (this.#breakoutBlockerCount) {
   3146          return;
   3147        }
   3148 
   3149        this.setAttribute("breakout", "true");
   3150        this.parentNode.setAttribute("breakout", "true");
   3151        this.showPopover();
   3152        this.#updateTextboxPosition();
   3153 
   3154        resolve();
   3155      });
   3156    });
   3157  }
   3158 
   3159  /**
   3160   * Sets the input field value.
   3161   *
   3162   * @param {string} val The new value to set.
   3163   * @param {object} [options] Options for setting.
   3164   * @param {boolean} [options.allowTrim] Whether the value can be trimmed.
   3165   * @param {string} [options.untrimmedValue] Override for this._untrimmedValue.
   3166   * @param {boolean} [options.valueIsTyped] Override for this.valueIsTypede.
   3167   * @param {string} [options.actionType] Value for the `actiontype` attribute.
   3168   *
   3169   * @returns {string} The set value.
   3170   */
   3171  _setValue(
   3172    val,
   3173    {
   3174      allowTrim = false,
   3175      untrimmedValue = null,
   3176      valueIsTyped = false,
   3177      actionType = undefined,
   3178    } = {}
   3179  ) {
   3180    // Don't expose internal about:reader URLs to the user.
   3181    let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
   3182    if (originalUrl) {
   3183      val = originalUrl.displaySpec;
   3184    }
   3185    this._untrimmedValue = untrimmedValue ?? val;
   3186    this._protocolIsTrimmed = false;
   3187    if (allowTrim) {
   3188      let oldVal = val;
   3189      val = this._trimValue(val);
   3190      this._protocolIsTrimmed =
   3191        oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
   3192        !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol);
   3193    }
   3194 
   3195    this.valueIsTyped = valueIsTyped;
   3196    this._resultForCurrentValue = null;
   3197    this.#setInputValue(val);
   3198    this.formatValue();
   3199 
   3200    if (actionType !== undefined) {
   3201      this.setAttribute("actiontype", actionType);
   3202    } else {
   3203      this.removeAttribute("actiontype");
   3204    }
   3205 
   3206    // Dispatch ValueChange event for accessibility.
   3207    let event = this.document.createEvent("Events");
   3208    event.initEvent("ValueChange", true, true);
   3209    this.inputField.dispatchEvent(event);
   3210 
   3211    return val;
   3212  }
   3213 
   3214  /**
   3215   * Extracts a input value from a UrlbarResult, used when filling the input
   3216   * field on selecting a result.
   3217   *
   3218   * Some examples:
   3219   *  - If the result is a bookmark keyword or dynamic, the value will be
   3220   *    its `input` property.
   3221   *  - If the result is search, the value may be `keyword` combined with
   3222   *    `suggestion` or `query`.
   3223   *  - If the result is WebExtension Omnibox, the value will be extracted
   3224   *    from `content`.
   3225   *  - For results returning URLs the value may be `urlOverride` or `url`.
   3226   *
   3227   * @param {UrlbarResult} result
   3228   *   The result to extract the value from.
   3229   * @param {object} options
   3230   *   Options object.
   3231   * @param {string} [options.urlOverride]
   3232   *   For results normally returning a url string, this allows to override
   3233   *   it. A blank string may passed-in to clear the input.
   3234   * @param {HTMLElement} [options.element]
   3235   *   The element that was selected or picked, if available. For results that
   3236   *   have multiple selectable children, the value may be taken from a child
   3237   *   element rather than the result.
   3238   * @returns {string} The value.
   3239   */
   3240  #getValueFromResult(result, { urlOverride = null, element = null } = {}) {
   3241    switch (result.type) {
   3242      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
   3243        return result.payload.input;
   3244      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
   3245        let value = "";
   3246        if (result.payload.keyword) {
   3247          value += result.payload.keyword + " ";
   3248        }
   3249        value += result.payload.suggestion || result.payload.query;
   3250        return value;
   3251      }
   3252      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
   3253        return result.payload.content;
   3254      case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
   3255        return (
   3256          element?.dataset.query ||
   3257          element?.dataset.url ||
   3258          result.payload.input ||
   3259          result.payload.query ||
   3260          ""
   3261        );
   3262      case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT:
   3263        return result.payload.autofillKeyword + " ";
   3264      case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
   3265        let value = element?.dataset.url || element?.dataset.input;
   3266        if (value) {
   3267          return value;
   3268        }
   3269        break;
   3270      }
   3271    }
   3272 
   3273    // Always respect a set urlOverride property.
   3274    if (urlOverride !== null) {
   3275      // This returns null for the empty string, allowing callers to clear the
   3276      // input by passing an empty string as urlOverride.
   3277      let url = URL.parse(urlOverride);
   3278      return url ? losslessDecodeURI(url.URI) : "";
   3279    }
   3280 
   3281    let parsedUrl = URL.parse(result.payload.url);
   3282    // If the url is not parsable, just return an empty string;
   3283    if (!parsedUrl) {
   3284      return "";
   3285    }
   3286 
   3287    let url = losslessDecodeURI(parsedUrl.URI);
   3288    // If the user didn't originally type a protocol, and we generated one,
   3289    // trim the http protocol from the input value, as https-first may upgrade
   3290    // it to https, breaking user expectations.
   3291    let stripHttp =
   3292      result.heuristic &&
   3293      result.payload.url.startsWith("http://") &&
   3294      this.userTypedValue &&
   3295      this.#getSchemelessInput(this.userTypedValue) ==
   3296        Ci.nsILoadInfo.SchemelessInputTypeSchemeless;
   3297    if (!stripHttp) {
   3298      return url;
   3299    }
   3300    // Attempt to trim the url. If doing so results in a string that is
   3301    // interpreted as search (e.g. unknown single word host, or domain suffix),
   3302    // use the unmodified url instead. Otherwise, if the user edits the url
   3303    // and confirms the new value, we may transform the url into a search.
   3304    let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0];
   3305    let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent;
   3306    if (isSearch) {
   3307      // Although https-first might not respect the shown protocol, converting
   3308      // the result to a search would be more disruptive.
   3309      return url;
   3310    }
   3311    return trimmedUrl;
   3312  }
   3313 
   3314  /**
   3315   * Extracts from a result the value to use for the `actiontype` attribute.
   3316   *
   3317   * @param {UrlbarResult} result The UrlbarResult to consider.
   3318   *
   3319   * @returns {string} The `actiontype` value, or undefined.
   3320   */
   3321  #getActionTypeFromResult(result) {
   3322    switch (result.type) {
   3323      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
   3324        return "switchtab";
   3325      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
   3326        return "extension";
   3327      default:
   3328        return undefined;
   3329    }
   3330  }
   3331 
   3332  /**
   3333   * Resets some state so that searches from the user's previous interaction
   3334   * with the input don't interfere with searches from a new interaction.
   3335   */
   3336  _resetSearchState() {
   3337    this._lastSearchString = this.value;
   3338    this._autofillPlaceholder = null;
   3339  }
   3340 
   3341  /**
   3342   * Autofills the autofill placeholder string if appropriate, and determines
   3343   * whether autofill should be allowed for the new search started by an input
   3344   * event.
   3345   *
   3346   * @param {string} value
   3347   *   The new search string.
   3348   * @returns {boolean}
   3349   *   Whether autofill should be allowed in the new search.
   3350   */
   3351  _maybeAutofillPlaceholder(value) {
   3352    // We allow autofill in local but not remote search modes.
   3353    let allowAutofill =
   3354      this.selectionEnd == value.length &&
   3355      !this.searchMode?.engineName &&
   3356      this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
   3357 
   3358    if (!allowAutofill) {
   3359      this.#clearAutofill();
   3360      return false;
   3361    }
   3362 
   3363    // Determine whether we can autofill the placeholder.  The placeholder is a
   3364    // value that we autofill now, when the search starts and before we wait on
   3365    // its first result, in order to prevent a flicker in the input caused by
   3366    // the previous autofilled substring disappearing and reappearing when the
   3367    // first result arrives.  Of course we can only autofill the placeholder if
   3368    // it starts with the new search string, and we shouldn't autofill anything
   3369    // if the caret isn't at the end of the input.
   3370    let canAutofillPlaceholder = false;
   3371    if (this._autofillPlaceholder) {
   3372      if (this._autofillPlaceholder.type == "adaptive") {
   3373        canAutofillPlaceholder =
   3374          value.length >=
   3375            this._autofillPlaceholder.adaptiveHistoryInput.length &&
   3376          this._autofillPlaceholder.value
   3377            .toLocaleLowerCase()
   3378            .startsWith(value.toLocaleLowerCase());
   3379      } else {
   3380        canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL(
   3381          this._autofillPlaceholder.value,
   3382          value
   3383        );
   3384      }
   3385    }
   3386 
   3387    if (!canAutofillPlaceholder) {
   3388      this._autofillPlaceholder = null;
   3389    } else if (
   3390      this._autofillPlaceholder &&
   3391      this.selectionEnd == this.value.length &&
   3392      this._enableAutofillPlaceholder
   3393    ) {
   3394      let autofillValue =
   3395        value + this._autofillPlaceholder.value.substring(value.length);
   3396      if (
   3397        this.value === autofillValue &&
   3398        this.selectionStart === value.length &&
   3399        this.selectionEnd === autofillValue.length
   3400      ) {
   3401        return true;
   3402      }
   3403      this._autofillValue({
   3404        value: autofillValue,
   3405        selectionStart: value.length,
   3406        selectionEnd: autofillValue.length,
   3407        type: this._autofillPlaceholder.type,
   3408        adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
   3409        untrimmedValue: this._autofillPlaceholder.untrimmedValue,
   3410      });
   3411    }
   3412 
   3413    return true;
   3414  }
   3415 
   3416  /**
   3417   * Invoked on overflow/underflow/scrollend events to update attributes
   3418   * related to the input text directionality. Overflow fade masks use these
   3419   * attributes to appear at the proper side of the urlbar.
   3420   */
   3421  updateTextOverflow() {
   3422    if (!this._overflowing) {
   3423      this.removeAttribute("textoverflow");
   3424      return;
   3425    }
   3426 
   3427    let isRTL =
   3428      this.getAttribute("domaindir") === "rtl" &&
   3429      lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window);
   3430 
   3431    this.window.promiseDocumentFlushed(() => {
   3432      // Check overflow again to ensure it didn't change in the meanwhile.
   3433      let input = this.inputField;
   3434      if (input && this._overflowing) {
   3435        // Normally we overflow at the end side of the text direction, though
   3436        // RTL domains may cause us to overflow at the opposite side.
   3437        // The outcome differs depending on the input field contents and applied
   3438        // formatting, and reports the final state of all the scrolling into an
   3439        // attribute available to css rules.
   3440        // Note it's also possible to scroll an unfocused input field using
   3441        // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
   3442        // scroll (without modifiers) on Mac.
   3443        let side = "both";
   3444        if (isRTL) {
   3445          if (input.scrollLeft == 0) {
   3446            side = "left";
   3447          } else if (input.scrollLeft == input.scrollLeftMin) {
   3448            side = "right";
   3449          }
   3450        } else if (input.scrollLeft == 0) {
   3451          side = "right";
   3452        } else if (input.scrollLeft == input.scrollLeftMax) {
   3453          side = "left";
   3454        }
   3455 
   3456        this.window.requestAnimationFrame(() => {
   3457          // And check once again, since we might have stopped overflowing
   3458          // since the promiseDocumentFlushed callback fired.
   3459          if (this._overflowing) {
   3460            this.setAttribute("textoverflow", side);
   3461          }
   3462        });
   3463      }
   3464    });
   3465  }
   3466 
   3467  _updateUrlTooltip() {
   3468    if (this.focused || !this._overflowing) {
   3469      this.inputField.removeAttribute("title");
   3470    } else {
   3471      this.inputField.setAttribute("title", this.untrimmedValue);
   3472    }
   3473  }
   3474 
   3475  _getSelectedValueForClipboard() {
   3476    let selectedVal = this.#selectedText;
   3477 
   3478    // Handle multiple-range selection as a string for simplicity.
   3479    if (this.editor.selection.rangeCount > 1) {
   3480      return selectedVal;
   3481    }
   3482 
   3483    // If the selection doesn't start at the beginning or doesn't span the
   3484    // full domain or the URL bar is modified or there is no text at all,
   3485    // nothing else to do here.
   3486    // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often
   3487    // it doesn't really indicate a user typed a value, it's rather used as
   3488    // a way to tell if the value was modified.
   3489    if (
   3490      this.selectionStart > 0 ||
   3491      selectedVal == "" ||
   3492      (this.valueIsTyped && !this._protocolIsTrimmed)
   3493    ) {
   3494      return selectedVal;
   3495    }
   3496 
   3497    // The selection doesn't span the full domain if it doesn't contain a slash and is
   3498    // followed by some character other than a slash.
   3499    if (!selectedVal.includes("/")) {
   3500      let remainder = this.value.replace(selectedVal, "");
   3501      if (remainder != "" && remainder[0] != "/") {
   3502        return selectedVal;
   3503      }
   3504    }
   3505 
   3506    let uri;
   3507    if (this.getAttribute("pageproxystate") == "valid") {
   3508      uri = this.#isOpenedPageInBlankTargetLoading
   3509        ? this.window.gBrowser.selectedBrowser.browsingContext
   3510            .nonWebControlledBlankURI
   3511        : this.window.gBrowser.currentURI;
   3512    } else {
   3513      // The value could be:
   3514      // 1. a trimmed url, set by selecting a result
   3515      // 2. a search string set by selecting a result
   3516      // 3. a url that was confirmed but didn't finish loading yet
   3517      // If it's an url the untrimmedValue should resolve to a valid URI,
   3518      // otherwise it's a search string that should be copied as-is.
   3519 
   3520      // If the copied text is that autofilled value, return the url including
   3521      // the protocol from its suggestion.
   3522      let result = this._resultForCurrentValue;
   3523 
   3524      if (result?.autofill?.value == selectedVal) {
   3525        return result.payload.url;
   3526      }
   3527 
   3528      uri = URL.parse(this._untrimmedValue)?.URI;
   3529      if (!uri) {
   3530        return selectedVal;
   3531      }
   3532    }
   3533    uri = this.makeURIReadable(uri);
   3534    let displaySpec = uri.displaySpec;
   3535 
   3536    // If the entire URL is selected, just use the actual loaded URI,
   3537    // unless we want a decoded URI, or it's a data: or javascript: URI,
   3538    // since those are hard to read when encoded.
   3539    if (
   3540      this.value == selectedVal &&
   3541      !uri.schemeIs("javascript") &&
   3542      !uri.schemeIs("data") &&
   3543      !lazy.UrlbarPrefs.get("decodeURLsOnCopy")
   3544    ) {
   3545      return displaySpec;
   3546    }
   3547 
   3548    // Just the beginning of the URL is selected, or we want a decoded
   3549    // url. First check for a trimmed value.
   3550 
   3551    if (
   3552      !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
   3553      // Note _trimValue may also trim a trailing slash, thus we can't just do
   3554      // a straight string compare to tell if the protocol was trimmed.
   3555      !displaySpec.startsWith(this._trimValue(displaySpec))
   3556    ) {
   3557      selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal;
   3558    }
   3559 
   3560    // If selection starts from the beginning and part or all of the URL
   3561    // is selected, we check for decoded characters and encode them.
   3562    // Unless decodeURLsOnCopy is set. Do not encode data: URIs.
   3563    if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) {
   3564      try {
   3565        if (URL.canParse(selectedVal)) {
   3566          // Use encodeURI instead of URL.href because we don't want
   3567          // trailing slash.
   3568          selectedVal = encodeURI(selectedVal);
   3569        }
   3570      } catch (ex) {
   3571        // URL is invalid. Return original selected value.
   3572      }
   3573    }
   3574 
   3575    return selectedVal;
   3576  }
   3577 
   3578  _toggleActionOverride(event) {
   3579    if (
   3580      event.keyCode == KeyEvent.DOM_VK_SHIFT ||
   3581      event.keyCode == KeyEvent.DOM_VK_ALT ||
   3582      event.keyCode ==
   3583        (AppConstants.platform == "macosx"
   3584          ? KeyEvent.DOM_VK_META
   3585          : KeyEvent.DOM_VK_CONTROL)
   3586    ) {
   3587      if (event.type == "keydown") {
   3588        this._actionOverrideKeyCount++;
   3589        this.toggleAttribute("action-override", true);
   3590        this.view.panel.setAttribute("action-override", true);
   3591      } else if (
   3592        this._actionOverrideKeyCount &&
   3593        --this._actionOverrideKeyCount == 0
   3594      ) {
   3595        this._clearActionOverride();
   3596      }
   3597    }
   3598  }
   3599 
   3600  _clearActionOverride() {
   3601    this._actionOverrideKeyCount = 0;
   3602    this.removeAttribute("action-override");
   3603    this.view.panel.removeAttribute("action-override");
   3604  }
   3605 
   3606  /**
   3607   * Records in telemetry that a search is being loaded,
   3608   * updates an incremental total number of searches in a pref,
   3609   * and informs ASRouter that a search has occurred via a trigger send
   3610   *
   3611   * @param {nsISearchEngine} engine
   3612   *   The engine to generate the query for.
   3613   * @param {Event} event
   3614   *   The event that triggered this query.
   3615   * @param {object} [searchActionDetails]
   3616   *   The details associated with this search query.
   3617   * @param {boolean} [searchActionDetails.isSuggestion]
   3618   *   True if this query was initiated from a suggestion from the search engine.
   3619   * @param {boolean} [searchActionDetails.alias]
   3620   *   True if this query was initiated via a search alias.
   3621   * @param {boolean} [searchActionDetails.isFormHistory]
   3622   *   True if this query was initiated from a form history result.
   3623   * @param {string} [searchActionDetails.url]
   3624   *   The url this query was triggered with.
   3625   * @param {MozBrowser} [browser]
   3626   *   The browser where the search is being opened.
   3627   *   Defaults to the window's selected browser.
   3628   */
   3629  _recordSearch(
   3630    engine,
   3631    event,
   3632    searchActionDetails = {},
   3633    browser = this.window.gBrowser.selectedBrowser
   3634  ) {
   3635    const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
   3636    const searchSource = this.getSearchSource(event);
   3637 
   3638    // Record when the user uses the search bar to be
   3639    // used for message targeting. This is arbitrarily capped
   3640    // at 100, only to prevent the number from growing ifinitely.
   3641    const totalSearches = Services.prefs.getIntPref(
   3642      "browser.search.totalSearches"
   3643    );
   3644    const totalSearchesCap = 100;
   3645    if (totalSearches < totalSearchesCap) {
   3646      Services.prefs.setIntPref(
   3647        "browser.search.totalSearches",
   3648        totalSearches + 1
   3649      );
   3650    }
   3651 
   3652    // Sending a trigger to ASRouter when a search happens
   3653    lazy.ASRouter.sendTriggerMessage({
   3654      browser,
   3655      id: "onSearch",
   3656      context: {
   3657        isSuggestion: searchActionDetails.isSuggestion || false,
   3658        searchSource,
   3659        isOneOff,
   3660      },
   3661    });
   3662 
   3663    lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, {
   3664      ...searchActionDetails,
   3665      isOneOff,
   3666      newtabSessionId: this._handoffSession,
   3667    });
   3668  }
   3669 
   3670  /**
   3671   * Shortens the given value, usually by removing http:// and trailing slashes.
   3672   *
   3673   * @param {string} val
   3674   *   The string to be trimmed if it appears to be URI
   3675   * @returns {string}
   3676   *   The trimmed string
   3677   */
   3678  _trimValue(val) {
   3679    if (!this.#isAddressbar) {
   3680      return val;
   3681    }
   3682    let trimmedValue = lazy.UrlbarPrefs.get("trimURLs")
   3683      ? lazy.BrowserUIUtils.trimURL(val)
   3684      : val;
   3685    // Only trim value if the directionality doesn't change to RTL and we're not
   3686    // showing a strikeout https protocol.
   3687    return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) ||
   3688      this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val)
   3689      ? val
   3690      : trimmedValue;
   3691  }
   3692 
   3693  /**
   3694   * Returns whether the passed-in event may represents a canonization request.
   3695   *
   3696   * @param {Event} event
   3697   *   An Event to examine.
   3698   * @returns {boolean}
   3699   *   Whether the event is a KeyboardEvent that triggers canonization.
   3700   */
   3701  #isCanonizeKeyboardEvent(event) {
   3702    return (
   3703      KeyboardEvent.isInstance(event) &&
   3704      event.keyCode == KeyEvent.DOM_VK_RETURN &&
   3705      (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) &&
   3706      !event._disableCanonization &&
   3707      lazy.UrlbarPrefs.get("ctrlCanonizesURLs")
   3708    );
   3709  }
   3710 
   3711  /**
   3712   * If appropriate, this prefixes a search string with 'www.' and suffixes it
   3713   * with browser.fixup.alternate.suffix prior to navigating.
   3714   *
   3715   * @param {Event} event
   3716   *   The event that triggered this query.
   3717   * @param {string} value
   3718   *   The search string that should be canonized.
   3719   * @returns {string}
   3720   *   Returns the canonized URL if available and null otherwise.
   3721   */
   3722  _maybeCanonizeURL(event, value) {
   3723    // Only add the suffix when the URL bar value isn't already "URL-like",
   3724    // and only if we get a keyboard event, to match user expectations.
   3725    if (
   3726      this.sapName == "searchbar" ||
   3727      !this.#isCanonizeKeyboardEvent(event) ||
   3728      !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
   3729    ) {
   3730      return null;
   3731    }
   3732 
   3733    let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
   3734    if (!suffix.endsWith("/")) {
   3735      suffix += "/";
   3736    }
   3737 
   3738    // trim leading/trailing spaces (bug 233205)
   3739    value = value.trim();
   3740 
   3741    // Tack www. and suffix on.  If user has appended directories, insert
   3742    // suffix before them (bug 279035).  Be careful not to get two slashes.
   3743    let firstSlash = value.indexOf("/");
   3744    if (firstSlash >= 0) {
   3745      value =
   3746        value.substring(0, firstSlash) +
   3747        suffix +
   3748        value.substring(firstSlash + 1);
   3749    } else {
   3750      value = value + suffix;
   3751    }
   3752 
   3753    try {
   3754      const info = Services.uriFixup.getFixupURIInfo(
   3755        value,
   3756        Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI
   3757      );
   3758      value = info.fixedURI.spec;
   3759    } catch (ex) {
   3760      console.error(`An error occured while trying to fixup "${value}"`, ex);
   3761    }
   3762 
   3763    this.value = value;
   3764    return value;
   3765  }
   3766 
   3767  /**
   3768   * Autofills a value into the input.  The value will be autofilled regardless
   3769   * of the input's current value.
   3770   *
   3771   * @param {object} options
   3772   *   The options object.
   3773   * @param {string} options.value
   3774   *   The value to autofill.
   3775   * @param {number} options.selectionStart
   3776   *   The new selectionStart.
   3777   * @param {number} options.selectionEnd
   3778   *   The new selectionEnd.
   3779   * @param {"origin" | "url" | "adaptive"} options.type
   3780   *   The autofill type, one of: "origin", "url", "adaptive"
   3781   * @param {string} options.adaptiveHistoryInput
   3782   *   If the autofill type is "adaptive", this is the matching `input` value
   3783   *   from adaptive history.
   3784   * @param {string} [options.untrimmedValue]
   3785   *   Untrimmed value including a protocol.
   3786   */
   3787  _autofillValue({
   3788    value,
   3789    selectionStart,
   3790    selectionEnd,
   3791    type,
   3792    adaptiveHistoryInput,
   3793    untrimmedValue,
   3794  }) {
   3795    const valueMatches = this.value === value;
   3796    const selectionMatches =
   3797      this.selectionStart === selectionStart &&
   3798      this.selectionEnd === selectionEnd;
   3799    if (valueMatches && selectionMatches) {
   3800      return;
   3801    }
   3802    // The autofilled value may be a URL that includes a scheme at the
   3803    // beginning.  Do not allow it to be trimmed.
   3804    if (!valueMatches) {
   3805      this._setValue(value, { untrimmedValue });
   3806    }
   3807    this.setSelectionRange(selectionStart, selectionEnd);
   3808    // Ensure selection state is cached for contenteditable and events fire.
   3809    if (!selectionMatches) {
   3810      if (this.#smartbarInputController) {
   3811        this.#smartbarInputController.dispatchSelectionChange();
   3812      } else {
   3813        this.inputField.dispatchEvent(
   3814          new Event("selectionchange", { bubbles: true, cancelable: false })
   3815        );
   3816      }
   3817    }
   3818    this._autofillPlaceholder = {
   3819      value,
   3820      type,
   3821      adaptiveHistoryInput,
   3822      selectionStart,
   3823      selectionEnd,
   3824      untrimmedValue,
   3825    };
   3826  }
   3827 
   3828  /**
   3829   * Called when a menu item from results menu is picked.
   3830   *
   3831   * @param {UrlbarResult} result The result that was picked.
   3832   * @param {Event} event The event that picked the result.
   3833   * @param {HTMLElement} element the picked view element, if available.
   3834   * @param {object} browser The browser to use for the load.
   3835   */
   3836  #pickMenuResult(result, event, element, browser) {
   3837    this.controller.engagementEvent.record(event, {
   3838      result,
   3839      element,
   3840      searchString: this._lastSearchString,
   3841      selType: element.dataset.command,
   3842    });
   3843 
   3844    if (element.dataset.command == "manage") {
   3845      this.window.openPreferences("search-locationBar");
   3846      return;
   3847    }
   3848 
   3849    let url;
   3850    if (element.dataset.command == "help") {
   3851      url = result.payload.helpUrl;
   3852    }
   3853    url ||= element.dataset.url;
   3854 
   3855    if (!url) {
   3856      return;
   3857    }
   3858 
   3859    let where = this._whereToOpen(event);
   3860    if (element.dataset.command == "help" && where == "current") {
   3861      // Open help links in a new tab.
   3862      where = "tab";
   3863    }
   3864 
   3865    this.view.close({ elementPicked: true });
   3866 
   3867    this._loadURL(
   3868      url,
   3869      event,
   3870      where,
   3871      {
   3872        allowInheritPrincipal: false,
   3873        private: this.isPrivate,
   3874      },
   3875      {
   3876        source: result.source,
   3877        type: result.type,
   3878      },
   3879      browser
   3880    );
   3881  }
   3882 
   3883  /**
   3884   * Loads the url in the appropriate place.
   3885   *
   3886   * @param {string} url
   3887   *   The URL to open.
   3888   * @param {string} openUILinkWhere
   3889   *   Where we expect the result to be opened.
   3890   * @param {object} params
   3891   *   The parameters related to how and where the result will be opened.
   3892   *   Further supported paramters are listed in _loadURL.
   3893   * @param {object} [params.triggeringPrincipal]
   3894   *   The principal that the action was triggered from.
   3895   * @param {object} [resultDetails]
   3896   *   Details of the selected result, if any.
   3897   *   Further supported details are listed in _loadURL.
   3898   * @param {string} [resultDetails.searchTerm]
   3899   *   Search term of the result source, if any.
   3900   * @param {object} browser the browser to use for the load.
   3901   */
   3902  #prepareAddressbarLoad(
   3903    url,
   3904    openUILinkWhere,
   3905    params,
   3906    resultDetails = null,
   3907    browser
   3908  ) {
   3909    if (!this.#isAddressbar) {
   3910      throw new Error(
   3911        "Can't prepare addressbar load when this isn't an addressbar input"
   3912      );
   3913    }
   3914 
   3915    // No point in setting these because we'll handleRevert() a few rows below.
   3916    if (openUILinkWhere == "current") {
   3917      // Make sure URL is formatted properly (don't show punycode).
   3918      let formattedURL = url;
   3919      try {
   3920        formattedURL = losslessDecodeURI(new URL(url).URI);
   3921      } catch {}
   3922 
   3923      this.value =
   3924        lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() &&
   3925        resultDetails?.searchTerm
   3926          ? resultDetails.searchTerm
   3927          : formattedURL;
   3928      browser.userTypedValue = this.value;
   3929    }
   3930 
   3931    // No point in setting this if we are loading in a new window.
   3932    if (
   3933      openUILinkWhere != "window" &&
   3934      this.window.gInitialPages.includes(url)
   3935    ) {
   3936      browser.initialPageLoadedFromUserAction = url;
   3937    }
   3938 
   3939    try {
   3940      lazy.UrlbarUtils.addToUrlbarHistory(url, this.window);
   3941    } catch (ex) {
   3942      // Things may go wrong when adding url to session history,
   3943      // but don't let that interfere with the loading of the url.
   3944      console.error(ex);
   3945    }
   3946 
   3947    // TODO: When bug 1498553 is resolved, we should be able to
   3948    // remove the !triggeringPrincipal condition here.
   3949    if (
   3950      !params.triggeringPrincipal ||
   3951      params.triggeringPrincipal.isSystemPrincipal
   3952    ) {
   3953      // Reset DOS mitigations for the basic auth prompt.
   3954      delete browser.authPromptAbuseCounter;
   3955 
   3956      // Reset temporary permissions on the current tab if the user reloads
   3957      // the tab via the urlbar.
   3958      if (
   3959        openUILinkWhere == "current" &&
   3960        browser.currentURI &&
   3961        url === browser.currentURI.spec
   3962      ) {
   3963        this.window.SitePermissions.clearTemporaryBlockPermissions(browser);
   3964      }
   3965    }
   3966 
   3967    // Specifies that the URL load was initiated by the URL bar.
   3968    params.initiatedByURLBar = true;
   3969  }
   3970 
   3971  /**
   3972   * Loads the url in the appropriate place.
   3973   *
   3974   * @param {string} url
   3975   *   The URL to open.
   3976   * @param {Event} event
   3977   *   The event that triggered to load the url.
   3978   * @param {string} openUILinkWhere
   3979   *   Where we expect the result to be opened.
   3980   * @param {object} params
   3981   *   The parameters related to how and where the result will be opened.
   3982   *   Further supported parameters are listed in utilityOverlay.js#openUILinkIn.
   3983   * @param {object} [params.triggeringPrincipal]
   3984   *   The principal that the action was triggered from.
   3985   * @param {nsIInputStream} [params.postData]
   3986   *   The POST data associated with a search submission.
   3987   * @param {boolean} [params.allowInheritPrincipal]
   3988   *   Whether the principal can be inherited.
   3989   * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput]
   3990   *   Whether the search/URL term was without an explicit scheme.
   3991   * @param {object} [resultDetails]
   3992   *   Details of the selected result, if any.
   3993   * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type]
   3994   *   Details of the result type, if any.
   3995   * @param {string} [resultDetails.searchTerm]
   3996   *   Search term of the result source, if any.
   3997   * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source]
   3998   *   Details of the result source, if any.
   3999   * @param {object} browser [optional] the browser to use for the load.
   4000   */
   4001  _loadURL(
   4002    url,
   4003    event,
   4004    openUILinkWhere,
   4005    params,
   4006    resultDetails = null,
   4007    browser = this.window.gBrowser.selectedBrowser
   4008  ) {
   4009    if (this.#isAddressbar) {
   4010      this.#prepareAddressbarLoad(
   4011        url,
   4012        openUILinkWhere,
   4013        params,
   4014        resultDetails,
   4015        browser
   4016      );
   4017    }
   4018 
   4019    params.allowThirdPartyFixup = true;
   4020 
   4021    if (openUILinkWhere == "current") {
   4022      params.targetBrowser = browser;
   4023      params.indicateErrorPageLoad = true;
   4024      params.allowPinnedTabHostChange = true;
   4025      params.allowPopups = url.startsWith("javascript:");
   4026    } else {
   4027      params.initiatingDoc = this.window.document;
   4028    }
   4029 
   4030    if (
   4031      this._keyDownEnterDeferred &&
   4032      event?.keyCode === KeyEvent.DOM_VK_RETURN &&
   4033      openUILinkWhere === "current"
   4034    ) {
   4035      // In this case, we move the focus to the browser that loads the content
   4036      // upon key up the enter key.
   4037      // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid
   4038      // focusing on the browser in the function. And also, set loadedContent
   4039      // flag that whether the content is loaded in the current tab by this enter
   4040      // key. _keyDownEnterDeferred promise is processed at key up the enter,
   4041      // focus on the browser passed by _keyDownEnterDeferred.resolve().
   4042      params.avoidBrowserFocus = true;
   4043      this._keyDownEnterDeferred.loadedContent = true;
   4044      this._keyDownEnterDeferred.resolve(browser);
   4045    }
   4046 
   4047    // Ensure the window gets the `private` feature if the current window
   4048    // is private, unless the caller explicitly requested not to.
   4049    if (this.isPrivate && !("private" in params)) {
   4050      params.private = true;
   4051    }
   4052 
   4053    // Focus the content area before triggering loads, since if the load
   4054    // occurs in a new tab, we want focus to be restored to the content
   4055    // area when the current tab is re-selected.
   4056    if (!params.avoidBrowserFocus) {
   4057      browser.focus();
   4058      // Make sure the domain name stays visible for spoof protection and usability.
   4059      this.setSelectionRange(0, 0);
   4060    }
   4061 
   4062    if (openUILinkWhere != "current") {
   4063      this.handleRevert();
   4064    }
   4065 
   4066    // Notify about the start of navigation.
   4067    this.#notifyStartNavigation(resultDetails);
   4068 
   4069    try {
   4070      this.window.openTrustedLinkIn(url, openUILinkWhere, params);
   4071    } catch (ex) {
   4072      // This load can throw an exception in certain cases, which means
   4073      // we'll want to replace the URL with the loaded URL:
   4074      if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
   4075        this.handleRevert();
   4076      }
   4077    }
   4078 
   4079    // If we show the focus border after closing the view, it would appear to
   4080    // flash since this._on_blur would remove it immediately after.
   4081    this.view.close({ showFocusBorder: false });
   4082  }
   4083 
   4084  /**
   4085   * Determines where a URL/page should be opened.
   4086   *
   4087   * @param {Event} event the event triggering the opening.
   4088   * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
   4089   */
   4090  _whereToOpen(event) {
   4091    let isKeyboardEvent = KeyboardEvent.isInstance(event);
   4092    let reuseEmpty = isKeyboardEvent;
   4093    let where = undefined;
   4094    if (
   4095      isKeyboardEvent &&
   4096      (event.altKey || event.getModifierState("AltGraph"))
   4097    ) {
   4098      // We support using 'alt' to open in a tab, because ctrl/shift
   4099      // might be used for canonizing URLs:
   4100      where = event.shiftKey ? "tabshifted" : "tab";
   4101    } else if (this.#isCanonizeKeyboardEvent(event)) {
   4102      // If we're allowing canonization, and this is a canonization key event,
   4103      // open in current tab to avoid handling as new tab modifier.
   4104      where = "current";
   4105    } else {
   4106      where = lazy.BrowserUtils.whereToOpenLink(event, false, false);
   4107    }
   4108    if (lazy.UrlbarPrefs.get("openintab")) {
   4109      if (where == "current") {
   4110        where = "tab";
   4111      } else if (where == "tab") {
   4112        where = "current";
   4113      }
   4114      reuseEmpty = true;
   4115    }
   4116    if (
   4117      where == "tab" &&
   4118      reuseEmpty &&
   4119      this.window.gBrowser.selectedTab.isEmpty
   4120    ) {
   4121      where = "current";
   4122    }
   4123    return where;
   4124  }
   4125 
   4126  _initCopyCutController() {
   4127    // Clipboard handling is managed by the multiline editor in smartbar mode.
   4128    if (this.#isSmartbarMode) {
   4129      return;
   4130    }
   4131    if (this._copyCutController) {
   4132      return;
   4133    }
   4134    this._copyCutController = new CopyCutController(this);
   4135    this.inputField.controllers.insertControllerAt(0, this._copyCutController);
   4136  }
   4137 
   4138  /**
   4139   * Searches the context menu for the location of a specific command.
   4140   *
   4141   * @param {string} menuItemCommand
   4142   *    The command to search for.
   4143   * @returns {HTMLElement}
   4144   *    Html element that matches the command or
   4145   *    the last element if we could not find the command.
   4146   */
   4147  #findMenuItemLocation(menuItemCommand) {
   4148    let inputBox = this.querySelector("moz-input-box");
   4149    let contextMenu = inputBox.menupopup;
   4150    let insertLocation = contextMenu.firstElementChild;
   4151    // find the location of the command
   4152    while (
   4153      insertLocation.nextElementSibling &&
   4154      insertLocation.getAttribute("cmd") != menuItemCommand
   4155    ) {
   4156      insertLocation = insertLocation.nextElementSibling;
   4157    }
   4158 
   4159    return insertLocation;
   4160  }
   4161 
   4162  /**
   4163   * Strips known tracking query parameters/ link decorators.
   4164   *
   4165   * @returns {nsIURI}
   4166   *   The stripped URI or original URI, if nothing can be
   4167   *   stripped
   4168   */
   4169  #stripURI() {
   4170    let copyString = this._getSelectedValueForClipboard();
   4171    if (!copyString) {
   4172      return null;
   4173    }
   4174    let strippedURI = null;
   4175 
   4176    // Error check occurs during isClipboardURIValid
   4177    let uri = Services.io.newURI(copyString);
   4178    try {
   4179      strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
   4180    } catch (e) {
   4181      console.warn(`stripForCopyOrShare: ${e.message}`);
   4182      return uri;
   4183    }
   4184 
   4185    if (strippedURI) {
   4186      return this.makeURIReadable(strippedURI);
   4187    }
   4188    return uri;
   4189  }
   4190 
   4191  /**
   4192   * Checks if the clipboard contains a valid URI
   4193   *
   4194   * @returns {true|false}
   4195   */
   4196  #isClipboardURIValid() {
   4197    let copyString = this._getSelectedValueForClipboard();
   4198    if (!copyString) {
   4199      return false;
   4200    }
   4201 
   4202    return URL.canParse(copyString);
   4203  }
   4204 
   4205  /**
   4206   * Checks if there is a query parameter that can be stripped
   4207   *
   4208   * @returns {true|false}
   4209   */
   4210  #canStrip() {
   4211    let copyString = this._getSelectedValueForClipboard();
   4212    if (!copyString) {
   4213      return false;
   4214    }
   4215    // throws if the selected string is not a valid URI
   4216    try {
   4217      let uri = Services.io.newURI(copyString);
   4218      return lazy.QueryStringStripper.canStripForShare(uri);
   4219    } catch (e) {
   4220      console.warn("canStrip failed!", e);
   4221      return false;
   4222    }
   4223  }
   4224 
   4225  /**
   4226   * Restores the untrimmed value in the urlbar.
   4227   *
   4228   * @param {object} [options]
   4229   *  Options for untrimming.
   4230   * @param {boolean} [options.moveCursorToStart]
   4231   *  Whether the cursor should be moved at position 0 after untrimming.
   4232   * @param {boolean} [options.ignoreSelection]
   4233   *  Whether this should untrim, regardless of the current selection state.
   4234   */
   4235  #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) {
   4236    // Check if we can untrim the current value.
   4237    if (
   4238      !lazy.UrlbarPrefs.getScotchBonnetPref(
   4239        "untrimOnUserInteraction.featureGate"
   4240      ) ||
   4241      !this._protocolIsTrimmed ||
   4242      !this.focused ||
   4243      (!ignoreSelection && this.#allTextSelected)
   4244    ) {
   4245      return;
   4246    }
   4247 
   4248    let selectionStart = this.selectionStart;
   4249    let selectionEnd = this.selectionEnd;
   4250 
   4251    // Correct the selection taking the trimmed protocol into account.
   4252    let offset = lazy.BrowserUIUtils.trimURLProtocol.length;
   4253 
   4254    // In case of autofill, we may have to adjust its boundaries.
   4255    if (this._autofillPlaceholder) {
   4256      this._autofillPlaceholder.selectionStart += offset;
   4257      this._autofillPlaceholder.selectionEnd += offset;
   4258    }
   4259 
   4260    if (moveCursorToStart) {
   4261      this._setValue(this._untrimmedValue, {
   4262        valueIsTyped: this.valueIsTyped,
   4263      });
   4264      this.setSelectionRange(0, 0);
   4265      return;
   4266    }
   4267 
   4268    if (selectionStart == selectionEnd) {
   4269      // When cursor is at the end of the string, untrimming may
   4270      // reintroduced a trailing slash and we want to move past it.
   4271      if (selectionEnd == this.value.length) {
   4272        offset += 1;
   4273      }
   4274      selectionStart = selectionEnd += offset;
   4275    } else {
   4276      // There's a selection, so we must calculate both the initial
   4277      // protocol and the eventual trailing slash.
   4278      if (selectionStart != 0) {
   4279        selectionStart += offset;
   4280      } else {
   4281        // When selection starts at the beginning, the adjusted selection will
   4282        // include the protocol only if the selected text includes the host.
   4283        // The port is left out, as one may want to exclude it from the copy.
   4284        let prePathMinusPort;
   4285        try {
   4286          let uri = Services.io.newURI(this._untrimmedValue);
   4287          prePathMinusPort = [uri.userPass, uri.displayHost]
   4288            .filter(Boolean)
   4289            .join("@");
   4290        } catch (ex) {
   4291          lazy.logger.error("Should only try to untrim valid URLs");
   4292        }
   4293        if (!this.#selectedText.startsWith(prePathMinusPort)) {
   4294          selectionStart += offset;
   4295        }
   4296      }
   4297      if (selectionEnd == this.value.length) {
   4298        offset += 1;
   4299      }
   4300      selectionEnd += offset;
   4301    }
   4302 
   4303    this._setValue(this._untrimmedValue, {
   4304      valueIsTyped: this.valueIsTyped,
   4305    });
   4306 
   4307    this.setSelectionRange(selectionStart, selectionEnd);
   4308  }
   4309 
   4310  // The strip-on-share feature will strip known tracking/decorational
   4311  // query params from the URI and copy the stripped version to the clipboard.
   4312  _initStripOnShare() {
   4313    let contextMenu = this.querySelector("moz-input-box").menupopup;
   4314    let insertLocation = this.#findMenuItemLocation("cmd_copy");
   4315    // set up the menu item
   4316    let stripOnShare = this.document.createXULElement("menuitem");
   4317    this.document.l10n.setAttributes(
   4318      stripOnShare,
   4319      "text-action-copy-clean-link"
   4320    );
   4321    stripOnShare.setAttribute("anonid", "strip-on-share");
   4322    stripOnShare.id = "strip-on-share";
   4323 
   4324    insertLocation.insertAdjacentElement("afterend", stripOnShare);
   4325 
   4326    // Register listener that returns the stripped url or falls back
   4327    // to the original url if nothing can be stripped.
   4328    stripOnShare.addEventListener("command", () => {
   4329      let strippedURI = this.#stripURI();
   4330      lazy.ClipboardHelper.copyString(strippedURI.displaySpec);
   4331    });
   4332 
   4333    // Register a listener that hides the menu item if there is nothing to copy.
   4334    contextMenu.addEventListener("popupshowing", () => {
   4335      // feature is not enabled
   4336      if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) {
   4337        stripOnShare.setAttribute("hidden", true);
   4338        return;
   4339      }
   4340      let controller =
   4341        this.document.commandDispatcher.getControllerForCommand("cmd_copy");
   4342      if (
   4343        !controller.isCommandEnabled("cmd_copy") ||
   4344        !this.#isClipboardURIValid()
   4345      ) {
   4346        stripOnShare.setAttribute("hidden", true);
   4347        return;
   4348      }
   4349      stripOnShare.removeAttribute("hidden");
   4350      if (!this.#canStrip()) {
   4351        stripOnShare.setAttribute("disabled", true);
   4352        return;
   4353      }
   4354      stripOnShare.removeAttribute("disabled");
   4355    });
   4356  }
   4357 
   4358  _initPasteAndGo() {
   4359    let inputBox = this.querySelector("moz-input-box");
   4360    let contextMenu = inputBox.menupopup;
   4361    let insertLocation = this.#findMenuItemLocation("cmd_paste");
   4362    if (!insertLocation) {
   4363      return;
   4364    }
   4365 
   4366    let pasteAndGo = this.document.createXULElement("menuitem");
   4367    pasteAndGo.id = "paste-and-go";
   4368    let label = Services.strings
   4369      .createBundle("chrome://browser/locale/browser.properties")
   4370      .GetStringFromName("pasteAndGo.label");
   4371    pasteAndGo.setAttribute("label", label);
   4372    pasteAndGo.setAttribute("anonid", "paste-and-go");
   4373    pasteAndGo.addEventListener("command", () => {
   4374      this._suppressStartQuery = true;
   4375 
   4376      this.select();
   4377      this.window.goDoCommand("cmd_paste");
   4378      this.setResultForCurrentValue(null);
   4379      this.handleCommand();
   4380      this.controller.clearLastQueryContextCache();
   4381 
   4382      this._suppressStartQuery = false;
   4383    });
   4384 
   4385    contextMenu.addEventListener("popupshowing", () => {
   4386      // Close the results pane when the input field contextual menu is open,
   4387      // because paste and go doesn't want a result selection.
   4388      this.view.close();
   4389 
   4390      let controller =
   4391        this.document.commandDispatcher.getControllerForCommand("cmd_paste");
   4392      let enabled = controller.isCommandEnabled("cmd_paste");
   4393      if (enabled) {
   4394        pasteAndGo.removeAttribute("disabled");
   4395      } else {
   4396        pasteAndGo.setAttribute("disabled", "true");
   4397      }
   4398    });
   4399 
   4400    insertLocation.insertAdjacentElement("afterend", pasteAndGo);
   4401  }
   4402 
   4403  /**
   4404   * This notifies observers that the user has entered or selected something in
   4405   * the URL bar which will cause navigation.
   4406   *
   4407   * We use the observer service, so that we don't need to load extra facilities
   4408   * if they aren't being used, e.g. WebNavigation.
   4409   *
   4410   * @param {UrlbarResult} result
   4411   *   Details of the result that was selected, if any.
   4412   */
   4413  #notifyStartNavigation(result) {
   4414    if (this.#isAddressbar) {
   4415      Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
   4416    }
   4417  }
   4418 
   4419  /**
   4420   * Returns a search mode object if a result should enter search mode when
   4421   * selected.
   4422   *
   4423   * @param {UrlbarResult} result
   4424   *   The result to check.
   4425   * @param {string} [entry]
   4426   *   If provided, this will be recorded as the entry point into search mode.
   4427   *   See setSearchMode() documentation for details.
   4428   * @returns {object} A search mode object. Null if search mode should not be
   4429   *   entered. See setSearchMode documentation for details.
   4430   */
   4431  _searchModeForResult(result, entry = null) {
   4432    // Search mode is determined by the result's keyword or engine.
   4433    if (!result.payload.keyword && !result.payload.engine) {
   4434      return null;
   4435    }
   4436 
   4437    let searchMode = this.searchModeForToken(result.payload.keyword);
   4438    // If result.originalEngine is set, then the user is Alt+Tabbing
   4439    // through the one-offs, so the keyword doesn't match the engine.
   4440    if (
   4441      !searchMode &&
   4442      result.payload.engine &&
   4443      (!result.payload.originalEngine ||
   4444        result.payload.engine == result.payload.originalEngine)
   4445    ) {
   4446      searchMode = { engineName: result.payload.engine };
   4447    }
   4448 
   4449    if (searchMode) {
   4450      if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) {
   4451        searchMode.restrictType = "keyword";
   4452      } else if (
   4453        lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword)
   4454      ) {
   4455        searchMode.restrictType = "symbol";
   4456      }
   4457      if (entry) {
   4458        searchMode.entry = entry;
   4459      } else {
   4460        switch (result.providerName) {
   4461          case "UrlbarProviderTopSites":
   4462            searchMode.entry = "topsites_urlbar";
   4463            break;
   4464          case "UrlbarProviderTabToSearch":
   4465            if (result.payload.dynamicType) {
   4466              searchMode.entry = "tabtosearch_onboard";
   4467            } else {
   4468              searchMode.entry = "tabtosearch";
   4469            }
   4470            break;
   4471          default:
   4472            searchMode.entry = "keywordoffer";
   4473            break;
   4474        }
   4475      }
   4476    }
   4477 
   4478    return searchMode;
   4479  }
   4480 
   4481  /**
   4482   * Updates the UI so that search mode is either entered or exited.
   4483   *
   4484   * @param {object} searchMode
   4485   *   See setSearchMode documentation.  If null, then search mode is exited.
   4486   */
   4487  _updateSearchModeUI(searchMode) {
   4488    let { engineName, source, isGeneralPurposeEngine } = searchMode || {};
   4489 
   4490    // As an optimization, bail if the given search mode is null but search mode
   4491    // is already inactive. Otherwise, browser_preferences_usage.js fails due to
   4492    // accessing the browser.urlbar.placeholderName pref (via the call to
   4493    // initPlaceHolder below) too many times. That test does not enter search mode,
   4494    // but it triggers many calls to this method with a null search mode, via setURI.
   4495    if (!engineName && !source && !this.hasAttribute("searchmode")) {
   4496      return;
   4497    }
   4498 
   4499    if (this._searchModeIndicatorTitle) {
   4500      this._searchModeIndicatorTitle.textContent = "";
   4501      this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
   4502    }
   4503 
   4504    if (!engineName && !source) {
   4505      this.removeAttribute("searchmode");
   4506      this.initPlaceHolder(true);
   4507      return;
   4508    }
   4509 
   4510    if (this.#isAddressbar) {
   4511      if (engineName) {
   4512        // Set text content for the search mode indicator.
   4513        this._searchModeIndicatorTitle.textContent = engineName;
   4514        this.document.l10n.setAttributes(
   4515          this.inputField,
   4516          isGeneralPurposeEngine
   4517            ? "urlbar-placeholder-search-mode-web-2"
   4518            : "urlbar-placeholder-search-mode-other-engine",
   4519          { name: engineName }
   4520        );
   4521      } else if (source) {
   4522        const messageIDs = {
   4523          actions: "urlbar-placeholder-search-mode-other-actions",
   4524          bookmarks: "urlbar-placeholder-search-mode-other-bookmarks",
   4525          engine: "urlbar-placeholder-search-mode-other-engine",
   4526          history: "urlbar-placeholder-search-mode-other-history",
   4527          tabs: "urlbar-placeholder-search-mode-other-tabs",
   4528        };
   4529        let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
   4530        let l10nID = `urlbar-search-mode-${sourceName}`;
   4531        this.document.l10n.setAttributes(
   4532          this._searchModeIndicatorTitle,
   4533          l10nID
   4534        );
   4535        this.document.l10n.setAttributes(
   4536          this.inputField,
   4537          messageIDs[sourceName]
   4538        );
   4539      }
   4540    }
   4541 
   4542    this.toggleAttribute("searchmode", true);
   4543    // Clear autofill.
   4544    if (this._autofillPlaceholder && this.userTypedValue) {
   4545      this.value = this.userTypedValue;
   4546    }
   4547    // Search mode should only be active when pageproxystate is invalid.
   4548    if (this.getAttribute("pageproxystate") == "valid") {
   4549      this.value = "";
   4550      this.setPageProxyState("invalid", true);
   4551    }
   4552 
   4553    this.searchModeSwitcher?.onSearchModeChanged();
   4554  }
   4555 
   4556  /**
   4557   * Handles persisted search terms logic for the current browser. This manages
   4558   * state and updates the UI accordingly.
   4559   *
   4560   * @param {object} options
   4561   * @param {object} options.state
   4562   *   The state object for the currently viewed browser.
   4563   * @param {boolean} options.hideSearchTerms
   4564   *   True if we must hide the search terms and instead show the page URL.
   4565   * @param {boolean} options.dueToTabSwitch
   4566   *   True if the browser was revealed again due to a tab switch.
   4567   * @param {boolean} options.isSameDocument
   4568   *   True if the page load was same document.
   4569   * @param {nsIURI} [options.uri]
   4570   *   The latest URI of the page.
   4571   * @returns {boolean}
   4572   *   Whether search terms should persist.
   4573   */
   4574  #handlePersistedSearchTerms({
   4575    state,
   4576    hideSearchTerms,
   4577    dueToTabSwitch,
   4578    isSameDocument,
   4579    uri,
   4580  }) {
   4581    if (!this.#isAddressbar) {
   4582      return false;
   4583    }
   4584    if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) {
   4585      if (state.persist) {
   4586        this.removeAttribute("persistsearchterms");
   4587        delete state.persist;
   4588      }
   4589      return false;
   4590    }
   4591 
   4592    // The first time the browser URI has been loaded to the input. If
   4593    // persist is not defined, it is likely due to the tab being created in
   4594    // the background or an existing tab moved to a new window and we have to
   4595    // do the work for the first time.
   4596    let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist;
   4597 
   4598    let cachedUriDidChange =
   4599      state.persist?.originalURI &&
   4600      (!this.window.gBrowser.selectedBrowser.originalURI ||
   4601        !state.persist.originalURI.equals(
   4602          this.window.gBrowser.selectedBrowser.originalURI
   4603        ));
   4604 
   4605    // Capture the shouldPersist property if it exists before
   4606    // setPersistenceState potentially modifies it.
   4607    let wasPersisting = state.persist?.shouldPersist ?? false;
   4608 
   4609    if (firstView || cachedUriDidChange) {
   4610      lazy.UrlbarSearchTermsPersistence.setPersistenceState(
   4611        state,
   4612        this.window.gBrowser.selectedBrowser.originalURI
   4613      );
   4614    }
   4615    let shouldPersist =
   4616      !hideSearchTerms &&
   4617      lazy.UrlbarSearchTermsPersistence.shouldPersist(state, {
   4618        dueToTabSwitch,
   4619        isSameDocument,
   4620        uri: uri ?? this.window.gBrowser.currentURI,
   4621        userTypedValue: this.userTypedValue,
   4622        firstView,
   4623      });
   4624    // When persisting, userTypedValue should have a value consistent with the
   4625    // search terms to mimic a user typing the search terms.
   4626    // When turning off persist, check if the userTypedValue needs to be
   4627    // removed in order for the URL to return to the address bar. Single page
   4628    // application SERPs will load secondary search pages (e.g. Maps, Images)
   4629    // with the same document, which won't unset userTypedValue.
   4630    if (shouldPersist) {
   4631      this.userTypedValue = state.persist.searchTerms;
   4632    } else if (wasPersisting && !shouldPersist) {
   4633      this.userTypedValue = null;
   4634    }
   4635 
   4636    state.persist.shouldPersist = shouldPersist;
   4637    this.toggleAttribute("persistsearchterms", state.persist.shouldPersist);
   4638 
   4639    if (state.persist.shouldPersist && !isSameDocument) {
   4640      Glean.urlbarPersistedsearchterms.viewCount.add(1);
   4641    }
   4642 
   4643    return shouldPersist;
   4644  }
   4645 
   4646  /**
   4647   * Initializes the urlbar placeholder to the pre-saved engine name. We do this
   4648   * via a preference, to avoid needing to synchronously init the search service.
   4649   *
   4650   * This should be called around the time of DOMContentLoaded, so that it is
   4651   * initialized quickly before the user sees anything.
   4652   *
   4653   * Note: If the preference doesn't exist, we don't do anything as the default
   4654   * placeholder is a string which doesn't have the engine name; however, this
   4655   * can be overridden using the `force` parameter.
   4656   *
   4657   * @param {boolean} force If true and the preference doesn't exist, the
   4658   *                        placeholder will be set to the default version
   4659   *                        without an engine name ("Search or enter address").
   4660   */
   4661  initPlaceHolder(force = false) {
   4662    if (!this.#isAddressbar) {
   4663      return;
   4664    }
   4665 
   4666    let prefName =
   4667      "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : "");
   4668    let engineName = Services.prefs.getStringPref(prefName, "");
   4669    if (engineName || force) {
   4670      // We can do this directly, since we know we're at DOMContentLoaded.
   4671      this._setPlaceholder(engineName || null);
   4672    }
   4673  }
   4674 
   4675  /**
   4676   * Asynchronously changes the urlbar placeholder to the name of the default
   4677   * engine according to the search service when it is initialized.
   4678   *
   4679   * This should be called around the time of MozAfterPaint. Since the
   4680   * placeholder was already initialized to the pre-saved engine name by
   4681   * initPlaceHolder when this is called, the update is delayed to avoid
   4682   * confusing the user.
   4683   */
   4684  async delayedStartupInit() {
   4685    // Only delay if requested, and we're not displaying text in the URL bar
   4686    // currently.
   4687    if (!this.value) {
   4688      // Delays changing the URL Bar placeholder and Unified Search Button icon
   4689      // until the user is not going to be seeing it, e.g. when there is a value
   4690      // entered in the bar, or if there is a tab switch to a tab which has a url
   4691      // loaded. We delay the update until the user is out of search mode since
   4692      // an alternative placeholder is used in search mode.
   4693      let updateListener = () => {
   4694        if (this.value && !this.searchMode) {
   4695          // By the time the user has switched, they may have changed the engine
   4696          // again, so we need to call this function again but with the
   4697          // new engine name.
   4698          // No need to await for this to finish, we're in a listener here anyway.
   4699          this.searchModeSwitcher.updateSearchIcon();
   4700          this._updatePlaceholderFromDefaultEngine();
   4701          this.inputField.removeEventListener("input", updateListener);
   4702          this.window.gBrowser.tabContainer.removeEventListener(
   4703            "TabSelect",
   4704            updateListener
   4705          );
   4706        }
   4707      };
   4708 
   4709      this.inputField.addEventListener("input", updateListener);
   4710      this.window.gBrowser.tabContainer.addEventListener(
   4711        "TabSelect",
   4712        updateListener
   4713      );
   4714    } else {
   4715      await this._updatePlaceholderFromDefaultEngine();
   4716    }
   4717 
   4718    // If we haven't finished initializing, ensure the placeholder
   4719    // preference is set for the next startup.
   4720    if (this.#isAddressbar) {
   4721      lazy.SearchUIUtils.updatePlaceholderNamePreference(
   4722        await this._getDefaultSearchEngine(),
   4723        this.isPrivate
   4724      );
   4725    }
   4726  }
   4727 
   4728  /**
   4729   * Set Unified Search Button availability.
   4730   *
   4731   * @param {boolean} available If true Unified Search Button will be available.
   4732   */
   4733  setUnifiedSearchButtonAvailability(available) {
   4734    if (this.#isSmartbarMode) {
   4735      return;
   4736    }
   4737    this.toggleAttribute("unifiedsearchbutton-available", available);
   4738    this.getBrowserState(
   4739      this.window.gBrowser.selectedBrowser
   4740    ).isUnifiedSearchButtonAvailable = available;
   4741  }
   4742 
   4743  /**
   4744   * Returns a Promise that resolves with default search engine.
   4745   *
   4746   * @returns {Promise<nsISearchEngine>}
   4747   */
   4748  _getDefaultSearchEngine() {
   4749    return this.isPrivate
   4750      ? Services.search.getDefaultPrivate()
   4751      : Services.search.getDefault();
   4752  }
   4753 
   4754  /**
   4755   * This is a wrapper around '_updatePlaceholder' that uses the appropriate
   4756   * default engine to get the engine name.
   4757   */
   4758  async _updatePlaceholderFromDefaultEngine() {
   4759    const defaultEngine = await this._getDefaultSearchEngine();
   4760    this._updatePlaceholder(defaultEngine.name);
   4761  }
   4762 
   4763  /**
   4764   * Updates the URLBar placeholder for the specified engine, delaying the
   4765   * update if required.
   4766   *
   4767   * Note: The engine name will only be displayed for application-provided
   4768   * engines, as we know they should have short names.
   4769   *
   4770   * @param {string}  engineName     The search engine name to use for the update.
   4771   */
   4772  _updatePlaceholder(engineName) {
   4773    if (!engineName) {
   4774      throw new Error("Expected an engineName to be specified");
   4775    }
   4776 
   4777    if (this.searchMode || !this.#isAddressbar) {
   4778      return;
   4779    }
   4780 
   4781    let engine = Services.search.getEngineByName(engineName);
   4782    if (engine.isConfigEngine) {
   4783      this._setPlaceholder(engineName);
   4784    } else {
   4785      // Display the default placeholder string.
   4786      this._setPlaceholder(null);
   4787    }
   4788  }
   4789 
   4790  /**
   4791   * Sets the URLBar placeholder to either something based on the engine name,
   4792   * or the default placeholder.
   4793   *
   4794   * @param {?string} engineName
   4795   * The name of the engine or null to use the default placeholder.
   4796   */
   4797  _setPlaceholder(engineName) {
   4798    if (this.#isSmartbarMode) {
   4799      this.document.l10n.setAttributes(this.inputField, "smartbar-placeholder");
   4800      return;
   4801    }
   4802 
   4803    if (!this.#isAddressbar) {
   4804      this.document.l10n.setAttributes(this.inputField, "searchbar-input");
   4805      return;
   4806    }
   4807 
   4808    let l10nId;
   4809    if (lazy.UrlbarPrefs.get("keyword.enabled")) {
   4810      l10nId = engineName
   4811        ? "urlbar-placeholder-with-name"
   4812        : "urlbar-placeholder";
   4813    } else {
   4814      l10nId = "urlbar-placeholder-keyword-disabled";
   4815    }
   4816 
   4817    this.document.l10n.setAttributes(
   4818      this.inputField,
   4819      l10nId,
   4820      l10nId == "urlbar-placeholder-with-name"
   4821        ? { name: engineName }
   4822        : undefined
   4823    );
   4824  }
   4825 
   4826  /**
   4827   * Determines if we should select all the text in the Urlbar based on the
   4828   *  Urlbar state, and whether the selection is empty.
   4829   */
   4830  #maybeSelectAll() {
   4831    if (
   4832      !this._preventClickSelectsAll &&
   4833      this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
   4834      this.focused &&
   4835      this.selectionStart == this.selectionEnd
   4836    ) {
   4837      this.select();
   4838    }
   4839  }
   4840 
   4841  // Event handlers below.
   4842 
   4843  _on_command(event) {
   4844    // Something is executing a command, likely causing a focus change. This
   4845    // should not be recorded as an abandonment. If the user is selecting a
   4846    // result menu item or entering search mode from a one-off, then they are
   4847    // in the same engagement and we should not discard.
   4848    if (
   4849      !event.target.classList.contains("urlbarView-result-menuitem") &&
   4850      (!event.target.classList.contains("searchbar-engine-one-off-item") ||
   4851        this.searchMode?.entry != "oneoff")
   4852    ) {
   4853      this.controller.engagementEvent.discard();
   4854    }
   4855  }
   4856 
   4857  _on_blur(event) {
   4858    lazy.logger.debug("Blur Event");
   4859    // We cannot count every blur events after a missed engagement as abandoment
   4860    // because the user may have clicked on some view element that executes
   4861    // a command causing a focus change. For example opening preferences from
   4862    // the oneoff settings button.
   4863    // For now we detect that case by discarding the event on command, but we
   4864    // may want to figure out a more robust way to detect abandonment.
   4865    this.controller.engagementEvent.record(event, {
   4866      searchString: this._lastSearchString,
   4867      searchSource: this.getSearchSource(event),
   4868    });
   4869 
   4870    this.focusedViaMousedown = false;
   4871    this._handoffSession = undefined;
   4872    this._isHandoffSession = false;
   4873    this.removeAttribute("focused");
   4874 
   4875    if (this._autofillPlaceholder && this.userTypedValue) {
   4876      // If we were autofilling, remove the autofilled portion, by restoring
   4877      // the value to the last typed one.
   4878      this.value = this.userTypedValue;
   4879    } else if (
   4880      this.value == this._untrimmedValue &&
   4881      !this.userTypedValue &&
   4882      !this.focused
   4883    ) {
   4884      // If the value was untrimmed by _on_focus and didn't change, trim it.
   4885      this.value = this._untrimmedValue;
   4886    } else {
   4887      // We're not updating the value, so just format it.
   4888      this.formatValue();
   4889    }
   4890 
   4891    this._resetSearchState();
   4892 
   4893    // In certain cases, like holding an override key and confirming an entry,
   4894    // we don't key a keyup event for the override key, thus we make this
   4895    // additional cleanup on blur.
   4896    this._clearActionOverride();
   4897 
   4898    // The extension input sessions depends more on blur than on the fact we
   4899    // actually cancel a running query, so we do it here.
   4900    if (lazy.ExtensionSearchHandler.hasActiveInputSession()) {
   4901      lazy.ExtensionSearchHandler.handleInputCancelled();
   4902    }
   4903 
   4904    // Respect the autohide preference for easier inspecting/debugging via
   4905    // the browser toolbox.
   4906    if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   4907      this.view.close();
   4908    }
   4909 
   4910    // We may have hidden popup notifications, show them again if necessary.
   4911    if (
   4912      this.getAttribute("pageproxystate") != "valid" &&
   4913      this.window.UpdatePopupNotificationsVisibility
   4914    ) {
   4915      this.window.UpdatePopupNotificationsVisibility();
   4916    }
   4917 
   4918    // If user move the focus to another component while pressing Enter key,
   4919    // then keyup at that component, as we can't get the event, clear the promise.
   4920    if (this._keyDownEnterDeferred) {
   4921      this._keyDownEnterDeferred.resolve();
   4922      this._keyDownEnterDeferred = null;
   4923    }
   4924    this._isKeyDownWithCtrl = false;
   4925    this._isKeyDownWithMeta = false;
   4926    this._isKeyDownWithMetaAndLeft = false;
   4927 
   4928    Services.obs.notifyObservers(null, "urlbar-blur");
   4929  }
   4930 
   4931  _on_click(event) {
   4932    switch (event.target) {
   4933      case this.inputField:
   4934      case this._inputContainer:
   4935        this.#maybeSelectAll();
   4936        this.#maybeUntrimUrl();
   4937        break;
   4938 
   4939      case this._searchModeIndicatorClose:
   4940        if (event.button != 2) {
   4941          this.searchMode = null;
   4942          if (this.view.oneOffSearchButtons) {
   4943            this.view.oneOffSearchButtons.selectedButton = null;
   4944          }
   4945          if (this.view.isOpen) {
   4946            this.startQuery({
   4947              event,
   4948            });
   4949          }
   4950        }
   4951        break;
   4952 
   4953      case this._revertButton:
   4954        this.handleRevert();
   4955        this.select();
   4956        break;
   4957 
   4958      case this.goButton:
   4959        this.handleCommand(event);
   4960        break;
   4961    }
   4962  }
   4963 
   4964  _on_contextmenu(event) {
   4965    this.#lazy.addSearchEngineHelper.refreshContextMenu(event);
   4966 
   4967    // Context menu opened via keyboard shortcut.
   4968    if (!event.button) {
   4969      return;
   4970    }
   4971 
   4972    this.#maybeSelectAll();
   4973  }
   4974 
   4975  _on_focus(event) {
   4976    lazy.logger.debug("Focus Event");
   4977    if (!this._hideFocus) {
   4978      this.toggleAttribute("focused", true);
   4979    }
   4980 
   4981    // If the value was trimmed, check whether we should untrim it.
   4982    // This is necessary when a protocol was typed, but the whole url has
   4983    // invalid parts, like the origin, then editing and confirming the trimmed
   4984    // value would execute a search instead of visiting the typed url.
   4985    if (this._protocolIsTrimmed) {
   4986      let untrim = false;
   4987      let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
   4988      if (fixedURI) {
   4989        try {
   4990          let expectedURI = Services.io.newURI(this._untrimmedValue);
   4991          if (
   4992            lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") &&
   4993            this._untrimmedValue.startsWith("https://")
   4994          ) {
   4995            untrim =
   4996              fixedURI.displaySpec.replace("http://", "https://") !=
   4997              expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI.
   4998          } else {
   4999            untrim = fixedURI.displaySpec != expectedURI.displaySpec;
   5000          }
   5001        } catch (ex) {
   5002          untrim = true;
   5003        }
   5004      }
   5005      if (untrim) {
   5006        this._setValue(this._untrimmedValue);
   5007      }
   5008    }
   5009 
   5010    if (this.focusedViaMousedown) {
   5011      this.view.autoOpen({ event });
   5012    } else {
   5013      if (this._untrimOnFocusAfterKeydown) {
   5014        // While the mousedown focus has more complex implications due to drag
   5015        // and double-click select, we can untrim immediately when the urlbar is
   5016        // focused by a keyboard shortcut.
   5017        this.#maybeUntrimUrl({ ignoreSelection: true });
   5018      }
   5019 
   5020      if (this.inputField.hasAttribute("refocused-by-panel")) {
   5021        this.#maybeSelectAll();
   5022      }
   5023    }
   5024 
   5025    this._updateUrlTooltip();
   5026    this.formatValue();
   5027 
   5028    // Hide popup notifications, to reduce visual noise.
   5029    if (
   5030      this.getAttribute("pageproxystate") != "valid" &&
   5031      this.window.UpdatePopupNotificationsVisibility
   5032    ) {
   5033      this.window.UpdatePopupNotificationsVisibility();
   5034    }
   5035 
   5036    Services.obs.notifyObservers(null, "urlbar-focus");
   5037  }
   5038 
   5039  _on_mouseover() {
   5040    this._updateUrlTooltip();
   5041  }
   5042 
   5043  _on_draggableregionleftmousedown() {
   5044    if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   5045      this.view.close();
   5046    }
   5047  }
   5048 
   5049  _on_mousedown(event) {
   5050    switch (event.currentTarget) {
   5051      case this: {
   5052        this._mousedownOnUrlbarDescendant = true;
   5053        if (
   5054          event.composedTarget != this.inputField &&
   5055          event.composedTarget != this._inputContainer
   5056        ) {
   5057          break;
   5058        }
   5059 
   5060        this.focusedViaMousedown = !this.focused;
   5061        this._preventClickSelectsAll = this.focused;
   5062 
   5063        // Keep the focus status, since the attribute may be changed
   5064        // upon calling this.focus().
   5065        const hasFocus = this.hasAttribute("focused");
   5066        if (event.composedTarget != this.inputField) {
   5067          this.focus();
   5068        }
   5069 
   5070        // The rest of this case only cares about left clicks.
   5071        if (event.button != 0) {
   5072          break;
   5073        }
   5074 
   5075        // Clear any previous selection unless we are focused, to ensure it
   5076        // doesn't affect drag selection.
   5077        if (this.focusedViaMousedown) {
   5078          this.setSelectionRange(0, 0);
   5079        }
   5080 
   5081        // Do not suppress the focus border if we are already focused. If we
   5082        // did, we'd hide the focus border briefly then show it again if the
   5083        // user has Top Sites disabled, creating a flashing effect.
   5084        this.view.autoOpen({
   5085          event,
   5086          suppressFocusBorder: !hasFocus,
   5087        });
   5088        break;
   5089      }
   5090      case this.window:
   5091        if (this._mousedownOnUrlbarDescendant) {
   5092          this._mousedownOnUrlbarDescendant = false;
   5093          break;
   5094        }
   5095        // Don't close the view when clicking on a tab; we may want to keep the
   5096        // view open on tab switch, and the TabSelect event arrived earlier.
   5097        if (event.target.closest("tab")) {
   5098          break;
   5099        }
   5100 
   5101        // Close the view when clicking on toolbars and other UI pieces that
   5102        // might not automatically remove focus from the input.
   5103        // Respect the autohide preference for easier inspecting/debugging via
   5104        // the browser toolbox.
   5105        if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   5106          if (this.view.isOpen && !this.hasAttribute("focused")) {
   5107            // In this case, as blur event never happen from the inputField, we
   5108            // record abandonment event explicitly.
   5109            let blurEvent = new FocusEvent("blur", {
   5110              relatedTarget: this.inputField,
   5111            });
   5112            this.controller.engagementEvent.record(blurEvent, {
   5113              searchString: this._lastSearchString,
   5114              searchSource: this.getSearchSource(blurEvent),
   5115            });
   5116          }
   5117 
   5118          this.view.close();
   5119        }
   5120        break;
   5121    }
   5122  }
   5123 
   5124  _on_input(event) {
   5125    if (
   5126      this._autofillPlaceholder &&
   5127      this.value === this.userTypedValue &&
   5128      (event.inputType === "deleteContentBackward" ||
   5129        event.inputType === "deleteContentForward")
   5130    ) {
   5131      // Take a telemetry if user deleted whole autofilled value.
   5132      Glean.urlbar.autofillDeletion.add(1);
   5133    }
   5134 
   5135    let value = this.value;
   5136    this.valueIsTyped = true;
   5137    this._untrimmedValue = value;
   5138    this._protocolIsTrimmed = false;
   5139    this._resultForCurrentValue = null;
   5140 
   5141    this.userTypedValue = value;
   5142    // Unset userSelectionBehavior because the user is modifying the search
   5143    // string, thus there's no valid selection. This is also used by the view
   5144    // to set "aria-activedescendant", thus it should never get stale.
   5145    this.controller.userSelectionBehavior = "none";
   5146 
   5147    let compositionState = this.#compositionState;
   5148    let compositionClosedPopup = this.#compositionClosedPopup;
   5149 
   5150    // Clear composition values if we're no more composing.
   5151    if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   5152      this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
   5153      this.#compositionClosedPopup = false;
   5154    }
   5155 
   5156    this.toggleAttribute("usertyping", value);
   5157    this.removeAttribute("actiontype");
   5158 
   5159    if (
   5160      this.getAttribute("pageproxystate") == "valid" &&
   5161      this.value != this._lastValidURLStr
   5162    ) {
   5163      this.setPageProxyState("invalid", true);
   5164    }
   5165 
   5166    if (this.#isAddressbar) {
   5167      let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   5168      if (
   5169        state.persist?.shouldPersist &&
   5170        this.value !== state.persist.searchTerms
   5171      ) {
   5172        state.persist.shouldPersist = false;
   5173        this.removeAttribute("persistsearchterms");
   5174      }
   5175    }
   5176 
   5177    if (this.view.isOpen) {
   5178      if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) {
   5179        // SmartbarView rolls up all popups when it opens, but we should
   5180        // do the same for SmartbarInput when it's already open in case
   5181        // a tab preview was opened
   5182        this.window.docShell.treeOwner
   5183          .QueryInterface(Ci.nsIInterfaceRequestor)
   5184          .getInterface(Ci.nsIAppWindow)
   5185          .rollupAllPopups();
   5186      }
   5187      if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) {
   5188        this.view.clear();
   5189        if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) {
   5190          this.view.close();
   5191          return;
   5192        }
   5193      }
   5194    } else {
   5195      this.view.clear();
   5196    }
   5197 
   5198    this.view.removeAccessibleFocus();
   5199 
   5200    // During composition with an IME, the following events happen in order:
   5201    // 1. a compositionstart event
   5202    // 2. some input events
   5203    // 3. a compositionend event
   5204    // 4. an input event
   5205 
   5206    // We should do nothing during composition or if composition was canceled
   5207    // and we didn't close the popup on composition start.
   5208    if (
   5209      !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") &&
   5210      (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING ||
   5211        (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED &&
   5212          !compositionClosedPopup))
   5213    ) {
   5214      return;
   5215    }
   5216 
   5217    // Autofill only when text is inserted (i.e., event.data is not empty) and
   5218    // it's not due to pasting.
   5219    const allowAutofill =
   5220      (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") ||
   5221        compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) &&
   5222      !!event.data &&
   5223      !lazy.UrlbarUtils.isPasteEvent(event) &&
   5224      this._maybeAutofillPlaceholder(value);
   5225 
   5226    this.startQuery({
   5227      searchString: value,
   5228      allowAutofill,
   5229      resetSearchState: false,
   5230      event,
   5231    });
   5232  }
   5233 
   5234  _on_selectionchange() {
   5235    // Confirm placeholder as user text if it gets explicitly deselected. This
   5236    // happens when the user wants to modify the autofilled text by either
   5237    // clicking on it, or pressing HOME, END, RIGHT, …
   5238    if (
   5239      this._autofillPlaceholder &&
   5240      this._autofillPlaceholder.value == this.value &&
   5241      (this._autofillPlaceholder.selectionStart != this.selectionStart ||
   5242        this._autofillPlaceholder.selectionEnd != this.selectionEnd)
   5243    ) {
   5244      this._autofillPlaceholder = null;
   5245      this.userTypedValue = this.value;
   5246    }
   5247  }
   5248 
   5249  _on_select() {
   5250    // On certain user input, AutoCopyListener::OnSelectionChange() updates
   5251    // the primary selection with user-selected text (when supported).
   5252    // Selection::NotifySelectionListeners() then dispatches a "select" event
   5253    // under similar conditions via TextInputListener::OnSelectionChange().
   5254    // This event is received here in order to replace the primary selection
   5255    // from the editor with text having the adjustments of
   5256    // _getSelectedValueForClipboard(), such as adding the scheme for the url.
   5257    //
   5258    // Other "select" events are also received, however, and must be excluded.
   5259    if (
   5260      // _suppressPrimaryAdjustment is set during select().  Don't update
   5261      // the primary selection because that is not the intent of user input,
   5262      // which may be new tab or urlbar focus.
   5263      this._suppressPrimaryAdjustment ||
   5264      // The check on isHandlingUserInput filters out async "select" events
   5265      // from setSelectionRange(), which occur when autofill text is selected.
   5266      !this.window.windowUtils.isHandlingUserInput ||
   5267      !Services.clipboard.isClipboardTypeSupported(
   5268        Services.clipboard.kSelectionClipboard
   5269      )
   5270    ) {
   5271      return;
   5272    }
   5273 
   5274    let val = this._getSelectedValueForClipboard();
   5275    if (!val) {
   5276      return;
   5277    }
   5278 
   5279    lazy.ClipboardHelper.copyStringToClipboard(
   5280      val,
   5281      Services.clipboard.kSelectionClipboard
   5282    );
   5283  }
   5284 
   5285  _on_overflow(event) {
   5286    const targetIsPlaceholder =
   5287      event.originalTarget.implementedPseudoElement == "::placeholder";
   5288    // We only care about the non-placeholder text.
   5289    // This shouldn't be needed, see bug 1487036.
   5290    if (targetIsPlaceholder) {
   5291      return;
   5292    }
   5293    this._overflowing = true;
   5294    this.updateTextOverflow();
   5295  }
   5296 
   5297  _on_underflow(event) {
   5298    const targetIsPlaceholder =
   5299      event.originalTarget.implementedPseudoElement == "::placeholder";
   5300    // We only care about the non-placeholder text.
   5301    // This shouldn't be needed, see bug 1487036.
   5302    if (targetIsPlaceholder) {
   5303      return;
   5304    }
   5305    this._overflowing = false;
   5306 
   5307    this.updateTextOverflow();
   5308 
   5309    this._updateUrlTooltip();
   5310  }
   5311 
   5312  _on_paste(event) {
   5313    let originalPasteData = event.clipboardData.getData("text/plain");
   5314    if (!originalPasteData) {
   5315      return;
   5316    }
   5317 
   5318    let oldValue = this.value;
   5319    let oldStart = oldValue.substring(0, this.selectionStart);
   5320    // If there is already non-whitespace content in the URL bar
   5321    // preceding the pasted content, it's not necessary to check
   5322    // protocols used by the pasted content:
   5323    if (oldStart.trim()) {
   5324      return;
   5325    }
   5326    let oldEnd = oldValue.substring(this.selectionEnd);
   5327 
   5328    const pasteData = this.sanitizeTextFromClipboard(originalPasteData);
   5329 
   5330    if (originalPasteData != pasteData) {
   5331      // Unfortunately we're not allowed to set the bits being pasted
   5332      // so cancel this event:
   5333      event.preventDefault();
   5334      event.stopImmediatePropagation();
   5335 
   5336      const value = oldStart + pasteData + oldEnd;
   5337      this._setValue(value, { valueIsTyped: true });
   5338      this.userTypedValue = value;
   5339 
   5340      // Since we prevent the default paste event, we have to ensure the
   5341      // pageproxystate is updated. The paste event replaces the actual current
   5342      // page's URL with user-typed content, so we should set pageproxystate to
   5343      // invalid.
   5344      if (this.getAttribute("pageproxystate") == "valid") {
   5345        this.setPageProxyState("invalid");
   5346      }
   5347      this.toggleAttribute("usertyping", this._untrimmedValue);
   5348 
   5349      // Fix up cursor/selection:
   5350      let newCursorPos = oldStart.length + pasteData.length;
   5351      this.setSelectionRange(newCursorPos, newCursorPos);
   5352 
   5353      this.startQuery({
   5354        searchString: this.value,
   5355        allowAutofill: false,
   5356        resetSearchState: false,
   5357        event,
   5358      });
   5359    }
   5360  }
   5361 
   5362  /**
   5363   * Sanitize and process data retrieved from the clipboard
   5364   *
   5365   * @param {string} clipboardData
   5366   *   The original data retrieved from the clipboard.
   5367   * @returns {string}
   5368   *   The sanitized paste data, ready to use.
   5369   */
   5370  sanitizeTextFromClipboard(clipboardData) {
   5371    let fixedURI, keywordAsSent;
   5372    try {
   5373      ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo(
   5374        clipboardData,
   5375        Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   5376          Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
   5377      ));
   5378    } catch (e) {}
   5379 
   5380    let pasteData;
   5381    if (keywordAsSent) {
   5382      // For performance reasons, we don't want to beautify a long string.
   5383      if (clipboardData.length < 500) {
   5384        // For only keywords, replace any white spaces including line break
   5385        // with white space.
   5386        pasteData = clipboardData.replace(/\s/g, " ");
   5387      } else {
   5388        pasteData = clipboardData;
   5389      }
   5390    } else if (
   5391      fixedURI?.scheme == "data" &&
   5392      !fixedURI.spec.match(/^data:.+;base64,/)
   5393    ) {
   5394      // For data url without base64, replace line break with white space.
   5395      pasteData = clipboardData.replace(/[\r\n]/g, " ");
   5396    } else {
   5397      // For normal url or data url having basic64, or if fixup failed, just
   5398      // remove line breaks.
   5399      pasteData = clipboardData.replace(/[\r\n]/g, "");
   5400    }
   5401 
   5402    return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData);
   5403  }
   5404 
   5405  /**
   5406   * Generate a UrlbarQueryContext from the current context.
   5407   *
   5408   * @param {object} [options]
   5409   *   Optional params
   5410   * @param {boolean} [options.allowAutofill]
   5411   *   Whether autofill is enabled.
   5412   * @param {string} [options.searchString]
   5413   *   The string being searched.
   5414   * @param {object} [options.event]
   5415   *   The event triggering the query.
   5416   * @returns {UrlbarQueryContext}
   5417   *   The queryContext object.
   5418   */
   5419  #makeQueryContext({
   5420    allowAutofill = true,
   5421    searchString = null,
   5422    event = null,
   5423  } = {}) {
   5424    // When we are in actions search mode we can show more results so
   5425    // increase the limit.
   5426    let maxResults =
   5427      this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS
   5428        ? lazy.UrlbarPrefs.get("maxRichResults")
   5429        : UNLIMITED_MAX_RESULTS;
   5430    let options = {
   5431      allowAutofill,
   5432      isPrivate: this.isPrivate,
   5433      sapName: this.sapName,
   5434      maxResults,
   5435      searchString,
   5436      prohibitRemoteResults:
   5437        event &&
   5438        lazy.UrlbarUtils.isPasteEvent(event) &&
   5439        lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") <
   5440          event.data?.length,
   5441    };
   5442 
   5443    // Only add gBrowser-dependent properties if gBrowser exists.
   5444    if (this.window.gBrowser) {
   5445      options.userContextId = parseInt(
   5446        this.window.gBrowser.selectedBrowser?.getAttribute("usercontextid") ?? 0
   5447      );
   5448      options.tabGroup = this.window.gBrowser.selectedTab.group?.id ?? null;
   5449      options.currentPage = this.window.gBrowser.currentURI?.spec ?? "";
   5450    }
   5451 
   5452    if (this.searchMode) {
   5453      options.searchMode = this.searchMode;
   5454      if (this.searchMode.source) {
   5455        options.sources = [this.searchMode.source];
   5456      }
   5457    }
   5458 
   5459    return new lazy.UrlbarQueryContext(options);
   5460  }
   5461 
   5462  _on_scrollend() {
   5463    this.updateTextOverflow();
   5464  }
   5465 
   5466  _on_TabSelect() {
   5467    // TabSelect may be activated by a keyboard shortcut and cause the urlbar
   5468    // to take focus, in this case we should not untrim.
   5469    this._untrimOnFocusAfterKeydown = false;
   5470    this._gotTabSelect = true;
   5471    this._afterTabSelectAndFocusChange();
   5472  }
   5473 
   5474  _on_TabClose(event) {
   5475    this.controller.engagementEvent.handleBounceEventTrigger(
   5476      event.target.linkedBrowser
   5477    );
   5478 
   5479    if (this.view.isOpen) {
   5480      // Refresh results when a tab is closed while the results view is open.
   5481      // This prevents switch-to-tab results from remaining in the results
   5482      // list after their tab is closed.
   5483      this.startQuery();
   5484    }
   5485  }
   5486 
   5487  _on_beforeinput(event) {
   5488    if (event.data && this._keyDownEnterDeferred) {
   5489      // Ignore char key input while processing enter key.
   5490      event.preventDefault();
   5491    }
   5492  }
   5493 
   5494  _on_keydown(event) {
   5495    if (event.currentTarget == this.window) {
   5496      // It would be great if we could more easily detect the user focusing the
   5497      // address bar through a keyboard shortcut, but F6 and TAB bypass are
   5498      // not going through commands handling.
   5499      // Also note we'll unset this on TabSelect, as it can focus the address
   5500      // bar but we should not untrim in that case.
   5501      this._untrimOnFocusAfterKeydown = !this.focused;
   5502      return;
   5503    }
   5504 
   5505    if (
   5506      this.#isSmartbarMode &&
   5507      event.keyCode === KeyEvent.DOM_VK_RETURN &&
   5508      event.shiftKey
   5509    ) {
   5510      event.preventDefault();
   5511      return;
   5512    }
   5513 
   5514    if (!this.controller) {
   5515      return;
   5516    }
   5517 
   5518    // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if
   5519    // not properly handled, so let's first handle things that should not be
   5520    // evaluated repeatedly.
   5521    if (!event.repeat) {
   5522      this.#allTextSelectedOnKeyDown = this.#allTextSelected;
   5523 
   5524      if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
   5525        if (this._keyDownEnterDeferred) {
   5526          this._keyDownEnterDeferred.reject();
   5527        }
   5528        this._keyDownEnterDeferred = Promise.withResolvers();
   5529        event._disableCanonization =
   5530          AppConstants.platform == "macosx"
   5531            ? this._isKeyDownWithMeta
   5532            : this._isKeyDownWithCtrl;
   5533      }
   5534 
   5535      // Now set the keydown trackers for the current event, anything that wants
   5536      // to check the previous events should have happened before this point.
   5537      // The previously value is persisted until keyup, as we check if the
   5538      // modifiers were down, even if other keys are pressed in the meanwhile.
   5539      if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) {
   5540        this._isKeyDownWithCtrl = true;
   5541      }
   5542      if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) {
   5543        this._isKeyDownWithMeta = true;
   5544      }
   5545      // This is used in keyup, so it can be set every time.
   5546      this._isKeyDownWithMetaAndLeft =
   5547        this._isKeyDownWithMeta &&
   5548        !event.shiftKey &&
   5549        event.keyCode == KeyEvent.DOM_VK_LEFT;
   5550 
   5551      this._toggleActionOverride(event);
   5552    }
   5553 
   5554    // Due to event deferring, it's possible preventDefault() won't be invoked
   5555    // soon enough to actually prevent some of the default behaviors, thus we
   5556    // have to handle the event "twice". This first immediate call passes false
   5557    // as second argument so that handleKeyNavigation will only simulate the
   5558    // event handling, without actually executing actions.
   5559    // TODO (Bug 1541806): improve this handling, maybe by delaying actions
   5560    // instead of events.
   5561    if (this.eventBufferer.shouldDeferEvent(event)) {
   5562      this.controller.handleKeyNavigation(event, false);
   5563    }
   5564    this.eventBufferer.maybeDeferEvent(event, () => {
   5565      this.controller.handleKeyNavigation(event);
   5566    });
   5567  }
   5568 
   5569  async _on_keyup(event) {
   5570    if (event.currentTarget == this.window) {
   5571      this._untrimOnFocusAfterKeydown = false;
   5572      return;
   5573    }
   5574 
   5575    if (this.#allTextSelectedOnKeyDown) {
   5576      let moveCursorToStart = this.#isHomeKeyUpEvent(event);
   5577      // We must set the selection immediately because:
   5578      //  - on Mac Fn + Left is not handled properly as Home
   5579      //  - untrim depends on text not being fully selected.
   5580      if (moveCursorToStart) {
   5581        this.selectionStart = this.selectionEnd = 0;
   5582      }
   5583      this.#maybeUntrimUrl({ moveCursorToStart });
   5584    }
   5585    if (event.keyCode === KeyEvent.DOM_VK_META) {
   5586      this._isKeyDownWithMeta = false;
   5587      this._isKeyDownWithMetaAndLeft = false;
   5588    }
   5589    if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
   5590      this._isKeyDownWithCtrl = false;
   5591    }
   5592 
   5593    this._toggleActionOverride(event);
   5594 
   5595    // Pressing Enter key while pressing Meta key, and next, even when releasing
   5596    // Enter key before releasing Meta key, the keyup event is not fired.
   5597    // Therefore, if Enter keydown is detecting, continue the post processing
   5598    // for Enter key when any keyup event is detected.
   5599    if (this._keyDownEnterDeferred) {
   5600      if (this._keyDownEnterDeferred.loadedContent) {
   5601        try {
   5602          const loadingBrowser = await this._keyDownEnterDeferred.promise;
   5603          // Ensure the selected browser didn't change in the meanwhile.
   5604          if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
   5605            loadingBrowser.focus();
   5606            // Make sure the domain name stays visible for spoof protection and usability.
   5607            this.setSelectionRange(0, 0);
   5608          }
   5609        } catch (ex) {
   5610          // Not all the Enter actions in the urlbar will cause a navigation, then it
   5611          // is normal for this to be rejected.
   5612          // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
   5613          // to ensure not overwriting the new value created by keydown.
   5614        }
   5615      } else {
   5616        // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately.
   5617        this._keyDownEnterDeferred.resolve();
   5618      }
   5619 
   5620      this._keyDownEnterDeferred = null;
   5621    }
   5622  }
   5623 
   5624  _on_compositionstart() {
   5625    if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   5626      throw new Error("Trying to start a nested composition?");
   5627    }
   5628    this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING;
   5629 
   5630    if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
   5631      return;
   5632    }
   5633 
   5634    // Close the view. This will also stop searching.
   5635    if (this.view.isOpen) {
   5636      // We're closing the view, but we want to retain search mode if the
   5637      // selected result was previewing it.
   5638      if (this.searchMode) {
   5639        // If we entered search mode with an empty string, clear userTypedValue,
   5640        // otherwise confirmSearchMode may try to set it as value.
   5641        // This can happen for example if we entered search mode typing a
   5642        // a partial engine domain and selecting a tab-to-search result.
   5643        if (!this.value) {
   5644          this.userTypedValue = null;
   5645        }
   5646        this.confirmSearchMode();
   5647      }
   5648      this.#compositionClosedPopup = true;
   5649      this.view.close();
   5650    } else {
   5651      this.#compositionClosedPopup = false;
   5652    }
   5653  }
   5654 
   5655  _on_compositionend(event) {
   5656    if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   5657      throw new Error("Trying to stop a non existing composition?");
   5658    }
   5659 
   5660    if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
   5661      // Clear the selection and the cached result, since they refer to the
   5662      // state before this composition. A new input even will be generated
   5663      // after this.
   5664      this.view.clearSelection();
   5665      this._resultForCurrentValue = null;
   5666    }
   5667 
   5668    // We can't yet retrieve the committed value from the editor, since it isn't
   5669    // completely committed yet. We'll handle it at the next input event.
   5670    this.#compositionState = event.data
   5671      ? lazy.UrlbarUtils.COMPOSITION.COMMIT
   5672      : lazy.UrlbarUtils.COMPOSITION.CANCELED;
   5673  }
   5674 
   5675  _on_dragstart(event) {
   5676    // Drag only if the gesture starts from the input field.
   5677    let nodePosition = this.inputField.compareDocumentPosition(
   5678      event.originalTarget
   5679    );
   5680    if (
   5681      event.target != this.inputField &&
   5682      !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
   5683    ) {
   5684      return;
   5685    }
   5686 
   5687    // Don't cover potential drop targets on the toolbars or in content.
   5688    this.view.close();
   5689 
   5690    // Only customize the drag data if the entire value is selected and it's a
   5691    // loaded URI. Use default behavior otherwise.
   5692    if (
   5693      !this.#allTextSelected ||
   5694      this.getAttribute("pageproxystate") != "valid"
   5695    ) {
   5696      return;
   5697    }
   5698 
   5699    let uri = this.makeURIReadable(this.window.gBrowser.currentURI);
   5700    let href = uri.displaySpec;
   5701    let title = this.window.gBrowser.contentTitle || href;
   5702 
   5703    event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
   5704    event.dataTransfer.setData("text/plain", href);
   5705    event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
   5706    event.dataTransfer.effectAllowed = "copyLink";
   5707    event.stopPropagation();
   5708  }
   5709 
   5710  /**
   5711   * Handles dragover events for the input.
   5712   *
   5713   * @param {DragEvent} event
   5714   */
   5715  _on_dragover(event) {
   5716    if (!getDroppableData(event)) {
   5717      event.dataTransfer.dropEffect = "none";
   5718    }
   5719  }
   5720 
   5721  /**
   5722   * Handles dropping of data on the input.
   5723   *
   5724   * @param {DragEvent} event
   5725   */
   5726  _on_drop(event) {
   5727    let droppedItem = getDroppableData(event);
   5728    let droppedURL = URL.isInstance(droppedItem)
   5729      ? droppedItem.href
   5730      : droppedItem;
   5731    if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) {
   5732      let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event);
   5733      this.value = droppedURL;
   5734      this.setPageProxyState("invalid");
   5735      this.focus();
   5736      // To simplify tracking of events, register an initial event for event
   5737      // telemetry, to replace the missing input event.
   5738      let queryContext = this.#makeQueryContext({ searchString: droppedURL });
   5739      this.controller.setLastQueryContextCache(queryContext);
   5740      this.controller.engagementEvent.start(event, queryContext);
   5741      this.handleNavigation({ triggeringPrincipal: principal });
   5742      if (this.#isAddressbar) {
   5743        // For safety reasons, in the drop case we don't want to immediately show
   5744        // the dropped value, instead we want to keep showing the current page
   5745        // url until an onLocationChange happens.
   5746        // See the handling in `setURI` for further details.
   5747        this.userTypedValue = null;
   5748        this.setURI({ dueToTabSwitch: true });
   5749      }
   5750    }
   5751  }
   5752 
   5753  _on_customizationstarting() {
   5754    this.incrementBreakoutBlockerCount();
   5755    this.blur();
   5756  }
   5757 
   5758  _on_aftercustomization() {
   5759    this.decrementBreakoutBlockerCount();
   5760    this.#updateLayoutBreakout();
   5761  }
   5762 
   5763  uiDensityChanged() {
   5764    if (this.#breakoutBlockerCount) {
   5765      return;
   5766    }
   5767    this.#updateLayoutBreakout();
   5768  }
   5769 
   5770  _on_toolbarvisibilitychange() {
   5771    this.#updateTextboxPositionNextFrame();
   5772  }
   5773 
   5774  _on_DOMMenuBarActive() {
   5775    this.#updateTextboxPositionNextFrame();
   5776  }
   5777 
   5778  _on_DOMMenuBarInactive() {
   5779    this.#updateTextboxPositionNextFrame();
   5780  }
   5781 
   5782  #allTextSelectedOnKeyDown = false;
   5783  get #allTextSelected() {
   5784    return this.selectionStart == 0 && this.selectionEnd == this.value.length;
   5785  }
   5786 
   5787  /**
   5788   * @param {string} value
   5789   *   A untrimmed address bar input.
   5790   * @returns {nsILoadInfo.SchemelessInputType}
   5791   *   Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input
   5792   *   doesn't start with a scheme relevant for schemeless HTTPS-First
   5793   *   (http://, https:// and file://).
   5794   *   Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme.
   5795   */
   5796  #getSchemelessInput(value) {
   5797    return ["http://", "https://", "file://"].every(
   5798      scheme => !value.trim().startsWith(scheme)
   5799    )
   5800      ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless
   5801      : Ci.nsILoadInfo.SchemelessInputTypeSchemeful;
   5802  }
   5803 
   5804  get #isOpenedPageInBlankTargetLoading() {
   5805    return (
   5806      this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
   5807        ?.count === 0 &&
   5808      this.window.gBrowser.selectedBrowser.browsingContext
   5809        .nonWebControlledBlankURI
   5810    );
   5811  }
   5812 
   5813  // Search modes are per browser and are stored in the `searchModes` property of this map.
   5814  // For a browser, search mode can be in preview mode, confirmed, or both.
   5815  // Typically, search mode is entered in preview mode with a particular
   5816  // source and is confirmed with the same source once a query starts.  It's
   5817  // also possible for a confirmed search mode to be replaced with a preview
   5818  // mode with a different source, and in those cases, we need to re-confirm
   5819  // search mode when preview mode is exited. In addition, only confirmed
   5820  // search modes should be restored across sessions. We therefore need to
   5821  // keep track of both the current confirmed and preview modes, per browser.
   5822  //
   5823  // For each browser with a search mode, this maps the browser to an object
   5824  // like this: { preview, confirmed }.  Both `preview` and `confirmed` are
   5825  // search mode objects; see the setSearchMode documentation.  Either one may
   5826  // be undefined if that particular mode is not active for the browser.
   5827 
   5828  /**
   5829   * Tracks a state object per browser.
   5830   */
   5831  #browserStates = new WeakMap();
   5832 
   5833  get #selectedText() {
   5834    return this.editor.selection.toStringWithFormat(
   5835      "text/plain",
   5836      Ci.nsIDocumentEncoder.OutputPreformatted |
   5837        Ci.nsIDocumentEncoder.OutputRaw,
   5838      0
   5839    );
   5840  }
   5841 
   5842  /**
   5843   * Check whether a key event has a similar effect as the Home key.
   5844   *
   5845   * @param {KeyboardEvent} event A Keyboard event
   5846   * @returns {boolean} Whether the even will act like the Home key.
   5847   */
   5848  #isHomeKeyUpEvent(event) {
   5849    let isMac = AppConstants.platform === "macosx";
   5850    return (
   5851      // On MacOS this can be generated with Fn + Left.
   5852      event.keyCode == KeyEvent.DOM_VK_HOME ||
   5853      // Windows and Linux also support Ctrl + Left.
   5854      (!isMac &&
   5855        event.keyCode == KeyboardEvent.DOM_VK_LEFT &&
   5856        event.ctrlKey &&
   5857        !event.shiftKey) ||
   5858      // MacOS supports other combos to move cursor at the start of the line.
   5859      // For example Ctrl + A.
   5860      (isMac &&
   5861        event.keyCode == KeyboardEvent.DOM_VK_A &&
   5862        event.ctrlKey &&
   5863        !event.shiftKey) ||
   5864      // And also Cmd (Meta) + Left.
   5865      // Unfortunately on MacOS it's not possible to detect combos with the meta
   5866      // key during the keyup event, due to how the OS handles events. Thus we
   5867      // record the combo on keydown, and check for it here.
   5868      (isMac &&
   5869        event.keyCode == KeyEvent.DOM_VK_META &&
   5870        this._isKeyDownWithMetaAndLeft)
   5871    );
   5872  }
   5873 }
   5874 
   5875 /**
   5876 * Tries to extract droppable data from a DND event.
   5877 *
   5878 * @param {DragEvent} event The DND event to examine.
   5879 * @returns {URL|string|null}
   5880 *          null if there's a security reason for which we should do nothing.
   5881 *          A URL object if it's a value we can load.
   5882 *          A string value otherwise.
   5883 */
   5884 function getDroppableData(event) {
   5885  let links;
   5886  try {
   5887    links = Services.droppedLinkHandler.dropLinks(event);
   5888  } catch (ex) {
   5889    // This is either an unexpected failure or a security exception; in either
   5890    // case we should always return null.
   5891    return null;
   5892  }
   5893  // The URL bar automatically handles inputs with newline characters,
   5894  // so we can get away with treating text/x-moz-url flavours as text/plain.
   5895  if (links[0]?.url) {
   5896    event.preventDefault();
   5897    let href = links[0].url;
   5898    if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
   5899      // We may have stripped an unsafe protocol like javascript: and if so
   5900      // there's no point in handling a partial drop.
   5901      event.stopImmediatePropagation();
   5902      return null;
   5903    }
   5904 
   5905    // If this fails, checkLoadURIStrWithPrincipal would also fail,
   5906    // as that's what it does with things that don't pass the IO
   5907    // service's newURI constructor without fixup. It's conceivable we
   5908    // may want to relax this check in the future (so e.g. www.foo.com
   5909    // gets fixed up), but not right now.
   5910    let url = URL.parse(href);
   5911    if (url) {
   5912      // If we succeed, try to pass security checks. If this works, return the
   5913      // URL object. If the *security checks* fail, return null.
   5914      try {
   5915        let principal =
   5916          Services.droppedLinkHandler.getTriggeringPrincipal(event);
   5917        Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
   5918          principal,
   5919          url.href,
   5920          Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
   5921        );
   5922        return url;
   5923      } catch (ex) {
   5924        return null;
   5925      }
   5926    }
   5927    // We couldn't make a URL out of this. Continue on, and return text below.
   5928  }
   5929  // Handle as text.
   5930  return event.dataTransfer.getData("text/plain");
   5931 }
   5932 
   5933 /**
   5934 * Decodes the given URI for displaying it in the address bar without losing
   5935 * information, such that hitting Enter again will load the same URI.
   5936 *
   5937 * @param {nsIURI} aURI
   5938 *   The URI to decode
   5939 * @returns {string}
   5940 *   The decoded URI
   5941 */
   5942 function losslessDecodeURI(aURI) {
   5943  let scheme = aURI.scheme;
   5944  let value = aURI.displaySpec;
   5945 
   5946  // Try to decode as UTF-8 if there's no encoding sequence that we would break.
   5947  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
   5948    let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
   5949    if (decodeASCIIOnly) {
   5950      // This only decodes ascii characters (hex) 20-7e, except 25 (%).
   5951      // This avoids both cases stipulated below (%-related issues, and \r, \n
   5952      // and \t, which would be %0d, %0a and %09, respectively) as well as any
   5953      // non-US-ascii characters.
   5954      value = value.replace(
   5955        /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
   5956        decodeURI
   5957      );
   5958    } else {
   5959      try {
   5960        value = decodeURI(value)
   5961          // decodeURI decodes %25 to %, which creates unintended encoding
   5962          // sequences. Re-encode it, unless it's part of a sequence that
   5963          // survived decodeURI, i.e. one for:
   5964          // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
   5965          // (RFC 3987 section 3.2)
   5966          .replace(
   5967            /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
   5968            encodeURIComponent
   5969          );
   5970      } catch (e) {}
   5971    }
   5972  }
   5973 
   5974  // IMPORTANT: The following regular expressions are Unicode-aware due to /v.
   5975  // Avoid matching high or low surrogate pairs directly, always work with
   5976  // full Unicode scalar values.
   5977 
   5978  // Encode potentially invisible characters:
   5979  //   U+0000-001F: C0/C1 control characters
   5980  //   U+007F-009F: commands
   5981  //   U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
   5982  //   U+2028-2029: line and paragraph separators
   5983  //   U+2800: braille empty pattern
   5984  //   U+FFFC: object replacement character
   5985  // Encode any trailing whitespace that may be part of a pasted URL, so that it
   5986  // doesn't get eaten away by the location bar (bug 410726).
   5987  // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
   5988  // where they would push part of the URL to overflow the location bar
   5989  // (bug 1395508). A single space, or the last space if the are many, is
   5990  // preserved to maintain readability of certain urls if it's not followed by a
   5991  // control or separator character. We only do this for the common space,
   5992  // because others may be eaten when copied to the clipboard,so it's safer to
   5993  // preserve them encoded.
   5994  value = value.replace(
   5995    // eslint-disable-next-line no-control-regex
   5996    /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv,
   5997    encodeURIComponent
   5998  );
   5999 
   6000  // Encode characters that are ignorable, can't be rendered usefully, or may
   6001  // confuse users.
   6002  //
   6003  // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
   6004  // per bug 582186:
   6005  //   U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
   6006  //   U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
   6007  //   U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF
   6008  // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
   6009  //   U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
   6010  // Other format characters in the Cf category that are unlikely to be rendered
   6011  // usefully:
   6012  //   U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3
   6013  // Mimicking UI parts:
   6014  //   U+1F50F-1F513, U+1F6E1
   6015  // Unassigned codepoints, sometimes shown as empty glyphs.
   6016  value = value.replace(
   6017    // eslint-disable-next-line no-misleading-character-class
   6018    /[[\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,
   6019    encodeURIComponent
   6020  );
   6021  return value;
   6022 }
   6023 
   6024 /**
   6025 * Handles copy and cut commands for the urlbar.
   6026 */
   6027 class CopyCutController {
   6028  /**
   6029   * @param {SmartbarInput} urlbar
   6030   *   The SmartbarInput instance to use this controller for.
   6031   */
   6032  constructor(urlbar) {
   6033    this.urlbar = urlbar;
   6034  }
   6035 
   6036  /**
   6037   * @param {string} command
   6038   *   The name of the command to handle.
   6039   */
   6040  doCommand(command) {
   6041    let urlbar = this.urlbar;
   6042    let val = urlbar._getSelectedValueForClipboard();
   6043    if (!val) {
   6044      return;
   6045    }
   6046 
   6047    if (command == "cmd_cut" && this.isCommandEnabled(command)) {
   6048      let start = urlbar.selectionStart;
   6049      let end = urlbar.selectionEnd;
   6050      urlbar.inputField.value =
   6051        urlbar.inputField.value.substring(0, start) +
   6052        urlbar.inputField.value.substring(end);
   6053      urlbar.inputField.setSelectionRange(start, start);
   6054 
   6055      let event = new UIEvent("input", {
   6056        bubbles: true,
   6057        cancelable: false,
   6058        view: urlbar.window,
   6059        detail: 0,
   6060      });
   6061      urlbar.inputField.dispatchEvent(event);
   6062    }
   6063 
   6064    lazy.ClipboardHelper.copyString(val);
   6065  }
   6066 
   6067  /**
   6068   * @param {string} command
   6069   *   The name of the command to check.
   6070   * @returns {boolean}
   6071   *   Whether the command is handled by this controller.
   6072   */
   6073  supportsCommand(command) {
   6074    switch (command) {
   6075      case "cmd_copy":
   6076      case "cmd_cut":
   6077        return true;
   6078    }
   6079    return false;
   6080  }
   6081 
   6082  /**
   6083   * @param {string} command
   6084   *   The name of the command to check.
   6085   * @returns {boolean}
   6086   *   Whether the command should be enabled.
   6087   */
   6088  isCommandEnabled(command) {
   6089    return (
   6090      this.supportsCommand(command) &&
   6091      (command != "cmd_cut" || !this.urlbar.readOnly) &&
   6092      this.urlbar.selectionStart < this.urlbar.selectionEnd
   6093    );
   6094  }
   6095 
   6096  onEvent() {}
   6097 }
   6098 
   6099 /**
   6100 * Manages the Add Search Engine contextual menu entries.
   6101 *
   6102 * Note: setEnginesFromBrowser must be invoked from the outside when the
   6103 *       page provided engines list changes.
   6104 *       refreshContextMenu must be invoked when the context menu is opened.
   6105 */
   6106 class AddSearchEngineHelper {
   6107  /**
   6108   * @type {UrlbarSearchOneOffs}
   6109   */
   6110  shortcutButtons;
   6111 
   6112  /**
   6113   * @param {SmartbarInput} input The parent SmartbarInput.
   6114   */
   6115  constructor(input) {
   6116    this.input = input;
   6117    this.shortcutButtons = input.view.oneOffSearchButtons;
   6118  }
   6119 
   6120  /**
   6121   * If there's more than this number of engines, the context menu offers
   6122   * them in a submenu.
   6123   *
   6124   * @returns {number}
   6125   */
   6126  get maxInlineEngines() {
   6127    return this.shortcutButtons._maxInlineAddEngines;
   6128  }
   6129 
   6130  /**
   6131   * Invoked by OpenSearchManager when the list of available engines changes.
   6132   *
   6133   * @param {object} browser The current browser.
   6134   * @param {object} engines The updated list of available engines.
   6135   */
   6136  setEnginesFromBrowser(browser, engines) {
   6137    this.browsingContext = browser.browsingContext;
   6138    // Make a copy of the array for state comparison.
   6139    engines = engines.slice();
   6140    if (!this._sameEngines(this.engines, engines)) {
   6141      this.engines = engines;
   6142      this.shortcutButtons?.updateWebEngines();
   6143    }
   6144  }
   6145 
   6146  _sameEngines(engines1, engines2) {
   6147    if (engines1?.length != engines2?.length) {
   6148      return false;
   6149    }
   6150    return lazy.ObjectUtils.deepEqual(
   6151      engines1.map(e => e.title),
   6152      engines2.map(e => e.title)
   6153    );
   6154  }
   6155 
   6156  _createMenuitem(engine, index) {
   6157    let elt = this.input.document.createXULElement("menuitem");
   6158    elt.setAttribute("anonid", `add-engine-${index}`);
   6159    elt.classList.add("menuitem-iconic");
   6160    elt.classList.add("context-menu-add-engine");
   6161    this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", {
   6162      engineName: engine.title,
   6163    });
   6164    elt.setAttribute("uri", engine.uri);
   6165    if (engine.icon) {
   6166      elt.setAttribute("image", engine.icon);
   6167    } else {
   6168      elt.removeAttribute("image");
   6169    }
   6170    elt.addEventListener("command", this._onCommand.bind(this));
   6171    return elt;
   6172  }
   6173 
   6174  _createMenu(engine) {
   6175    let elt = this.input.document.createXULElement("menu");
   6176    elt.setAttribute("anonid", "add-engine-menu");
   6177    elt.classList.add("menu-iconic");
   6178    elt.classList.add("context-menu-add-engine");
   6179    this.input.document.l10n.setAttributes(
   6180      elt,
   6181      "search-one-offs-add-engine-menu"
   6182    );
   6183    if (engine.icon) {
   6184      elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon));
   6185    }
   6186    let popup = this.input.document.createXULElement("menupopup");
   6187    elt.appendChild(popup);
   6188    return elt;
   6189  }
   6190 
   6191  refreshContextMenu() {
   6192    let engines = this.engines;
   6193    let contextMenu = this.input.querySelector("moz-input-box").menupopup;
   6194 
   6195    // Certain operations, like customization, destroy and recreate widgets,
   6196    // so we cannot rely on cached elements.
   6197    if (!contextMenu.querySelector(".menuseparator-add-engine")) {
   6198      this.contextSeparator =
   6199        this.input.document.createXULElement("menuseparator");
   6200      this.contextSeparator.setAttribute("anonid", "add-engine-separator");
   6201      this.contextSeparator.classList.add("menuseparator-add-engine");
   6202      this.contextSeparator.collapsed = true;
   6203      contextMenu.appendChild(this.contextSeparator);
   6204    }
   6205 
   6206    this.contextSeparator.collapsed = !engines.length;
   6207    let curElt = this.contextSeparator;
   6208    // Remove the previous items, if any.
   6209    for (let elt = curElt.nextElementSibling; elt; ) {
   6210      let nextElementSibling = elt.nextElementSibling;
   6211      elt.remove();
   6212      elt = nextElementSibling;
   6213    }
   6214 
   6215    // If the page provides too many engines, we only show a single menu entry
   6216    // with engines in a submenu.
   6217    if (engines.length > this.maxInlineEngines) {
   6218      // Set the menu button's image to the image of the first engine.  The
   6219      // offered engines may have differing images, so there's no perfect
   6220      // choice here.
   6221      let elt = this._createMenu(engines[0]);
   6222      this.contextSeparator.insertAdjacentElement("afterend", elt);
   6223      curElt = elt.lastElementChild;
   6224    }
   6225 
   6226    // Insert the engines, either in the contextual menu or the sub menu.
   6227    for (let i = 0; i < engines.length; ++i) {
   6228      let elt = this._createMenuitem(engines[i], i);
   6229      if (curElt.localName == "menupopup") {
   6230        curElt.appendChild(elt);
   6231      } else {
   6232        curElt.insertAdjacentElement("afterend", elt);
   6233      }
   6234      curElt = elt;
   6235    }
   6236  }
   6237 
   6238  async _onCommand(event) {
   6239    let added = await lazy.SearchUIUtils.addOpenSearchEngine(
   6240      event.target.getAttribute("uri"),
   6241      event.target.getAttribute("image"),
   6242      this.browsingContext
   6243    ).catch(console.error);
   6244    if (added) {
   6245      // Remove the offered engine from the list. The browser updated the
   6246      // engines list at this point, so we just have to refresh the menu.)
   6247      this.refreshContextMenu();
   6248    }
   6249  }
   6250 }
   6251 
   6252 customElements.define("moz-smartbar", SmartbarInput);