/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; /** * Handles handing off searches from an in-page search input field to the * browser's main URL bar. Communicates with the parent via the ContentSearch * actor, using custom events to talk to the child actor. */ class ContentSearchHandoffUIController { #ui = null; #shadowRoot = null; /** * Whether or not we are in private browsing mode. */ #inPrivateBrowsing = false; constructor(ui) { this._isPrivateEngine = false; this._engineIcon = null; this.#ui = ui; this.#shadowRoot = ui.shadowRoot; window.addEventListener("ContentSearchService", this); this._sendMsg("GetEngine"); this._sendMsg("GetHandoffSearchModePrefs"); } handleEvent(event) { let methodName = "_onMsg" + event.detail.type; if (methodName in this) { this[methodName](event.detail.data); } } get defaultEngine() { return this._defaultEngine; } doSearchHandoff(text) { this._sendMsg("SearchHandoff", { text }); } static privateBrowsingRegex = /^about:privatebrowsing([#?]|$)/i; get _isAboutPrivateBrowsing() { return ContentSearchHandoffUIController.privateBrowsingRegex.test( document.location.href ); } _onMsgEngine({ inPrivateBrowsing, engine }) { this.#inPrivateBrowsing = inPrivateBrowsing; this._updateEngine(engine); } _onMsgCurrentEngine(engine) { if (!this.#inPrivateBrowsing) { this._updateEngine(engine); } } _onMsgCurrentPrivateEngine(engine) { if (this.#inPrivateBrowsing) { this._updateEngine(engine); } } _onMsgHandoffSearchModePrefs(pref) { this._shouldHandOffToSearchMode = pref; this._updatel10nIds(); } _onMsgDisableSearch() { this.#ui.disabled = true; } _onMsgShowSearch() { this.#ui.disabled = false; this.#ui.fakeFocus = false; } _updateEngine(engine) { this._defaultEngine = engine; if (this._engineIcon) { URL.revokeObjectURL(this._engineIcon); } // We only show the engines icon for config engines, otherwise show // a default. xref https://bugzilla.mozilla.org/show_bug.cgi?id=1449338#c19 if (!engine.isConfigEngine) { this._engineIcon = "chrome://global/skin/icons/search-glass.svg"; } else if (engine.iconData) { this._engineIcon = this._getFaviconURIFromIconData(engine.iconData); } else { this._engineIcon = "chrome://global/skin/icons/defaultFavicon.svg"; } document.body.style.setProperty( "--newtab-search-icon", "url(" + this._engineIcon + ")" ); this._updatel10nIds(); } _updatel10nIds() { let engine = this._defaultEngine; let fakeButton = this.#shadowRoot.querySelector(".search-handoff-button"); let fakeInput = this.#shadowRoot.querySelector(".fake-textbox"); if (!fakeButton || !fakeInput) { return; } if (!engine || this._shouldHandOffToSearchMode) { document.l10n.setAttributes( fakeButton, this._isAboutPrivateBrowsing ? "about-private-browsing-search-btn" : "newtab-search-box-input" ); document.l10n.setAttributes( fakeInput, this._isAboutPrivateBrowsing ? "about-private-browsing-search-placeholder" : "newtab-search-box-text" ); } else if (!engine.isConfigEngine) { document.l10n.setAttributes( fakeButton, this._isAboutPrivateBrowsing ? "about-private-browsing-handoff-no-engine" : "newtab-search-box-handoff-input-no-engine" ); document.l10n.setAttributes( fakeInput, this._isAboutPrivateBrowsing ? "about-private-browsing-handoff-text-no-engine" : "newtab-search-box-handoff-text-no-engine" ); } else { document.l10n.setAttributes( fakeButton, this._isAboutPrivateBrowsing ? "about-private-browsing-handoff" : "newtab-search-box-handoff-input", { engine: engine.name, } ); document.l10n.setAttributes( fakeInput, this._isAboutPrivateBrowsing ? "about-private-browsing-handoff-text" : "newtab-search-box-handoff-text", { engine: engine.name, } ); } } /** * If the favicon is an iconData object, convert it into a Blob URI. * Otherwise just return the plain URI. * * @param {string|iconData} data * The icon's URL or an iconData object containing the icon data. * @returns {string} * A blob URL or the plain icon URI. */ _getFaviconURIFromIconData(data) { if (typeof data == "string") { return data; } // If typeof(data) != "string", the iconData object is returned. let blob = new Blob([data.icon], { type: data.mimeType }); return URL.createObjectURL(blob); } _sendMsg(type, data = null) { dispatchEvent( new CustomEvent("ContentSearchClient", { detail: { type, data, }, }) ); } } window.ContentSearchHandoffUIController = ContentSearchHandoffUIController; /** * This custom element encapsulates the UI for the search handoff experience * for about:newtab and about:privatebrowsing. It is a temporary component * while we wait for the multi-context address bar (MCAB) to be available. */ class ContentSearchHandoffUI extends MozLitElement { static queries = { fakeCaret: ".fake-caret", }; static properties = { fakeFocus: { type: Boolean, reflect: true }, disabled: { type: Boolean, reflect: true }, }; #controller = null; #doSearchHandoff(text = "") { this.fakeFocus = true; this.#controller.doSearchHandoff(text); } #onSearchHandoffClick(event) { // When search hand-off is enabled, we render a big button that is styled to // look like a search textbox. If the button is clicked, we style // the button as if it was a focused search box and show a fake cursor but // really focus the awesomebar without the focus styles ("hidden focus"). event.preventDefault(); this.#doSearchHandoff(); } #onSearchHandoffPaste(event) { event.preventDefault(); this.#doSearchHandoff(event.clipboardData.getData("Text")); } #onSearchHandoffDrop(event) { event.preventDefault(); let text = event.dataTransfer.getData("text"); if (text) { this.#doSearchHandoff(text); } } connectedCallback() { super.connectedCallback(); if (!this.#controller) { this.#controller = new window.ContentSearchHandoffUIController(this); } } render() { return html` `; } } customElements.define("content-search-handoff-ui", ContentSearchHandoffUI);