SearchUIUtils.sys.mjs (16936B)
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 /** 6 * Various utilities for search related UI. 7 */ 8 9 /** 10 * @import { SearchUtils } from "moz-src:///toolkit/components/search/SearchUtils.sys.mjs" 11 * @import { UrlbarInput } from "chrome://browser/content/urlbar/UrlbarInput.mjs"; 12 */ 13 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 14 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 15 16 const lazy = XPCOMUtils.declareLazy({ 17 BrowserSearchTelemetry: 18 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 19 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 20 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 21 CustomizableUI: 22 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 24 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 25 SearchUIUtilsL10n: () => { 26 return new Localization(["browser/search.ftl", "branding/brand.ftl"]); 27 }, 28 }); 29 30 export var SearchUIUtils = { 31 initialized: false, 32 33 init() { 34 if (!this.initialized) { 35 Services.obs.addObserver(this, "browser-search-engine-modified"); 36 this.initialized = true; 37 } 38 }, 39 40 observe(engine, topic, data) { 41 switch (data) { 42 case "engine-default": 43 this.updatePlaceholderNamePreference(engine, false); 44 break; 45 case "engine-default-private": 46 this.updatePlaceholderNamePreference(engine, true); 47 break; 48 } 49 }, 50 51 /** 52 * This function is called by the category manager for the 53 * `search-service-notification` category. 54 * 55 * It allows the SearchService (in toolkit) to display 56 * notifications in the browser for certain events. 57 * 58 * @param {string} notificationType 59 * Determines the function displaying the notification. 60 * @param {...any} args 61 * The arguments for that function. 62 */ 63 showSearchServiceNotification(notificationType, ...args) { 64 switch (notificationType) { 65 case "search-engine-removal": { 66 let [oldEngine, newEngine] = args; 67 this.removalOfSearchEngineNotificationBox(oldEngine, newEngine); 68 break; 69 } 70 case "search-settings-reset": { 71 let [newEngine] = args; 72 this.searchSettingsResetNotificationBox(newEngine); 73 break; 74 } 75 } 76 }, 77 78 /** 79 * Infobar to notify the user's search engine has been removed 80 * and replaced with an application default search engine. 81 * 82 * @param {string} oldEngine 83 * name of the engine to be moved and replaced. 84 * @param {string} newEngine 85 * name of the application default engine to replaced the removed engine. 86 */ 87 async removalOfSearchEngineNotificationBox(oldEngine, newEngine) { 88 let win = lazy.BrowserWindowTracker.getTopWindow({ 89 allowFromInactiveWorkspace: true, 90 }); 91 92 let buttons = [ 93 { 94 "l10n-id": "remove-search-engine-button", 95 primary: true, 96 callback() { 97 const notificationBox = win.gNotificationBox.getNotificationWithValue( 98 "search-engine-removal" 99 ); 100 win.gNotificationBox.removeNotification(notificationBox); 101 }, 102 }, 103 { 104 supportPage: "search-engine-removal", 105 }, 106 ]; 107 108 await win.gNotificationBox.appendNotification( 109 "search-engine-removal", 110 { 111 label: { 112 "l10n-id": "removed-search-engine-message2", 113 "l10n-args": { oldEngine, newEngine }, 114 }, 115 priority: win.gNotificationBox.PRIORITY_SYSTEM, 116 }, 117 buttons 118 ); 119 120 // _updatePlaceholderFromDefaultEngine only updates the pref if the search service 121 // hasn't finished initializing, so we explicitly update it here to be sure. 122 SearchUIUtils.updatePlaceholderNamePreference( 123 await Services.search.getDefault(), 124 false 125 ); 126 SearchUIUtils.updatePlaceholderNamePreference( 127 await Services.search.getDefaultPrivate(), 128 true 129 ); 130 131 for (let openWin of lazy.BrowserWindowTracker.orderedWindows) { 132 openWin.gURLBar 133 ?._updatePlaceholderFromDefaultEngine() 134 .catch(console.error); 135 } 136 }, 137 138 /** 139 * Infobar informing the user that the search settings had to be reset 140 * and what their new default engine is. 141 * 142 * @param {string} newEngine 143 * Name of the new default engine. 144 */ 145 async searchSettingsResetNotificationBox(newEngine) { 146 let win = lazy.BrowserWindowTracker.getTopWindow({ 147 allowFromInactiveWorkspace: true, 148 }); 149 150 let buttons = [ 151 { 152 "l10n-id": "reset-search-settings-button", 153 primary: true, 154 callback() { 155 const notificationBox = win.gNotificationBox.getNotificationWithValue( 156 "search-settings-reset" 157 ); 158 win.gNotificationBox.removeNotification(notificationBox); 159 }, 160 }, 161 { 162 supportPage: "prefs-search", 163 }, 164 ]; 165 166 await win.gNotificationBox.appendNotification( 167 "search-settings-reset", 168 { 169 label: { 170 "l10n-id": "reset-search-settings-message", 171 "l10n-args": { newEngine }, 172 }, 173 priority: win.gNotificationBox.PRIORITY_SYSTEM, 174 }, 175 buttons 176 ); 177 }, 178 179 /** 180 * Adds an open search engine and handles error UI. 181 * 182 * @param {string} locationURL 183 * The URL where the OpenSearch definition is located. 184 * @param {string} image 185 * A URL string to an icon file to be used as the search engine's 186 * icon. This value may be overridden by an icon specified in the 187 * engine description file. 188 * @param {object} browsingContext 189 * The browsing context any error prompt should be opened for. 190 * @returns {Promise<boolean>} 191 * Returns true if the engine was added. 192 */ 193 async addOpenSearchEngine(locationURL, image, browsingContext) { 194 try { 195 await Services.search.addOpenSearchEngine( 196 locationURL, 197 image, 198 browsingContext?.embedderElement?.contentPrincipal?.originAttributes 199 ); 200 } catch (ex) { 201 let titleMsgName; 202 let descMsgName; 203 switch (ex.result) { 204 case Ci.nsISearchService.ERROR_DUPLICATE_ENGINE: 205 titleMsgName = "opensearch-error-duplicate-title"; 206 descMsgName = "opensearch-error-duplicate-desc"; 207 break; 208 case Ci.nsISearchService.ERROR_ENGINE_CORRUPTED: 209 titleMsgName = "opensearch-error-format-title"; 210 descMsgName = "opensearch-error-format-desc"; 211 break; 212 default: 213 // i.e. ERROR_DOWNLOAD_FAILURE 214 titleMsgName = "opensearch-error-download-title"; 215 descMsgName = "opensearch-error-download-desc"; 216 break; 217 } 218 219 let [title, text] = await lazy.SearchUIUtilsL10n.formatValues([ 220 { 221 id: titleMsgName, 222 }, 223 { 224 id: descMsgName, 225 args: { 226 "location-url": locationURL, 227 }, 228 }, 229 ]); 230 231 Services.prompt.alertBC( 232 browsingContext, 233 Ci.nsIPrompt.MODAL_TYPE_CONTENT, 234 title, 235 text 236 ); 237 return false; 238 } 239 return true; 240 }, 241 242 /** 243 * Returns the URL to use for where to get more search engines. 244 * 245 * @returns {string} 246 */ 247 get searchEnginesURL() { 248 return Services.urlFormatter.formatURLPref( 249 "browser.search.searchEnginesURL" 250 ); 251 }, 252 253 /** 254 * Update the placeholderName preference for the default search engine. 255 * 256 * @param {nsISearchEngine} engine The new default search engine. 257 * @param {boolean} isPrivate Whether this change applies to private windows. 258 */ 259 updatePlaceholderNamePreference(engine, isPrivate) { 260 const prefName = 261 "browser.urlbar.placeholderName" + (isPrivate ? ".private" : ""); 262 if (engine.isConfigEngine) { 263 Services.prefs.setStringPref(prefName, engine.name); 264 } else { 265 Services.prefs.clearUserPref(prefName); 266 } 267 }, 268 269 /** 270 * Focuses the search bar if present on the toolbar, or the address bar, 271 * putting it in search mode. Will do so in an existing non-popup browser 272 * window or open a new one if necessary. 273 * 274 * @param {WindowProxy} window 275 * The window where the seach was triggered. 276 */ 277 webSearch(window) { 278 if ( 279 window.location.href != AppConstants.BROWSER_CHROME_URL || 280 window.gURLBar.readOnly 281 ) { 282 let topWindow = lazy.BrowserWindowTracker.getTopWindow(); 283 if (topWindow && !topWindow.gURLBar.readOnly) { 284 // If there's an open browser window, it should handle this command. 285 topWindow.focus(); 286 SearchUIUtils.webSearch(topWindow); 287 } else { 288 // If there are no open browser windows, open a new one. 289 let newWindow = window.openDialog( 290 AppConstants.BROWSER_CHROME_URL, 291 "_blank", 292 "chrome,all,dialog=no", 293 "about:blank" 294 ); 295 296 let observer = subject => { 297 if (subject == newWindow) { 298 SearchUIUtils.webSearch(newWindow); 299 Services.obs.removeObserver( 300 observer, 301 "browser-delayed-startup-finished" 302 ); 303 } 304 }; 305 Services.obs.addObserver(observer, "browser-delayed-startup-finished"); 306 } 307 return; 308 } 309 310 /** @type {(searchBar: MozSearchbar | UrlbarInput) => void} */ 311 let focusUrlBarIfSearchFieldIsNotActive = function (searchBar) { 312 if (!searchBar || window.document.activeElement != searchBar.inputField) { 313 // Limit the results to search suggestions, like the search bar. 314 window.gURLBar.searchModeShortcut(); 315 } 316 }; 317 318 let searchBar = /** @type {MozSearchbar | UrlbarInput} */ ( 319 window.document.getElementById( 320 Services.prefs.getBoolPref("browser.search.widget.new") 321 ? "searchbar-new" 322 : "searchbar" 323 ) 324 ); 325 let placement = 326 lazy.CustomizableUI.getPlacementOfWidget("search-container"); 327 let focusSearchBar = () => { 328 searchBar = /** @type {MozSearchbar | UrlbarInput} */ ( 329 window.document.getElementById( 330 Services.prefs.getBoolPref("browser.search.widget.new") 331 ? "searchbar-new" 332 : "searchbar" 333 ) 334 ); 335 searchBar.select(); 336 focusUrlBarIfSearchFieldIsNotActive(searchBar); 337 }; 338 if ( 339 placement && 340 searchBar && 341 ((searchBar.parentElement.getAttribute("overflowedItem") == "true" && 342 placement.area == lazy.CustomizableUI.AREA_NAVBAR) || 343 placement.area == lazy.CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) 344 ) { 345 let navBar = window.document.getElementById( 346 lazy.CustomizableUI.AREA_NAVBAR 347 ); 348 // @ts-expect-error - Navbar receives the overflowable property upon registration. 349 navBar.overflowable.show().then(focusSearchBar); 350 return; 351 } 352 if (searchBar) { 353 if (window.fullScreen) { 354 window.FullScreen.showNavToolbox(); 355 } 356 searchBar.select(); 357 } 358 focusUrlBarIfSearchFieldIsNotActive(searchBar); 359 }, 360 361 /** 362 * Opens a search results page, given a set of search terms. 363 * 364 * @param {object} options 365 * Options objects. 366 * @param {WindowProxy} options.window 367 * The window where the search was triggered. 368 * @param {string} options.searchText 369 * The search terms to use for the search. 370 * @param {?string} [options.where] 371 * String indicating where the search should load. Most commonly used 372 * are ``tab`` or ``window``, defaults to ``current``. 373 * @param {boolean} [options.usePrivateWindow] 374 * Whether to open the window in private browsing mode (if opening a window). 375 * Defaults to the type of window that ``options.window` is. 376 * @param {nsIPrincipal} options.triggeringPrincipal 377 * The principal to use for a new window or tab. 378 * @param {nsIPolicyContainer} [options.policyContainer] 379 * The policyContainer to use for a new window or tab. 380 * @param {boolean} [options.inBackground] 381 * Set to true for the tab to be loaded in the background. 382 * @param {?nsISearchEngine} [options.engine] 383 * The search engine to use for the search. If not supplied, this will default 384 * to the default search engine for normal or private mode, depending on 385 * ``options.usePrivateWindow``. 386 * @param {?MozTabbrowserTab} [options.tab] 387 * The tab to show the search result. 388 * @param {?Values<typeof SearchUtils.URL_TYPE>} [options.searchUrlType] 389 * A `SearchUtils.URL_TYPE` value indicating the type of search that should 390 * be performed. A falsey value is equivalent to 391 * `SearchUtils.URL_TYPE.SEARCH`, which will perform a usual web search. 392 * @param {string} options.sapSource 393 * The search access point source, see 394 * {@link lazy.BrowserSearchTelemetry.KNOWN_SEARCH_SOURCES} 395 */ 396 async loadSearch({ 397 window, 398 searchText, 399 where, 400 usePrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window), 401 triggeringPrincipal, 402 policyContainer, 403 inBackground = false, 404 engine, 405 tab, 406 searchUrlType, 407 sapSource, 408 }) { 409 if (!triggeringPrincipal) { 410 throw new Error( 411 "Required argument triggeringPrincipal missing within loadSearch" 412 ); 413 } 414 415 if (!engine) { 416 engine = usePrivateWindow 417 ? await Services.search.getDefaultPrivate() 418 : await Services.search.getDefault(); 419 } 420 421 let submission = engine.getSubmission(searchText, searchUrlType); 422 423 // getSubmission can return null if the engine doesn't have a URL 424 // for the given response type. This is an error if it occurs, since 425 // we should only get here if the engine supports the URL type begin 426 // passed. 427 if (!submission) { 428 throw new Error(`No submission URL found for ${searchUrlType}`); 429 } 430 431 window.openLinkIn(submission.uri.spec, where || "current", { 432 private: usePrivateWindow, 433 postData: submission.postData, 434 inBackground, 435 relatedToCurrent: true, 436 triggeringPrincipal, 437 policyContainer, 438 targetBrowser: tab?.linkedBrowser, 439 globalHistoryOptions: { 440 triggeringSearchEngine: engine.name, 441 }, 442 }); 443 444 lazy.BrowserSearchTelemetry.recordSearch( 445 window.gBrowser.selectedBrowser, 446 engine, 447 sapSource, 448 { searchUrlType } 449 ); 450 }, 451 452 /** 453 * Perform a search initiated from the context menu. 454 * Note: This should only be called from the context menu. 455 * 456 * @param {object} options 457 * Options object. 458 * @param {nsISearchEngine} options.engine 459 * The engine to search with. 460 * @param {WindowProxy} options.window 461 * The window where the search was triggered. 462 * @param {string} options.searchText 463 * The search terms to use for the search. 464 * @param {boolean} [options.usePrivateWindow] 465 * Whether to open the window in private browsing mode (if opening a window). 466 * Defaults to the type of window that ``options.window` is. 467 * @param {nsIPrincipal} options.triggeringPrincipal 468 * The principal of the document whose context menu was clicked. 469 * @param {nsIPolicyContainer} options.policyContainer 470 * The policyContainer to use for a new window or tab. 471 * @param {XULCommandEvent|PointerEvent} options.event 472 * The event triggering the search. 473 * @param {?Values<typeof SearchUtils.URL_TYPE>} [options.searchUrlType] 474 * A `SearchUtils.URL_TYPE` value indicating the type of search that should 475 * be performed. A falsey value is equivalent to 476 * `SearchUtils.URL_TYPE.SEARCH` and will perform a usual web search. 477 */ 478 async loadSearchFromContext({ 479 window, 480 engine, 481 searchText, 482 usePrivateWindow, 483 triggeringPrincipal, 484 policyContainer, 485 event, 486 searchUrlType = null, 487 }) { 488 event = lazy.BrowserUtils.getRootEvent(event); 489 let where = lazy.BrowserUtils.whereToOpenLink(event); 490 if (where == "current") { 491 // override: historically search opens in new tab 492 where = "tab"; 493 } 494 if ( 495 usePrivateWindow && 496 !lazy.PrivateBrowsingUtils.isWindowPrivate(window) 497 ) { 498 where = "window"; 499 } 500 let inBackground = Services.prefs.getBoolPref( 501 "browser.search.context.loadInBackground" 502 ); 503 if (event.button == 1 || event.ctrlKey) { 504 inBackground = !inBackground; 505 } 506 507 return this.loadSearch({ 508 window, 509 engine, 510 searchText, 511 searchUrlType, 512 where, 513 usePrivateWindow, 514 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 515 triggeringPrincipal.originAttributes 516 ), 517 policyContainer, 518 inBackground, 519 sapSource: 520 searchUrlType == lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH 521 ? "contextmenu_visual" 522 : "contextmenu", 523 }); 524 }, 525 };