tor-browser

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

contentSearchHandoffUI.mjs (7479B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { html } from "chrome://global/content/vendor/lit.all.mjs";
      6 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
      7 
      8 /**
      9 * Handles handing off searches from an in-page search input field to the
     10 * browser's main URL bar. Communicates with the parent via the ContentSearch
     11 * actor, using custom events to talk to the child actor.
     12 */
     13 class ContentSearchHandoffUIController {
     14  #ui = null;
     15  #shadowRoot = null;
     16 
     17  constructor(ui) {
     18    this._isPrivateEngine = false;
     19    this._engineIcon = null;
     20    this.#ui = ui;
     21    this.#shadowRoot = ui.shadowRoot;
     22 
     23    window.addEventListener("ContentSearchService", this);
     24    this._sendMsg("GetEngine");
     25    this._sendMsg("GetHandoffSearchModePrefs");
     26  }
     27 
     28  handleEvent(event) {
     29    let methodName = "_onMsg" + event.detail.type;
     30    if (methodName in this) {
     31      this[methodName](event.detail.data);
     32    }
     33  }
     34 
     35  get defaultEngine() {
     36    return this._defaultEngine;
     37  }
     38 
     39  doSearchHandoff(text) {
     40    this._sendMsg("SearchHandoff", { text });
     41  }
     42 
     43  static privateBrowsingRegex = /^about:privatebrowsing([#?]|$)/i;
     44  get _isAboutPrivateBrowsing() {
     45    return ContentSearchHandoffUIController.privateBrowsingRegex.test(
     46      document.location.href
     47    );
     48  }
     49 
     50  _onMsgEngine({ isPrivateEngine, engine }) {
     51    this._isPrivateEngine = isPrivateEngine;
     52    this._updateEngine(engine);
     53  }
     54 
     55  _onMsgCurrentEngine(engine) {
     56    if (!this._isPrivateEngine) {
     57      this._updateEngine(engine);
     58    }
     59  }
     60 
     61  _onMsgCurrentPrivateEngine(engine) {
     62    if (this._isPrivateEngine) {
     63      this._updateEngine(engine);
     64    }
     65  }
     66 
     67  _onMsgHandoffSearchModePrefs(pref) {
     68    this._shouldHandOffToSearchMode = pref;
     69    this._updatel10nIds();
     70  }
     71 
     72  _onMsgDisableSearch() {
     73    this.#ui.disabled = true;
     74  }
     75 
     76  _onMsgShowSearch() {
     77    this.#ui.disabled = false;
     78    this.#ui.fakeFocus = false;
     79  }
     80 
     81  _updateEngine(engine) {
     82    this._defaultEngine = engine;
     83    if (this._engineIcon) {
     84      URL.revokeObjectURL(this._engineIcon);
     85    }
     86 
     87    // We only show the engines icon for config engines, otherwise show
     88    // a default. xref https://bugzilla.mozilla.org/show_bug.cgi?id=1449338#c19
     89    if (!engine.isConfigEngine) {
     90      this._engineIcon = "chrome://global/skin/icons/search-glass.svg";
     91    } else if (engine.iconData) {
     92      this._engineIcon = this._getFaviconURIFromIconData(engine.iconData);
     93    } else {
     94      this._engineIcon = "chrome://global/skin/icons/defaultFavicon.svg";
     95    }
     96 
     97    document.body.style.setProperty(
     98      "--newtab-search-icon",
     99      "url(" + this._engineIcon + ")"
    100    );
    101    this._updatel10nIds();
    102  }
    103 
    104  _updatel10nIds() {
    105    let engine = this._defaultEngine;
    106    let fakeButton = this.#shadowRoot.querySelector(".search-handoff-button");
    107    let fakeInput = this.#shadowRoot.querySelector(".fake-textbox");
    108    if (!fakeButton || !fakeInput) {
    109      return;
    110    }
    111    if (!engine || this._shouldHandOffToSearchMode) {
    112      document.l10n.setAttributes(
    113        fakeButton,
    114        this._isAboutPrivateBrowsing
    115          ? "about-private-browsing-search-btn"
    116          : "newtab-search-box-input"
    117      );
    118      document.l10n.setAttributes(
    119        fakeInput,
    120        this._isAboutPrivateBrowsing
    121          ? "about-private-browsing-search-placeholder"
    122          : "newtab-search-box-text"
    123      );
    124    } else if (!engine.isConfigEngine) {
    125      document.l10n.setAttributes(
    126        fakeButton,
    127        this._isAboutPrivateBrowsing
    128          ? "about-private-browsing-handoff-no-engine"
    129          : "newtab-search-box-handoff-input-no-engine"
    130      );
    131      document.l10n.setAttributes(
    132        fakeInput,
    133        this._isAboutPrivateBrowsing
    134          ? "about-private-browsing-handoff-text-no-engine"
    135          : "newtab-search-box-handoff-text-no-engine"
    136      );
    137    } else {
    138      document.l10n.setAttributes(
    139        fakeButton,
    140        this._isAboutPrivateBrowsing
    141          ? "about-private-browsing-handoff"
    142          : "newtab-search-box-handoff-input",
    143        {
    144          engine: engine.name,
    145        }
    146      );
    147      document.l10n.setAttributes(
    148        fakeInput,
    149        this._isAboutPrivateBrowsing
    150          ? "about-private-browsing-handoff-text"
    151          : "newtab-search-box-handoff-text",
    152        {
    153          engine: engine.name,
    154        }
    155      );
    156    }
    157  }
    158 
    159  /**
    160   * If the favicon is an iconData object, convert it into a Blob URI.
    161   * Otherwise just return the plain URI.
    162   *
    163   * @param {string|iconData} data
    164   *   The icon's URL or an iconData object containing the icon data.
    165   * @returns {string}
    166   *   A blob URL or the plain icon URI.
    167   */
    168  _getFaviconURIFromIconData(data) {
    169    if (typeof data == "string") {
    170      return data;
    171    }
    172 
    173    // If typeof(data) != "string", the iconData object is returned.
    174    let blob = new Blob([data.icon], { type: data.mimeType });
    175    return URL.createObjectURL(blob);
    176  }
    177 
    178  _sendMsg(type, data = null) {
    179    dispatchEvent(
    180      new CustomEvent("ContentSearchClient", {
    181        detail: {
    182          type,
    183          data,
    184        },
    185      })
    186    );
    187  }
    188 }
    189 
    190 window.ContentSearchHandoffUIController = ContentSearchHandoffUIController;
    191 
    192 /**
    193 * This custom element encapsulates the UI for the search handoff experience
    194 * for about:newtab and about:privatebrowsing. It is a temporary component
    195 * while we wait for the multi-context address bar (MCAB) to be available.
    196 */
    197 class ContentSearchHandoffUI extends MozLitElement {
    198  static queries = {
    199    fakeCaret: ".fake-caret",
    200  };
    201 
    202  static properties = {
    203    fakeFocus: { type: Boolean, reflect: true },
    204    disabled: { type: Boolean, reflect: true },
    205  };
    206 
    207  #controller = null;
    208 
    209  #doSearchHandoff(text = "") {
    210    this.fakeFocus = true;
    211    this.#controller.doSearchHandoff(text);
    212  }
    213 
    214  #onSearchHandoffClick(event) {
    215    // When search hand-off is enabled, we render a big button that is styled to
    216    // look like a search textbox. If the button is clicked, we style
    217    // the button as if it was a focused search box and show a fake cursor but
    218    // really focus the awesomebar without the focus styles ("hidden focus").
    219    event.preventDefault();
    220    this.#doSearchHandoff();
    221  }
    222 
    223  #onSearchHandoffPaste(event) {
    224    event.preventDefault();
    225    this.#doSearchHandoff(event.clipboardData.getData("Text"));
    226  }
    227 
    228  #onSearchHandoffDrop(event) {
    229    event.preventDefault();
    230    let text = event.dataTransfer.getData("text");
    231    if (text) {
    232      this.#doSearchHandoff(text);
    233    }
    234  }
    235 
    236  connectedCallback() {
    237    super.connectedCallback();
    238    if (!this.#controller) {
    239      this.#controller = new window.ContentSearchHandoffUIController(this);
    240    }
    241  }
    242 
    243  render() {
    244    return html`
    245      <link
    246        rel="stylesheet"
    247        href="chrome://browser/content/contentSearchHandoffUI.css"
    248      />
    249      <button
    250        class="search-handoff-button"
    251        @click=${this.#onSearchHandoffClick}
    252        tabindex="-1"
    253      >
    254        <div class="fake-textbox"></div>
    255        <input
    256          type="search"
    257          class="fake-editable"
    258          tabindex="-1"
    259          aria-hidden="true"
    260          @drop=${this.#onSearchHandoffDrop}
    261          @paste=${this.#onSearchHandoffPaste}
    262        />
    263        <div class="fake-caret"></div>
    264      </button>
    265    `;
    266  }
    267 }
    268 
    269 customElements.define("content-search-handoff-ui", ContentSearchHandoffUI);