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);