tor-browser

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

ActionsProviderQuickActions.sys.mjs (5876B)


      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 {
      6  ActionsProvider,
      7  ActionsResult,
      8 } from "moz-src:///browser/components/urlbar/ActionsProvider.sys.mjs";
      9 
     10 const lazy = {};
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  QuickActionsLoaderDefault:
     13    "moz-src:///browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs",
     14  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     15 });
     16 
     17 // These prefs are relative to the `browser.urlbar` branch.
     18 const ENABLED_PREF = "suggest.quickactions";
     19 const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase";
     20 const MIN_SEARCH_PREF = "quickactions.minimumSearchString";
     21 
     22 /**
     23 * @typedef QuickActionsDefinition
     24 * @property {string[]} commands
     25 *   The possible typed entries that this command will be displayed for.
     26 * @property {string} icon
     27 *   The URI of the icon associated with this command.
     28 * @property {string} label
     29 *   The id of the label for the result element.
     30 * @property {() => boolean} [isVisible]
     31 *   A function to call to check if this action should be visible or not.
     32 * @property {() => null|{focusContent: boolean}} onPick
     33 *   The function to call when the quick action is picked. It may return an object
     34 *   with property focusContent to indicate if the content area should be focussed
     35 *   after the pick.
     36 */
     37 
     38 /**
     39 * A provider that matches the urlbar input to built in actions.
     40 */
     41 class ProviderQuickActions extends ActionsProvider {
     42  get name() {
     43    return "ActionsProviderQuickActions";
     44  }
     45 
     46  isActive(queryContext) {
     47    return (
     48      queryContext.sapName == "urlbar" &&
     49      lazy.UrlbarPrefs.get(ENABLED_PREF) &&
     50      !queryContext.searchMode &&
     51      queryContext.trimmedSearchString.length < 50 &&
     52      queryContext.trimmedSearchString.length >=
     53        lazy.UrlbarPrefs.get(MIN_SEARCH_PREF)
     54    );
     55  }
     56 
     57  async queryActions(queryContext) {
     58    let input = queryContext.trimmedLowerCaseSearchString;
     59    let results = await this.getActions({ input });
     60 
     61    if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) {
     62      for (let [keyword, keys] of this.#keywords) {
     63        if (input.includes(keyword) && keys.length) {
     64          keys.forEach(key => results.add(key));
     65        }
     66      }
     67    }
     68 
     69    // Remove invisible actions.
     70    results.forEach(key => {
     71      const action = this.#actions.get(key);
     72      if (!(action.isVisible?.() ?? true)) {
     73        results.delete(key);
     74      }
     75    });
     76 
     77    if (!results.size) {
     78      return null;
     79    }
     80 
     81    return [...results].map(key => {
     82      let action = this.#actions.get(key);
     83      return new ActionsResult({
     84        key,
     85        l10nId: action.label,
     86        icon: action.icon,
     87        dataset: {
     88          action: key,
     89          inputLength: queryContext.trimmedSearchString.length,
     90        },
     91        onPick: action.onPick,
     92      });
     93    });
     94  }
     95 
     96  async getActions({ input, includesExactMatch = false }) {
     97    await lazy.QuickActionsLoaderDefault.ensureLoaded();
     98 
     99    let results = this.#prefixes.get(input) ?? new Set();
    100 
    101    if (includesExactMatch) {
    102      let actions = this.#keywords.get(input);
    103      actions?.forEach(action => results.add(action));
    104    }
    105 
    106    return results;
    107  }
    108 
    109  getAction(key) {
    110    return this.#actions.get(key);
    111  }
    112 
    113  pickAction(_queryContext, _controller, element) {
    114    let action = element.dataset.action;
    115    let inputLength = Math.min(element.dataset.inputLength, 10);
    116    Glean.urlbarQuickaction.picked[`${action}-${inputLength}`].add(1);
    117    let options = this.#actions.get(action).onPick();
    118    if (options?.focusContent) {
    119      element.ownerGlobal.gBrowser.selectedBrowser.focus();
    120    }
    121  }
    122 
    123  /**
    124   * Adds a new QuickAction.
    125   *
    126   * @param {string} key A key to identify this action.
    127   * @param {QuickActionsDefinition} definition An object that describes the action.
    128   */
    129  addAction(key, definition) {
    130    this.#actions.set(key, definition);
    131    definition.commands.forEach(cmd => {
    132      let keys = this.#keywords.get(cmd) ?? [];
    133      keys.push(key);
    134      this.#keywords.set(cmd, keys);
    135    });
    136    this.#loopOverPrefixes(definition.commands, prefix => {
    137      let result = this.#prefixes.get(prefix);
    138      if (result) {
    139        result.add(key);
    140      } else {
    141        result = new Set([key]);
    142      }
    143      this.#prefixes.set(prefix, result);
    144    });
    145  }
    146 
    147  /**
    148   * Removes an action.
    149   *
    150   * @param {string} key A key to identify this action.
    151   */
    152  removeAction(key) {
    153    let definition = this.#actions.get(key);
    154    this.#actions.delete(key);
    155    definition.commands.forEach(cmd => {
    156      let keys = this.#keywords.get(cmd) ?? [];
    157      this.#keywords.set(
    158        cmd,
    159        keys.filter(k => k != key)
    160      );
    161    });
    162    this.#loopOverPrefixes(definition.commands, prefix => {
    163      let result = this.#prefixes.get(prefix);
    164      if (result) {
    165        result.delete(key);
    166      }
    167      this.#prefixes.set(prefix, result);
    168    });
    169  }
    170 
    171  /**
    172   * A map from keywords to an action.
    173   *
    174   * @type {Map<string, Array>}
    175   */
    176  #keywords = new Map();
    177 
    178  /**
    179   * A map of all prefixes to an array of actions.
    180   *
    181   * @type {Map<string, Set>}
    182   */
    183  #prefixes = new Map();
    184 
    185  /**
    186   * The actions that have been added.
    187   *
    188   * @type {Map<string, QuickActionsDefinition>}
    189   */
    190  #actions = new Map();
    191 
    192  #loopOverPrefixes(commands, fun) {
    193    for (const command of commands) {
    194      // Loop over all the prefixes of the word, ie
    195      // "", "w", "wo", "wor", stopping just before the full
    196      // word itself which will be matched by the whole
    197      // phrase matching.
    198      for (let i = 1; i <= command.length; i++) {
    199        let prefix = command.substring(0, command.length - i);
    200        fun(prefix);
    201      }
    202    }
    203  }
    204 }
    205 
    206 export var ActionsProviderQuickActions = new ProviderQuickActions();