tor-browser

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

autocomplete-popup.js (10593B)


      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 // Wrap in a block to prevent leaking to window scope.
      8 {
      9  const lazy = {};
     10  ChromeUtils.defineESModuleGetters(lazy, {
     11    BrowserSearchTelemetry:
     12      "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     13    BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     14    SearchOneOffs: "moz-src:///browser/components/search/SearchOneOffs.sys.mjs",
     15  });
     16 
     17  /**
     18   * A richlistbox popup custom element for for a browser search autocomplete
     19   * widget.
     20   */
     21  class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup {
     22    constructor() {
     23      super();
     24 
     25      this.addEventListener("popupshowing", () => {
     26        // First handle deciding if we are showing the reduced version of the
     27        // popup containing only the preferences button. We do this if the
     28        // glass icon has been clicked if the text field is empty.
     29        if (this.searchbar.hasAttribute("showonlysettings")) {
     30          this.searchbar.removeAttribute("showonlysettings");
     31          this.setAttribute("showonlysettings", "true");
     32 
     33          // Setting this with an xbl-inherited attribute gets overridden the
     34          // second time the user clicks the glass icon for some reason...
     35          this.richlistbox.collapsed = true;
     36        } else {
     37          this.removeAttribute("showonlysettings");
     38          // Uncollapse as long as we have a view which has >= 1 row.
     39          // The autocomplete binding itself will take care of uncollapsing later,
     40          // if we currently have no rows but end up having some in the future
     41          // when the search string changes
     42          this.richlistbox.collapsed = this.matchCount == 0;
     43        }
     44 
     45        // Show the current default engine in the top header of the panel.
     46        this.updateHeader().catch(console.error);
     47 
     48        this._oneOffButtons.addEventListener(
     49          "SelectedOneOffButtonChanged",
     50          this
     51        );
     52      });
     53 
     54      this.addEventListener("popuphiding", () => {
     55        this._oneOffButtons.removeEventListener(
     56          "SelectedOneOffButtonChanged",
     57          this
     58        );
     59      });
     60 
     61      /**
     62       * This handles clicks on the topmost "Foo Search" header in the
     63       * popup (hbox.search-panel-header]).
     64       */
     65      this.addEventListener("click", event => {
     66        if (event.button == 2) {
     67          // Ignore right clicks.
     68          return;
     69        }
     70        let button = event.originalTarget;
     71        let engine = button.parentNode.engine;
     72        if (!engine) {
     73          return;
     74        }
     75        if (this.searchbar.value) {
     76          this.oneOffButtons.handleSearchCommand(event, engine);
     77        } else if (event.shiftKey) {
     78          this.openSearchForm(event, engine);
     79        }
     80      });
     81 
     82      this._bundle = null;
     83    }
     84 
     85    static get inheritedAttributes() {
     86      return {
     87        ".search-panel-current-engine": "showonlysettings",
     88        ".searchbar-engine-image": "src",
     89      };
     90    }
     91 
     92    // We override this because even though we have a shadow root, we want our
     93    // inheritance to be done on the light tree.
     94    getElementForAttrInheritance(selector) {
     95      return this.querySelector(selector);
     96    }
     97 
     98    initialize() {
     99      super.initialize();
    100      this.initializeAttributeInheritance();
    101 
    102      this._searchOneOffsContainer = this.querySelector(".search-one-offs");
    103      this._searchbarEngine = this.querySelector(".search-panel-header");
    104      this._searchbarEngineName = this.querySelector(".searchbar-engine-name");
    105      this._oneOffButtons = new lazy.SearchOneOffs(
    106        this._searchOneOffsContainer
    107      );
    108      this._searchbar = document.getElementById("searchbar");
    109    }
    110 
    111    get oneOffButtons() {
    112      if (!this._oneOffButtons) {
    113        this.initialize();
    114      }
    115      return this._oneOffButtons;
    116    }
    117 
    118    static get markup() {
    119      return `
    120      <hbox class="search-panel-header search-panel-current-engine">
    121        <image class="searchbar-engine-image"/>
    122        <label class="searchbar-engine-name" flex="1" crop="end" role="presentation"/>
    123      </hbox>
    124      <menuseparator class="searchbar-separator"/>
    125      <richlistbox class="autocomplete-richlistbox search-panel-tree"/>
    126      <menuseparator class="searchbar-separator"/>
    127      <hbox class="search-one-offs" is_searchbar="true"/>
    128    `;
    129    }
    130 
    131    get searchOneOffsContainer() {
    132      if (!this._searchOneOffsContainer) {
    133        this.initialize();
    134      }
    135      return this._searchOneOffsContainer;
    136    }
    137 
    138    get searchbarEngine() {
    139      if (!this._searchbarEngine) {
    140        this.initialize();
    141      }
    142      return this._searchbarEngine;
    143    }
    144 
    145    get searchbarEngineName() {
    146      if (!this._searchbarEngineName) {
    147        this.initialize();
    148      }
    149      return this._searchbarEngineName;
    150    }
    151 
    152    get searchbar() {
    153      if (!this._searchbar) {
    154        this.initialize();
    155      }
    156      return this._searchbar;
    157    }
    158 
    159    get bundle() {
    160      if (!this._bundle) {
    161        const kBundleURI = "chrome://browser/locale/search.properties";
    162        this._bundle = Services.strings.createBundle(kBundleURI);
    163      }
    164      return this._bundle;
    165    }
    166 
    167    openAutocompletePopup(aInput, aElement) {
    168      // initially the panel is hidden
    169      // to avoid impacting startup / new window performance
    170      aInput.popup.hidden = false;
    171 
    172      // this method is defined on the base binding
    173      this._openAutocompletePopup(aInput, aElement);
    174    }
    175 
    176    onPopupClick(aEvent) {
    177      // Ignore all right-clicks
    178      if (aEvent.button == 2) {
    179        return;
    180      }
    181 
    182      this.searchbar.telemetrySelectedIndex = this.selectedIndex;
    183 
    184      // Check for unmodified left-click, and use default behavior
    185      if (
    186        aEvent.button == 0 &&
    187        !aEvent.shiftKey &&
    188        !aEvent.ctrlKey &&
    189        !aEvent.altKey &&
    190        !aEvent.metaKey
    191      ) {
    192        this.input.controller.handleEnter(true, aEvent);
    193        return;
    194      }
    195 
    196      // Check for middle-click or modified clicks on the search bar
    197      lazy.BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod(
    198        aEvent,
    199        this.selectedIndex
    200      );
    201 
    202      // Handle search bar popup clicks
    203      let search = this.input.controller.getValueAt(this.selectedIndex);
    204 
    205      // open the search results according to the clicking subtlety
    206      let where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
    207      let params = {};
    208 
    209      // But open ctrl/cmd clicks on autocomplete items in a new background tab.
    210      let modifier =
    211        AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
    212      if (
    213        where == "tab" &&
    214        MouseEvent.isInstance(aEvent) &&
    215        (aEvent.button == 1 || modifier)
    216      ) {
    217        params.inBackground = true;
    218      }
    219 
    220      // leave the popup open for background tab loads
    221      if (!(where == "tab" && params.inBackground)) {
    222        // close the autocomplete popup and revert the entered search term
    223        this.closePopup();
    224        this.input.controller.handleEscape();
    225      }
    226 
    227      this.searchbar.doSearch(search, where, null, params);
    228      if (where == "tab" && params.inBackground) {
    229        this.searchbar.focus();
    230      } else {
    231        this.searchbar.value = search;
    232      }
    233    }
    234 
    235    /**
    236     * @type {string}
    237     *   The current engine name being displayed in updateHeader.
    238     */
    239    #currentEngineName;
    240 
    241    /**
    242     * Updates the header of the pop-up with the search engine name and icon.
    243     *
    244     * @param {nsISearchEngine} [engine]
    245     *   The engine to use, if not specified falls back to the default engine.
    246     */
    247    async updateHeader(engine) {
    248      if (!engine) {
    249        if (PrivateBrowsingUtils.isWindowPrivate(window)) {
    250          engine = await Services.search.getDefaultPrivate();
    251        } else {
    252          engine = await Services.search.getDefault();
    253        }
    254      }
    255      this.#currentEngineName = engine.name;
    256 
    257      let uri = await engine.getIconURL();
    258 
    259      // If the engine name has changed since we started loading, this means
    260      // that getIconURL probably took a long time and we had an update in
    261      // the meantime. Hence we skip updating to avoid displaying the wrong
    262      // thing.
    263      if (engine.name != this.#currentEngineName) {
    264        return;
    265      }
    266 
    267      if (uri) {
    268        this.setAttribute("src", uri);
    269      } else {
    270        // If the default has just been changed to a provider without icon,
    271        // avoid showing the icon of the previous default provider.
    272        this.removeAttribute("src");
    273      }
    274 
    275      let headerText = this.bundle.formatStringFromName("searchHeader", [
    276        engine.name,
    277      ]);
    278      this.searchbarEngineName.setAttribute("value", headerText);
    279      this.searchbarEngine.engine = engine;
    280    }
    281 
    282    /**
    283     * This is called when a one-off is clicked and when "search in new tab"
    284     * is selected from a one-off context menu.
    285     *
    286     * @param {Event} event
    287     *   The event that triggered the search.
    288     * @param {nsISearchEngine} engine
    289     *   The search engine being used for the search.
    290     * @param {string} where
    291     *   Where the search should be opened (current tab, new tab, window etc).
    292     * @param {object} params
    293     *   The parameters associated with opening the search.
    294     */
    295    handleOneOffSearch(event, engine, where, params) {
    296      this.searchbar.handleSearchCommandWhere(event, engine, where, params);
    297    }
    298 
    299    openSearchForm(event, engine, forceNewTab = false) {
    300      let { where, params } = this.oneOffButtons._whereToOpen(
    301        event,
    302        forceNewTab
    303      );
    304      this.searchbar.openSearchFormWhere(event, engine, where, params);
    305    }
    306 
    307    /**
    308     * Passes DOM events for the popup to the _on_<event type> methods.
    309     *
    310     * @param {Event} event
    311     *   DOM event from the <popup>.
    312     */
    313    handleEvent(event) {
    314      let methodName = "_on_" + event.type;
    315      if (methodName in this) {
    316        this[methodName](event);
    317      } else {
    318        throw new Error("Unrecognized UrlbarView event: " + event.type);
    319      }
    320    }
    321    _on_SelectedOneOffButtonChanged() {
    322      let engine =
    323        this.oneOffButtons.selectedButton &&
    324        this.oneOffButtons.selectedButton.engine;
    325      this.updateHeader(engine).catch(console.error);
    326    }
    327  }
    328 
    329  customElements.define(
    330    "search-autocomplete-richlistbox-popup",
    331    MozSearchAutocompleteRichlistboxPopup,
    332    {
    333      extends: "panel",
    334    }
    335  );
    336 }