tor-browser

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

searchbar.js (34250B)


      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 "use strict";
      6 
      7 /* globals XULCommandEvent */
      8 
      9 // This is loaded into chrome windows with the subscript loader. Wrap in
     10 // a block to prevent accidentally leaking globals onto `window`.
     11 {
     12  const lazy = {};
     13 
     14  ChromeUtils.defineESModuleGetters(lazy, {
     15    BrowserSearchTelemetry:
     16      "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     17    BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     18    FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
     19    SearchSuggestionController:
     20      "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     21  });
     22 
     23  /**
     24   * Defines the search bar element.
     25   */
     26  class MozSearchbar extends MozXULElement {
     27    static get inheritedAttributes() {
     28      return {
     29        ".searchbar-textbox":
     30          "disabled,disableautocomplete,searchengine,src,newlines",
     31        ".searchbar-search-button": "addengines",
     32      };
     33    }
     34 
     35    static get markup() {
     36      return `
     37        <stringbundle src="chrome://browser/locale/search.properties"></stringbundle>
     38        <hbox class="searchbar-search-button" data-l10n-id="searchbar-icon" role="button" keyNav="false" aria-expanded="false" aria-controls="PopupSearchAutoComplete" aria-haspopup="true">
     39          <image class="searchbar-search-icon"></image>
     40          <image class="searchbar-search-icon-overlay"></image>
     41        </hbox>
     42        <html:input class="searchbar-textbox" is="autocomplete-input" type="search" data-l10n-id="searchbar-input" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0"/>
     43        <menupopup class="textbox-contextmenu"></menupopup>
     44        <hbox class="search-go-container" align="center">
     45          <image class="search-go-button urlbar-icon" role="button" keyNav="false" hidden="true" data-l10n-id="searchbar-submit"></image>
     46        </hbox>
     47      `;
     48    }
     49 
     50    constructor() {
     51      super();
     52      MozXULElement.insertFTLIfNeeded("browser/search.ftl");
     53 
     54      this._setupEventListeners();
     55      let searchbar = this;
     56      this.observer = {
     57        observe(aEngine, aTopic, aData) {
     58          if (aTopic == "browser-search-engine-modified") {
     59            // Make sure the engine list is refetched next time it's needed
     60            searchbar._engines = null;
     61 
     62            // Update the popup header and update the display after any modification.
     63            searchbar._textbox.popup.updateHeader();
     64            searchbar.updateDisplay();
     65          } else if (
     66            aData == "browser.search.widget.new" &&
     67            searchbar.isConnected
     68          ) {
     69            if (Services.prefs.getBoolPref("browser.search.widget.new")) {
     70              searchbar.disconnectedCallback();
     71            } else {
     72              searchbar.connectedCallback();
     73            }
     74          }
     75        },
     76        QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
     77      };
     78 
     79      Services.prefs.addObserver("browser.search.widget.new", this.observer);
     80 
     81      window.addEventListener("unload", () => {
     82        this.destroy();
     83        Services.prefs.removeObserver(
     84          "browser.search.widget.new",
     85          this.observer
     86        );
     87      });
     88 
     89      this._ignoreFocus = false;
     90      this._engines = null;
     91      this.telemetrySelectedIndex = -1;
     92    }
     93 
     94    connectedCallback() {
     95      // Don't initialize if this isn't going to be visible.
     96      if (
     97        this.closest("#BrowserToolbarPalette") ||
     98        Services.prefs.getBoolPref("browser.search.widget.new")
     99      ) {
    100        return;
    101      }
    102 
    103      this.appendChild(this.constructor.fragment);
    104      this.initializeAttributeInheritance();
    105 
    106      // Don't go further if in Customize mode.
    107      if (this.parentNode.parentNode.localName == "toolbarpaletteitem") {
    108        return;
    109      }
    110 
    111      // Ensure we get persisted widths back, if we've been in the palette:
    112      let storedWidth = Services.xulStore.getValue(
    113        document.documentURI,
    114        this.parentNode.id,
    115        "width"
    116      );
    117      if (storedWidth) {
    118        this.parentNode.setAttribute("width", storedWidth);
    119        this.parentNode.style.width = storedWidth + "px";
    120      }
    121 
    122      this._stringBundle = this.querySelector("stringbundle");
    123      this._textbox = this.querySelector(".searchbar-textbox");
    124 
    125      this._menupopup = null;
    126      this._pasteAndSearchMenuItem = null;
    127 
    128      this._setupTextboxEventListeners();
    129      this._initTextbox();
    130 
    131      Services.obs.addObserver(this.observer, "browser-search-engine-modified");
    132 
    133      this._initialized = true;
    134 
    135      (window.delayedStartupPromise || Promise.resolve()).then(() => {
    136        window.requestIdleCallback(() => {
    137          Services.search
    138            .init()
    139            .then(() => {
    140              // Bail out if the binding's been destroyed
    141              if (!this._initialized) {
    142                return;
    143              }
    144 
    145              // Ensure the popup header is updated if the user has somehow
    146              // managed to open the popup before the search service has finished
    147              // initializing.
    148              this._textbox.popup.updateHeader();
    149              // Refresh the display (updating icon, etc)
    150              this.updateDisplay();
    151              OpenSearchManager.updateOpenSearchBadge(window);
    152            })
    153            .catch(status =>
    154              console.error(
    155                "Cannot initialize search service, bailing out:",
    156                status
    157              )
    158            );
    159        });
    160      });
    161 
    162      // Wait until the popupshowing event to avoid forcing immediate
    163      // attachment of the search-one-offs binding.
    164      this.textbox.popup.addEventListener(
    165        "popupshowing",
    166        () => {
    167          let oneOffButtons = this.textbox.popup.oneOffButtons;
    168          // Some accessibility tests create their own <searchbar> that doesn't
    169          // use the popup binding below, so null-check oneOffButtons.
    170          if (oneOffButtons) {
    171            oneOffButtons.telemetryOrigin = "searchbar";
    172            // Set .textbox first, since the popup setter will cause
    173            // a _rebuild call that uses it.
    174            oneOffButtons.textbox = this.textbox;
    175            oneOffButtons.popup = this.textbox.popup;
    176          }
    177        },
    178        { capture: true, once: true }
    179      );
    180 
    181      this.querySelector(".search-go-button").addEventListener("click", event =>
    182        this.handleSearchCommand(event)
    183      );
    184    }
    185 
    186    async getEngines() {
    187      if (!this._engines) {
    188        this._engines = await Services.search.getVisibleEngines();
    189      }
    190      return this._engines;
    191    }
    192 
    193    set currentEngine(val) {
    194      if (PrivateBrowsingUtils.isWindowPrivate(window)) {
    195        Services.search.setDefaultPrivate(
    196          val,
    197          Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR
    198        );
    199      } else {
    200        Services.search.setDefault(
    201          val,
    202          Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR
    203        );
    204      }
    205    }
    206 
    207    get currentEngine() {
    208      let currentEngine;
    209      if (PrivateBrowsingUtils.isWindowPrivate(window)) {
    210        currentEngine = Services.search.defaultPrivateEngine;
    211      } else {
    212        currentEngine = Services.search.defaultEngine;
    213      }
    214      // Return a dummy engine if there is no currentEngine
    215      return currentEngine || { name: "", uri: null };
    216    }
    217 
    218    /**
    219     * textbox is used by sanitize.js to clear the undo history when
    220     * clearing form information.
    221     *
    222     * @returns {HTMLInputElement}
    223     */
    224    get textbox() {
    225      return this._textbox;
    226    }
    227 
    228    /**
    229     * Textbox alias for API compatibility with UrlbarInput.
    230     */
    231    get inputField() {
    232      return this.textbox;
    233    }
    234 
    235    set value(val) {
    236      this._textbox.value = val;
    237    }
    238 
    239    get value() {
    240      return this._textbox.value;
    241    }
    242 
    243    destroy() {
    244      if (this._initialized) {
    245        this._initialized = false;
    246 
    247        Services.obs.removeObserver(
    248          this.observer,
    249          "browser-search-engine-modified"
    250        );
    251      }
    252 
    253      // Make sure to break the cycle from _textbox to us. Otherwise we leak
    254      // the world. But make sure it's actually pointing to us.
    255      // Also make sure the textbox has ever been constructed, otherwise the
    256      // _textbox getter will cause the textbox constructor to run, add an
    257      // observer, and leak the world too.
    258      if (
    259        this._textbox &&
    260        this._textbox.mController &&
    261        this._textbox.mController.input &&
    262        this._textbox.mController.input.wrappedJSObject ==
    263          this.nsIAutocompleteInput
    264      ) {
    265        this._textbox.mController.input = null;
    266      }
    267    }
    268 
    269    focus() {
    270      this._textbox.focus();
    271    }
    272 
    273    select() {
    274      this._textbox.select();
    275    }
    276 
    277    setIcon(element, uri) {
    278      element.setAttribute("src", uri);
    279    }
    280 
    281    updateDisplay() {
    282      this._textbox.title = this._stringBundle.getFormattedString("searchtip", [
    283        this.currentEngine.name,
    284      ]);
    285    }
    286 
    287    updateGoButtonVisibility() {
    288      this.querySelector(".search-go-button").hidden = !this._textbox.value;
    289    }
    290 
    291    openSuggestionsPanel(aShowOnlySettingsIfEmpty) {
    292      if (this._textbox.open) {
    293        return;
    294      }
    295 
    296      this._textbox.showHistoryPopup();
    297      let searchIcon = document.querySelector(".searchbar-search-button");
    298      searchIcon.setAttribute("aria-expanded", "true");
    299 
    300      if (this._textbox.value) {
    301        // showHistoryPopup does a startSearch("") call, ensure the
    302        // controller handles the text from the input box instead:
    303        this._textbox.mController.handleText();
    304      } else if (aShowOnlySettingsIfEmpty) {
    305        this.setAttribute("showonlysettings", "true");
    306      }
    307    }
    308 
    309    async selectEngine(aEvent, isNextEngine) {
    310      // Stop event bubbling now, because the rest of this method is async.
    311      aEvent.preventDefault();
    312      aEvent.stopPropagation();
    313 
    314      // Find the new index.
    315      let engines = await this.getEngines();
    316      let currentName = this.currentEngine.name;
    317      let newIndex = -1;
    318      let lastIndex = engines.length - 1;
    319      for (let i = lastIndex; i >= 0; --i) {
    320        if (engines[i].name == currentName) {
    321          // Check bounds to cycle through the list of engines continuously.
    322          if (!isNextEngine && i == 0) {
    323            newIndex = lastIndex;
    324          } else if (isNextEngine && i == lastIndex) {
    325            newIndex = 0;
    326          } else {
    327            newIndex = i + (isNextEngine ? 1 : -1);
    328          }
    329          break;
    330        }
    331      }
    332 
    333      this.currentEngine = engines[newIndex];
    334 
    335      this.openSuggestionsPanel();
    336    }
    337 
    338    handleSearchCommand(aEvent, aEngine, aForceNewTab) {
    339      if (
    340        aEvent &&
    341        aEvent.originalTarget.classList.contains("search-go-button") &&
    342        aEvent.button == 2
    343      ) {
    344        return;
    345      }
    346      let { where, params } = this._whereToOpen(aEvent, aForceNewTab);
    347      this.handleSearchCommandWhere(aEvent, aEngine, where, params);
    348    }
    349 
    350    handleSearchCommandWhere(aEvent, aEngine, aWhere, aParams = {}) {
    351      let textBox = this._textbox;
    352      let textValue = textBox.value;
    353 
    354      let selectedIndex = this.telemetrySelectedIndex;
    355      let isOneOff = false;
    356 
    357      lazy.BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod(
    358        aEvent,
    359        selectedIndex
    360      );
    361 
    362      if (selectedIndex == -1) {
    363        isOneOff =
    364          this.textbox.popup.oneOffButtons.eventTargetIsAOneOff(aEvent);
    365      }
    366 
    367      if (aWhere === "tab" && !!aParams.inBackground) {
    368        // Keep the focus in the search bar.
    369        aParams.avoidBrowserFocus = true;
    370      } else if (
    371        aWhere !== "window" &&
    372        aEvent.keyCode === KeyEvent.DOM_VK_RETURN
    373      ) {
    374        // Move the focus to the selected browser when keyup the Enter.
    375        aParams.avoidBrowserFocus = true;
    376        this._needBrowserFocusAtEnterKeyUp = true;
    377      }
    378 
    379      // This is a one-off search only if oneOffRecorded is true.
    380      this.doSearch(textValue, aWhere, aEngine, aParams, isOneOff);
    381    }
    382 
    383    doSearch(aData, aWhere, aEngine, aParams, isOneOff = false) {
    384      let textBox = this._textbox;
    385      let engine = aEngine || this.currentEngine;
    386 
    387      // Save the current value in the form history
    388      if (
    389        aData &&
    390        !PrivateBrowsingUtils.isWindowPrivate(window) &&
    391        lazy.FormHistory.enabled &&
    392        aData.length <=
    393          lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
    394      ) {
    395        lazy.FormHistory.update({
    396          op: "bump",
    397          fieldname: textBox.getAttribute("autocompletesearchparam"),
    398          value: aData,
    399          source: engine.name,
    400        }).catch(error =>
    401          console.error("Saving search to form history failed:", error)
    402        );
    403      }
    404 
    405      let submission = engine.getSubmission(aData, null);
    406 
    407      // If we hit here, we come either from a one-off, a plain search or a suggestion.
    408      const details = {
    409        isOneOff,
    410        isSuggestion: !isOneOff && this.telemetrySelectedIndex != -1,
    411      };
    412 
    413      this.telemetrySelectedIndex = -1;
    414 
    415      // Record when the user uses the search bar
    416      Services.prefs.setStringPref(
    417        "browser.search.widget.lastUsed",
    418        new Date().toISOString()
    419      );
    420 
    421      // null parameter below specifies HTML response for search
    422      let params = {
    423        postData: submission.postData,
    424        globalHistoryOptions: {
    425          triggeringSearchEngine: engine.name,
    426        },
    427      };
    428      if (aParams) {
    429        for (let key in aParams) {
    430          params[key] = aParams[key];
    431        }
    432      }
    433 
    434      if (aWhere == "tab") {
    435        gBrowser.tabContainer.addEventListener(
    436          "TabOpen",
    437          event =>
    438            lazy.BrowserSearchTelemetry.recordSearch(
    439              event.target.linkedBrowser,
    440              engine,
    441              "searchbar",
    442              details
    443            ),
    444          { once: true }
    445        );
    446      } else {
    447        lazy.BrowserSearchTelemetry.recordSearch(
    448          gBrowser.selectedBrowser,
    449          engine,
    450          "searchbar",
    451          details
    452        );
    453      }
    454 
    455      openTrustedLinkIn(submission.uri.spec, aWhere, params);
    456    }
    457 
    458    /**
    459     * Returns information on where a search results page should be loaded: in the
    460     * current tab or a new tab.
    461     *
    462     * @param {event} aEvent
    463     *        The event that triggered the page load.
    464     * @param {boolean} [aForceNewTab]
    465     *        True to force the load in a new tab.
    466     * @returns {object} An object { where, params }.  `where` is a string:
    467     *          "current" or "tab".  `params` is an object further describing how
    468     *          the page should be loaded.
    469     */
    470    _whereToOpen(aEvent, aForceNewTab = false) {
    471      let where = "current";
    472      let params = {};
    473      const newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
    474 
    475      // Open ctrl/cmd clicks on one-off buttons in a new background tab.
    476      if (aEvent?.originalTarget.classList.contains("search-go-button")) {
    477        where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
    478        if (
    479          newTabPref &&
    480          !aEvent.altKey &&
    481          !aEvent.getModifierState("AltGraph") &&
    482          where == "current" &&
    483          !gBrowser.selectedTab.isEmpty
    484        ) {
    485          where = "tab";
    486        }
    487      } else if (aForceNewTab) {
    488        where = "tab";
    489        if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
    490          params = {
    491            inBackground: true,
    492          };
    493        }
    494      } else {
    495        if (
    496          (KeyboardEvent.isInstance(aEvent) &&
    497            (aEvent.altKey || aEvent.getModifierState("AltGraph"))) ^
    498            newTabPref &&
    499          !gBrowser.selectedTab.isEmpty
    500        ) {
    501          where = "tab";
    502        }
    503        if (
    504          MouseEvent.isInstance(aEvent) &&
    505          (aEvent.button == 1 || aEvent.getModifierState("Accel"))
    506        ) {
    507          where = "tab";
    508          params = {
    509            inBackground: true,
    510          };
    511        }
    512      }
    513 
    514      return { where, params };
    515    }
    516 
    517    /**
    518     * Opens the search form of the provided engine or the current engine
    519     * if no engine was provided.
    520     *
    521     * @param {event} aEvent
    522     *        The event causing the searchForm to be opened.
    523     * @param {nsISearchEngine} [aEngine]
    524     *        The search engine or undefined to use the current engine.
    525     * @param {string} where
    526     *        Where the search form should be opened.
    527     * @param {object} [params]
    528     *        Parameters for URILoadingHelper.openLinkIn.
    529     */
    530    openSearchFormWhere(aEvent, aEngine, where, params = {}) {
    531      let engine = aEngine || this.currentEngine;
    532      let searchForm = engine.searchForm;
    533 
    534      if (where === "tab" && !!params.inBackground) {
    535        // Keep the focus in the search bar.
    536        params.avoidBrowserFocus = true;
    537      } else if (
    538        where !== "window" &&
    539        aEvent.keyCode === KeyEvent.DOM_VK_RETURN
    540      ) {
    541        // Move the focus to the selected browser when keyup the Enter.
    542        params.avoidBrowserFocus = true;
    543        this._needBrowserFocusAtEnterKeyUp = true;
    544      }
    545 
    546      lazy.BrowserSearchTelemetry.recordSearchForm(engine, "searchbar");
    547      openTrustedLinkIn(searchForm, where, params);
    548    }
    549 
    550    disconnectedCallback() {
    551      this.destroy();
    552      while (this.firstChild) {
    553        this.firstChild.remove();
    554      }
    555    }
    556 
    557    /**
    558     * Determines if we should select all the text in the searchbar based on the
    559     * searchbar state, and whether the selection is empty.
    560     */
    561    _maybeSelectAll() {
    562      if (
    563        !this._preventClickSelectsAll &&
    564        document.activeElement == this._textbox &&
    565        this._textbox.selectionStart == this._textbox.selectionEnd
    566      ) {
    567        this.select();
    568      }
    569    }
    570 
    571    _setupEventListeners() {
    572      this.addEventListener("click", () => {
    573        this._maybeSelectAll();
    574      });
    575 
    576      this.addEventListener(
    577        "DOMMouseScroll",
    578        event => {
    579          if (event.getModifierState("Accel")) {
    580            this.selectEngine(event, event.detail > 0);
    581          }
    582        },
    583        true
    584      );
    585 
    586      this.addEventListener("input", () => {
    587        this.updateGoButtonVisibility();
    588      });
    589 
    590      this.addEventListener("drop", () => {
    591        this.updateGoButtonVisibility();
    592      });
    593 
    594      this.addEventListener(
    595        "blur",
    596        () => {
    597          // Reset the flag since we can't capture enter keyup event if the event happens
    598          // after moving the focus.
    599          this._needBrowserFocusAtEnterKeyUp = false;
    600 
    601          // If the input field is still focused then a different window has
    602          // received focus, ignore the next focus event.
    603          this._ignoreFocus = document.activeElement == this._textbox;
    604        },
    605        true
    606      );
    607 
    608      this.addEventListener(
    609        "focus",
    610        () => {
    611          // Speculatively connect to the current engine's search URI (and
    612          // suggest URI, if different) to reduce request latency
    613          this.currentEngine.speculativeConnect({
    614            window,
    615            originAttributes: gBrowser.contentPrincipal.originAttributes,
    616          });
    617 
    618          if (this._ignoreFocus) {
    619            // This window has been re-focused, don't show the suggestions
    620            this._ignoreFocus = false;
    621            return;
    622          }
    623 
    624          // Don't open the suggestions if there is no text in the textbox.
    625          if (!this._textbox.value) {
    626            return;
    627          }
    628 
    629          // Don't open the suggestions if the mouse was used to focus the
    630          // textbox, that will be taken care of in the click handler.
    631          if (
    632            Services.focus.getLastFocusMethod(window) &
    633            Services.focus.FLAG_BYMOUSE
    634          ) {
    635            return;
    636          }
    637 
    638          this.openSuggestionsPanel();
    639        },
    640        true
    641      );
    642 
    643      this.addEventListener("mousedown", event => {
    644        this._preventClickSelectsAll = this._textbox.focused;
    645        // Ignore right clicks
    646        if (event.button != 0) {
    647          return;
    648        }
    649 
    650        // Ignore clicks on the search go button.
    651        if (event.originalTarget.classList.contains("search-go-button")) {
    652          return;
    653        }
    654 
    655        // Ignore clicks on menu items in the input's context menu.
    656        if (event.originalTarget.localName == "menuitem") {
    657          return;
    658        }
    659 
    660        let isIconClick = event.originalTarget.classList.contains(
    661          "searchbar-search-button"
    662        );
    663 
    664        // Hide popup when icon is clicked while popup is open
    665        if (isIconClick && this.textbox.popup.popupOpen) {
    666          this.textbox.popup.closePopup();
    667          let searchIcon = document.querySelector(".searchbar-search-button");
    668          searchIcon.setAttribute("aria-expanded", "false");
    669        } else if (isIconClick || this._textbox.value) {
    670          // Open the suggestions whenever clicking on the search icon or if there
    671          // is text in the textbox.
    672          this.openSuggestionsPanel(true);
    673        }
    674      });
    675    }
    676 
    677    _setupTextboxEventListeners() {
    678      this.textbox.addEventListener("input", () => {
    679        this.textbox.popup.removeAttribute("showonlysettings");
    680      });
    681 
    682      this.textbox.addEventListener("dragover", event => {
    683        let types = event.dataTransfer.types;
    684        if (
    685          types.includes("text/plain") ||
    686          types.includes("text/x-moz-text-internal")
    687        ) {
    688          event.preventDefault();
    689        }
    690      });
    691 
    692      this.textbox.addEventListener("drop", event => {
    693        let dataTransfer = event.dataTransfer;
    694        let data = dataTransfer.getData("text/plain");
    695        if (!data) {
    696          data = dataTransfer.getData("text/x-moz-text-internal");
    697        }
    698        if (data) {
    699          event.preventDefault();
    700          this.textbox.value = data;
    701          this.openSuggestionsPanel();
    702        }
    703      });
    704 
    705      this.textbox.addEventListener("contextmenu", event => {
    706        if (!this._menupopup) {
    707          this._buildContextMenu();
    708        }
    709 
    710        this._textbox.closePopup();
    711 
    712        // Make sure the context menu isn't opened via keyboard shortcut. Check for text selection
    713        // before updating the state of any menu items.
    714        if (event.button) {
    715          this._maybeSelectAll();
    716        }
    717 
    718        // Update disabled state of menu items
    719        for (let item of this._menupopup.querySelectorAll("menuitem[cmd]")) {
    720          let command = item.getAttribute("cmd");
    721          let controller =
    722            document.commandDispatcher.getControllerForCommand(command);
    723          item.disabled = !controller.isCommandEnabled(command);
    724        }
    725 
    726        let pasteEnabled = document.commandDispatcher
    727          .getControllerForCommand("cmd_paste")
    728          .isCommandEnabled("cmd_paste");
    729        this._pasteAndSearchMenuItem.disabled = !pasteEnabled;
    730 
    731        this._menupopup.openPopupAtScreen(event.screenX, event.screenY, true);
    732 
    733        event.preventDefault();
    734      });
    735    }
    736 
    737    _initTextbox() {
    738      if (this.parentNode.parentNode.localName == "toolbarpaletteitem") {
    739        return;
    740      }
    741 
    742      this.setAttribute("role", "combobox");
    743      this.setAttribute("aria-owns", this.textbox.popup.id);
    744 
    745      // This overrides the searchParam property in autocomplete.xml. We're
    746      // hijacking this property as a vehicle for delivering the privacy
    747      // information about the window into the guts of nsSearchSuggestions.
    748      // Note that the setter is the same as the parent. We were not sure whether
    749      // we can override just the getter. If that proves to be the case, the setter
    750      // can be removed.
    751      Object.defineProperty(this.textbox, "searchParam", {
    752        get() {
    753          return (
    754            this.getAttribute("autocompletesearchparam") +
    755            (PrivateBrowsingUtils.isWindowPrivate(window) ? "|private" : "")
    756          );
    757        },
    758        set(val) {
    759          this.setAttribute("autocompletesearchparam", val);
    760        },
    761      });
    762 
    763      Object.defineProperty(this.textbox, "selectedButton", {
    764        get() {
    765          return this.popup.oneOffButtons.selectedButton;
    766        },
    767        set(val) {
    768          this.popup.oneOffButtons.selectedButton = val;
    769        },
    770      });
    771 
    772      // This is implemented so that when textbox.value is set directly (e.g.,
    773      // by tests), the one-off query is updated.
    774      this.textbox.onBeforeValueSet = aValue => {
    775        if (this.textbox.popup._oneOffButtons) {
    776          this.textbox.popup.oneOffButtons.query = aValue;
    777        }
    778        return aValue;
    779      };
    780 
    781      // Returns true if the event is handled by us, false otherwise.
    782      this.textbox.onBeforeHandleKeyDown = aEvent => {
    783        if (aEvent.getModifierState("Accel")) {
    784          if (
    785            aEvent.keyCode == KeyEvent.DOM_VK_DOWN ||
    786            aEvent.keyCode == KeyEvent.DOM_VK_UP
    787          ) {
    788            this.selectEngine(aEvent, aEvent.keyCode == KeyEvent.DOM_VK_DOWN);
    789            return true;
    790          }
    791          return false;
    792        }
    793 
    794        if (
    795          (AppConstants.platform == "macosx" &&
    796            aEvent.keyCode == KeyEvent.DOM_VK_F4) ||
    797          (aEvent.getModifierState("Alt") &&
    798            (aEvent.keyCode == KeyEvent.DOM_VK_DOWN ||
    799              aEvent.keyCode == KeyEvent.DOM_VK_UP))
    800        ) {
    801          if (!this.textbox.openSearch()) {
    802            aEvent.preventDefault();
    803            aEvent.stopPropagation();
    804            return true;
    805          }
    806        }
    807 
    808        let popup = this.textbox.popup;
    809        let searchIcon = document.querySelector(".searchbar-search-button");
    810        searchIcon.setAttribute("aria-expanded", popup.popupOpen);
    811        if (popup.popupOpen) {
    812          let suggestionsHidden = popup.richlistbox.hasAttribute("collapsed");
    813          let numItems = suggestionsHidden ? 0 : popup.matchCount;
    814          return popup.oneOffButtons.handleKeyDown(aEvent, numItems, true);
    815        } else if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) {
    816          if (this.textbox.editor.canUndo) {
    817            this.textbox.editor.undoAll();
    818          } else {
    819            this.textbox.select();
    820          }
    821          aEvent.preventDefault();
    822          return true;
    823        }
    824        return false;
    825      };
    826 
    827      // This method overrides the autocomplete binding's openPopup (essentially
    828      // duplicating the logic from the autocomplete popup binding's
    829      // openAutocompletePopup method), modifying it so that the popup is aligned with
    830      // the inner textbox, but sized to not extend beyond the search bar border.
    831      this.textbox.openPopup = () => {
    832        // Entering customization mode after the search bar had focus causes
    833        // the popup to appear again, due to focus returning after the
    834        // hamburger panel closes. Don't open in that spurious event.
    835        if (document.documentElement.hasAttribute("customizing")) {
    836          return;
    837        }
    838 
    839        let popup = this.textbox.popup;
    840        let searchIcon = document.querySelector(".searchbar-search-button");
    841        if (!popup.mPopupOpen) {
    842          // Initially the panel used for the searchbar (PopupSearchAutoComplete
    843          // in browser.xhtml) is hidden to avoid impacting startup / new
    844          // window performance. The base binding's openPopup would normally
    845          // call the overriden openAutocompletePopup in
    846          // browser-search-autocomplete-result-popup binding to unhide the popup,
    847          // but since we're overriding openPopup we need to unhide the panel
    848          // ourselves.
    849          popup.hidden = false;
    850 
    851          // Don't roll up on mouse click in the anchor for the search UI.
    852          if (popup.id == "PopupSearchAutoComplete") {
    853            popup.setAttribute("norolluponanchor", "true");
    854          }
    855 
    856          popup.mInput = this.textbox;
    857          // clear any previous selection, see bugs 400671 and 488357
    858          popup.selectedIndex = -1;
    859 
    860          // Ensure the panel has a meaningful initial size and doesn't grow
    861          // unconditionally.
    862          let { width } = window.windowUtils.getBoundsWithoutFlushing(this);
    863          if (popup.oneOffButtons) {
    864            // We have a min-width rule on search-panel-one-offs to show at
    865            // least 4 buttons, so take that into account here.
    866            width = Math.max(width, popup.oneOffButtons.buttonWidth * 4);
    867          }
    868 
    869          popup.style.setProperty("--panel-width", width + "px");
    870          popup._invalidate();
    871          popup.openPopup(this, "after_start");
    872          searchIcon.setAttribute("aria-expanded", "true");
    873        }
    874      };
    875 
    876      this.textbox.openSearch = () => {
    877        if (!this.textbox.popupOpen) {
    878          this.openSuggestionsPanel();
    879          return false;
    880        }
    881        return true;
    882      };
    883 
    884      this.textbox.handleEnter = event => {
    885        // Toggle the open state of the add-engine menu button if it's
    886        // selected.  We're using handleEnter for this instead of listening
    887        // for the command event because a command event isn't fired.
    888        if (
    889          this.textbox.selectedButton &&
    890          this.textbox.selectedButton.getAttribute("anonid") ==
    891            "addengine-menu-button"
    892        ) {
    893          this.textbox.selectedButton.open = !this.textbox.selectedButton.open;
    894          return true;
    895        }
    896        // Ignore blank search unless add search engine or
    897        // settings button is selected, see bugs 1894910 and 1903608.
    898        if (
    899          !this.textbox.value &&
    900          !(
    901            this.textbox.selectedButton?.getAttribute("id") ==
    902              "searchbar-anon-search-settings" ||
    903            this.textbox.selectedButton?.classList.contains(
    904              "searchbar-engine-one-off-add-engine"
    905            )
    906          )
    907        ) {
    908          if (event.shiftKey) {
    909            let engine = this.textbox.selectedButton?.engine;
    910            let { where, params } = this._whereToOpen(event);
    911            this.openSearchFormWhere(event, engine, where, params);
    912          }
    913          return true;
    914        }
    915        // Otherwise, "call super": do what the autocomplete binding's
    916        // handleEnter implementation does.
    917        return this.textbox.mController.handleEnter(false, event || null);
    918      };
    919 
    920      // override |onTextEntered| in autocomplete.xml
    921      this.textbox.onTextEntered = event => {
    922        this.textbox.editor.clearUndoRedo();
    923 
    924        let engine;
    925        let oneOff = this.textbox.selectedButton;
    926        if (oneOff) {
    927          if (!oneOff.engine) {
    928            oneOff.doCommand();
    929            return;
    930          }
    931          engine = oneOff.engine;
    932        }
    933        if (this.textbox.popupSelectedIndex != -1) {
    934          this.telemetrySelectedIndex = this.textbox.popupSelectedIndex;
    935          this.textbox.popupSelectedIndex = -1;
    936        }
    937        this.handleSearchCommand(event, engine);
    938      };
    939 
    940      this.textbox.onbeforeinput = event => {
    941        if (event.data && this._needBrowserFocusAtEnterKeyUp) {
    942          // Ignore char key input while processing enter key.
    943          event.preventDefault();
    944        }
    945      };
    946 
    947      this.textbox.onkeyup = () => {
    948        // Pressing Enter key while pressing Meta key, and next, even when
    949        // releasing Enter key before releasing Meta key, the keyup event is not
    950        // fired. Therefore, if Enter keydown is detecting, continue the post
    951        // processing for Enter key when any keyup event is detected.
    952        if (this._needBrowserFocusAtEnterKeyUp) {
    953          this._needBrowserFocusAtEnterKeyUp = false;
    954          gBrowser.selectedBrowser.focus();
    955        }
    956      };
    957    }
    958 
    959    _buildContextMenu() {
    960      const raw = `
    961        <menuitem data-l10n-id="text-action-undo" cmd="cmd_undo"/>
    962        <menuitem data-l10n-id="text-action-redo" cmd="cmd_redo"/>
    963        <menuseparator/>
    964        <menuitem data-l10n-id="text-action-cut" cmd="cmd_cut"/>
    965        <menuitem data-l10n-id="text-action-copy" cmd="cmd_copy"/>
    966        <menuitem data-l10n-id="text-action-paste" cmd="cmd_paste"/>
    967        <menuitem class="searchbar-paste-and-search"/>
    968        <menuitem data-l10n-id="text-action-delete" cmd="cmd_delete"/>
    969        <menuitem data-l10n-id="text-action-select-all" cmd="cmd_selectAll"/>
    970        <menuseparator/>
    971        <menuitem class="searchbar-clear-history"/>
    972      `;
    973 
    974      this._menupopup = this.querySelector(".textbox-contextmenu");
    975 
    976      let frag = MozXULElement.parseXULToFragment(raw);
    977 
    978      // Insert attributes that come from localized properties
    979      this._pasteAndSearchMenuItem = frag.querySelector(
    980        ".searchbar-paste-and-search"
    981      );
    982      this._pasteAndSearchMenuItem.setAttribute(
    983        "label",
    984        this._stringBundle.getString("cmd_pasteAndSearch")
    985      );
    986 
    987      let clearHistoryItem = frag.querySelector(".searchbar-clear-history");
    988      clearHistoryItem.setAttribute(
    989        "label",
    990        this._stringBundle.getString("cmd_clearHistory")
    991      );
    992      clearHistoryItem.setAttribute(
    993        "accesskey",
    994        this._stringBundle.getString("cmd_clearHistory_accesskey")
    995      );
    996 
    997      this._menupopup.appendChild(frag);
    998 
    999      this._menupopup.addEventListener("command", event => {
   1000        switch (event.originalTarget) {
   1001          case this._pasteAndSearchMenuItem:
   1002            this.select();
   1003            goDoCommand("cmd_paste");
   1004            this.handleSearchCommand(event);
   1005            break;
   1006          case clearHistoryItem: {
   1007            let param = this.textbox.getAttribute("autocompletesearchparam");
   1008            lazy.FormHistory.update({ op: "remove", fieldname: param });
   1009            this.textbox.value = "";
   1010            break;
   1011          }
   1012          default: {
   1013            let cmd = event.originalTarget.getAttribute("cmd");
   1014            if (cmd) {
   1015              let controller =
   1016                document.commandDispatcher.getControllerForCommand(cmd);
   1017              controller.doCommand(cmd);
   1018            }
   1019            break;
   1020          }
   1021        }
   1022      });
   1023    }
   1024  }
   1025 
   1026  customElements.define("searchbar", MozSearchbar);
   1027 }