SmartbarInput.mjs (209753B)
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 { createEditor } from "chrome://browser/content/urlbar/SmartbarInputUtils.mjs"; 6 7 const { XPCOMUtils } = ChromeUtils.importESModule( 8 "resource://gre/modules/XPCOMUtils.sys.mjs" 9 ); 10 11 const { AppConstants } = ChromeUtils.importESModule( 12 "resource://gre/modules/AppConstants.sys.mjs" 13 ); 14 15 /** 16 * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs" 17 */ 18 19 const lazy = XPCOMUtils.declareLazy({ 20 ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", 21 BrowserSearchTelemetry: 22 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 23 BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", 24 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 25 ExtensionSearchHandler: 26 "resource://gre/modules/ExtensionSearchHandler.sys.mjs", 27 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 28 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 29 PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", 30 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 31 ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs", 32 SearchModeSwitcher: 33 "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs", 34 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 35 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 36 SmartbarInputController: 37 "chrome://browser/content/urlbar/SmartbarInputController.mjs", 38 UrlbarController: 39 "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs", 40 UrlbarEventBufferer: 41 "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs", 42 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 43 UrlbarQueryContext: 44 "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 45 UrlbarProviderGlobalActions: 46 "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs", 47 UrlbarProviderOpenTabs: 48 "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs", 49 UrlbarSearchUtils: 50 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 51 UrlbarTokenizer: 52 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 53 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 54 UrlbarValueFormatter: 55 "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs", 56 UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", 57 UrlbarSearchTermsPersistence: 58 "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs", 59 UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", 60 ClipboardHelper: { 61 service: "@mozilla.org/widget/clipboardhelper;1", 62 iid: Ci.nsIClipboardHelper, 63 }, 64 QueryStringStripper: { 65 service: "@mozilla.org/url-query-string-stripper;1", 66 iid: Ci.nsIURLQueryStringStripper, 67 }, 68 QUERY_STRIPPING_STRIP_ON_SHARE: { 69 pref: "privacy.query_stripping.strip_on_share.enabled", 70 default: false, 71 }, 72 logger: () => lazy.UrlbarUtils.getLogger({ prefix: "SmartbarInput" }), 73 }); 74 75 const UNLIMITED_MAX_RESULTS = 99; 76 77 let getBoundsWithoutFlushing = element => 78 element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element); 79 let px = number => number.toFixed(2) + "px"; 80 81 /** 82 * Implements the text input part of the address bar UI. 83 */ 84 export class SmartbarInput extends HTMLElement { 85 static get #markup() { 86 return ` 87 <hbox class="urlbar-background"/> 88 <hbox class="urlbar-input-container" 89 flex="1" 90 pageproxystate="invalid"> 91 <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot> 92 <toolbarbutton id="urlbar-searchmode-switcher" 93 class="searchmode-switcher chromeclass-toolbar-additional" 94 align="center" 95 aria-expanded="false" 96 aria-haspopup="menu" 97 tooltip="dynamic-shortcut-tooltip" 98 data-l10n-id="urlbar-searchmode-default" 99 type="menu"> 100 <image class="searchmode-switcher-icon toolbarbutton-icon"/> 101 <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker" 102 data-l10n-id="urlbar-searchmode-dropmarker" /> 103 <menupopup class="searchmode-switcher-popup toolbar-menupopup" 104 consumeoutsideclicks="false"> 105 <label class="searchmode-switcher-popup-description" 106 data-l10n-id="urlbar-searchmode-popup-description" 107 role="heading" /> 108 <menuseparator/> 109 <menuseparator class="searchmode-switcher-popup-footer-separator"/> 110 <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic" 111 data-action="openpreferences" 112 image="chrome://global/skin/icons/settings.svg" 113 data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/> 114 </menupopup> 115 </toolbarbutton> 116 <box class="searchmode-switcher-chicklet"> 117 <label class="searchmode-switcher-title" /> 118 <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button" 119 data-action="exitsearchmode" 120 role="button" 121 data-l10n-id="urlbar-searchmode-exit-button" /> 122 </box> 123 <moz-urlbar-slot name="site-info"> </moz-urlbar-slot> 124 <moz-input-box tooltip="aHTMLTooltip" 125 class="urlbar-input-box" 126 flex="1" 127 role="combobox" 128 aria-owns="urlbar-results"> 129 <html:input id="urlbar-scheme" 130 required="required"/> 131 <html:input id="urlbar-input" 132 class="urlbar-input textbox-input" 133 aria-controls="urlbar-results" 134 aria-autocomplete="both" 135 inputmode="mozAwesomebar" 136 data-l10n-id="smartbar-placeholder"/> 137 </moz-input-box> 138 <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot> 139 <image class="urlbar-icon urlbar-go-button" 140 role="button" 141 data-l10n-id="urlbar-go-button"/> 142 <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot> 143 </hbox> 144 <vbox class="urlbarView" 145 context="" 146 role="group" 147 tooltip="aHTMLTooltip"> 148 <html:div class="urlbarView-body-outer"> 149 <html:div class="urlbarView-body-inner"> 150 <html:div id="urlbar-results" 151 class="urlbarView-results" 152 role="listbox"/> 153 </html:div> 154 </html:div> 155 <menupopup class="urlbarView-result-menu" 156 consumeoutsideclicks="false"/> 157 <hbox class="search-one-offs" 158 includecurrentengine="true" 159 disabletab="true"/> 160 </vbox>`; 161 } 162 163 /** 164 * @type {DocumentFragment=} 165 * 166 * The cached fragment. 167 */ 168 static #fragment; 169 170 /** @type {DocumentFragment} */ 171 static get fragment() { 172 if (!this.#fragment) { 173 this.#fragment = window.MozXULElement.parseXULToFragment(this.#markup); 174 } 175 // @ts-ignore 176 return document.importNode(this.#fragment, true); 177 } 178 179 static #inputFieldEvents = [ 180 "compositionstart", 181 "compositionend", 182 "contextmenu", 183 "dragover", 184 "dragstart", 185 "drop", 186 "focus", 187 "blur", 188 "input", 189 "beforeinput", 190 "keydown", 191 "keyup", 192 "mouseover", 193 "overflow", 194 "underflow", 195 "paste", 196 "scrollend", 197 "select", 198 "selectionchange", 199 ]; 200 201 static #validSmartbarModes = ["chat", "search", "navigate"]; 202 203 #allowBreakout = false; 204 #gBrowserListenersAdded = false; 205 #breakoutBlockerCount = 0; 206 #isAddressbar = false; 207 /** 208 * `True` if this instance is in `smartbar` mode. 209 * 210 * Smartbar mode is enabled by adding the attribute `sap-name="smartbar"`. 211 * Both `#isSmartbarMode` and `#isAddressbar` are `false` if `sap-name` is neither 212 * `smartbar` nor `urlbar`. 213 */ 214 #isSmartbarMode = false; 215 #sapName = ""; 216 #smartbarEditor = null; 217 #smartbarInputController = null; 218 #smartbarMode = "search"; 219 _userTypedValue = ""; 220 _actionOverrideKeyCount = 0; 221 _lastValidURLStr = ""; 222 _valueOnLastSearch = ""; 223 _suppressStartQuery = false; 224 _suppressPrimaryAdjustment = false; 225 _lastSearchString = ""; 226 // Tracks IME composition. 227 #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; 228 #compositionClosedPopup = false; 229 230 valueIsTyped = false; 231 232 // Properties accessed in tests. 233 lastQueryContextPromise = Promise.resolve(); 234 _autofillPlaceholder = null; 235 _resultForCurrentValue = null; 236 _untrimmedValue = ""; 237 _enableAutofillPlaceholder = true; 238 239 constructor() { 240 super(); 241 242 this.window = this.ownerGlobal; 243 this.document = this.window.document; 244 this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window); 245 246 lazy.UrlbarPrefs.addObserver(this); 247 window.addEventListener("unload", () => { 248 // Stop listening to pref changes to make sure we don't init the new 249 // searchbar in closed windows that have not been gc'd yet. 250 lazy.UrlbarPrefs.removeObserver(this); 251 }); 252 } 253 254 /** 255 * Populates moz-urlbar-slots by moving all children with a urlbar-slot 256 * attribute into their moz-urlbar-slots and removing the slots. 257 * 258 * Should only be called once all children have been parsed. 259 */ 260 #populateSlots() { 261 let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]"); 262 for (let slot of urlbarSlots) { 263 let slotName = slot.getAttribute("name"); 264 let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`); 265 266 for (let node of nodes) { 267 slot.parentNode.insertBefore(node, slot); 268 } 269 270 slot.remove(); 271 } 272 273 // Slotted elements only used by the addressbar. 274 // Will be null for searchbar and others. 275 this._identityBox = this.querySelector(".identity-box"); 276 this._revertButton = this.querySelector(".urlbar-revert-button"); 277 // Pre scotch bonnet search mode indicator (addressbar only). 278 this._searchModeIndicator = this.querySelector( 279 "#urlbar-search-mode-indicator" 280 ); 281 this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector( 282 "#urlbar-search-mode-indicator-title" 283 ); 284 this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector( 285 "#urlbar-search-mode-indicator-close" 286 ); 287 } 288 289 /** 290 * Initialization that happens once on the first connect. 291 */ 292 #initOnce() { 293 this.#sapName = this.getAttribute("sap-name"); 294 this.#isAddressbar = this.#sapName == "urlbar"; 295 this.#isSmartbarMode = this.#sapName == "smartbar"; 296 297 // This listener must be added before connecting the fragment 298 // because the event could fire while or after connecting it. 299 this.addEventListener( 300 "moz-input-box-rebuilt", 301 this.#onContextMenuRebuilt.bind(this) 302 ); 303 304 this.appendChild(SmartbarInput.fragment); 305 306 // Make sure all children have been parsed before calling #populateSlots. 307 if (document.readyState === "loading") { 308 document.addEventListener( 309 "DOMContentLoaded", 310 () => this.#populateSlots(), 311 { once: true } 312 ); 313 } else { 314 this.#populateSlots(); 315 } 316 317 this.panel = this.querySelector(".urlbarView"); 318 this.inputField = /** @type {HTMLInputElement} */ ( 319 this.querySelector(".urlbar-input") 320 ); 321 if (this.#isSmartbarMode) { 322 this.#ensureSmartbarEditor(); 323 } 324 this._inputContainer = this.querySelector(".urlbar-input-container"); 325 326 this.controller = new lazy.UrlbarController({ input: this }); 327 this.view = new lazy.UrlbarView(this); 328 this.searchModeSwitcher = new lazy.SearchModeSwitcher(this); 329 330 // The event bufferer can be used to defer events that may affect users 331 // muscle memory; for example quickly pressing DOWN+ENTER should end up 332 // on a predictable result, regardless of the search status. The event 333 // bufferer will invoke the handling code at the right time. 334 this.eventBufferer = new lazy.UrlbarEventBufferer(this); 335 336 // Forward certain properties. 337 // Note if you are extending these, you'll also need to extend the inline 338 // type definitions. 339 const READ_WRITE_PROPERTIES = [ 340 "placeholder", 341 "readOnly", 342 "selectionStart", 343 "selectionEnd", 344 ]; 345 346 for (let property of READ_WRITE_PROPERTIES) { 347 Object.defineProperty(this, property, { 348 enumerable: true, 349 get() { 350 return this.inputField[property]; 351 }, 352 set(val) { 353 this.inputField[property] = val; 354 }, 355 }); 356 } 357 358 // The engine name is not known yet, but update placeholder anyway to 359 // reflect value of keyword.enabled or set the searchbar placeholder. 360 this._setPlaceholder(null); 361 } 362 363 connectedCallback() { 364 if ( 365 this.getAttribute("sap-name") == "searchbar" && 366 !lazy.UrlbarPrefs.get("browser.search.widget.new") 367 ) { 368 return; 369 } 370 371 this.#init(); 372 } 373 374 #init() { 375 if (!this.controller) { 376 this.#initOnce(); 377 } 378 379 if (this.sapName == "searchbar") { 380 this.parentNode.setAttribute("overflows", "false"); 381 } 382 383 // Don't attach event listeners if the toolbar is not visible 384 // in this window or the urlbar is readonly. 385 if ( 386 !this.window.toolbar.visible || 387 this.window.document.documentElement.hasAttribute("taskbartab") || 388 this.readOnly 389 ) { 390 return; 391 } 392 393 this._initCopyCutController(); 394 395 for (let event of SmartbarInput.#inputFieldEvents) { 396 this.inputField.addEventListener(event, this); 397 } 398 399 // These are on the window to detect focusing shortcuts like F6. 400 this.window.addEventListener("keydown", this); 401 this.window.addEventListener("keyup", this); 402 403 this.window.addEventListener("mousedown", this); 404 if (AppConstants.platform == "win") { 405 this.window.addEventListener("draggableregionleftmousedown", this); 406 } 407 this.addEventListener("mousedown", this); 408 409 // This listener handles clicks from our children too, included the search mode 410 // indicator close button. 411 this._inputContainer.addEventListener("click", this); 412 413 // This is used to detect commands launched from the panel, to avoid 414 // recording abandonment events when the command causes a blur event. 415 this.view.panel.addEventListener("command", this, true); 416 417 this.window.addEventListener("customizationstarting", this); 418 this.window.addEventListener("aftercustomization", this); 419 this.window.addEventListener("toolbarvisibilitychange", this); 420 let menuToolbar = this.window.document.getElementById("toolbar-menubar"); 421 if (menuToolbar) { 422 menuToolbar.addEventListener("DOMMenuBarInactive", this); 423 menuToolbar.addEventListener("DOMMenuBarActive", this); 424 } 425 426 if (this.window.gBrowser) { 427 // On startup, this will be called again by browser-init.js 428 // once gBrowser has been initialized. 429 this.addGBrowserListeners(); 430 } 431 432 // If the search service is not initialized yet, the placeholder 433 // and icon will be updated in delayedStartupInit. 434 if ( 435 Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") && 436 Services.search.isInitialized 437 ) { 438 this.searchModeSwitcher.updateSearchIcon(); 439 this._updatePlaceholderFromDefaultEngine(); 440 } 441 442 // Expanding requires a parent toolbar, and us not being read-only. 443 this.#allowBreakout = !!this.closest("toolbar"); 444 if (this.#allowBreakout) { 445 // TODO(emilio): This could use CSS anchor positioning rather than this 446 // ResizeObserver, eventually. 447 this._resizeObserver = new this.window.ResizeObserver(([entry]) => { 448 this.style.setProperty( 449 "--urlbar-width", 450 px(entry.borderBoxSize[0].inlineSize) 451 ); 452 }); 453 this._resizeObserver.observe(this.parentNode); 454 } 455 456 this.#updateLayoutBreakout(); 457 458 this._addObservers(); 459 } 460 461 disconnectedCallback() { 462 if ( 463 this.getAttribute("sap-name") == "searchbar" && 464 !lazy.UrlbarPrefs.get("browser.search.widget.new") 465 ) { 466 return; 467 } 468 469 this.#uninit(); 470 } 471 472 #uninit() { 473 if (this.sapName == "searchbar") { 474 this.parentNode.removeAttribute("overflows"); 475 } 476 477 if (this._copyCutController) { 478 this.inputField.controllers.removeController(this._copyCutController); 479 delete this._copyCutController; 480 } 481 482 for (let event of SmartbarInput.#inputFieldEvents) { 483 this.inputField.removeEventListener(event, this); 484 } 485 486 // These are on the window to detect focusing shortcuts like F6. 487 this.window.removeEventListener("keydown", this); 488 this.window.removeEventListener("keyup", this); 489 490 this.window.removeEventListener("mousedown", this); 491 if (AppConstants.platform == "win") { 492 this.window.removeEventListener("draggableregionleftmousedown", this); 493 } 494 this.removeEventListener("mousedown", this); 495 496 // This listener handles clicks from our children too, included the search mode 497 // indicator close button. 498 this._inputContainer.removeEventListener("click", this); 499 500 // This is used to detect commands launched from the panel, to avoid 501 // recording abandonment events when the command causes a blur event. 502 this.view.panel.removeEventListener("command", this, true); 503 504 this.window.removeEventListener("customizationstarting", this); 505 this.window.removeEventListener("aftercustomization", this); 506 this.window.removeEventListener("toolbarvisibilitychange", this); 507 let menuToolbar = this.window.document.getElementById("toolbar-menubar"); 508 if (menuToolbar) { 509 menuToolbar.removeEventListener("DOMMenuBarInactive", this); 510 menuToolbar.removeEventListener("DOMMenuBarActive", this); 511 } 512 if (this.#gBrowserListenersAdded) { 513 this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this); 514 this.window.gBrowser.tabContainer.removeEventListener("TabClose", this); 515 this.window.gBrowser.removeTabsProgressListener(this); 516 this.#gBrowserListenersAdded = false; 517 } 518 519 this._resizeObserver?.disconnect(); 520 521 this._removeObservers(); 522 } 523 524 /** 525 * This method is used to attach new context menu options to the urlbar 526 * context menu, i.e. the context menu of the moz-input-box. 527 * It is called when the moz-input-box rebuilds its context menu. 528 * 529 * Note that it might be called before #init has finished. 530 */ 531 #onContextMenuRebuilt() { 532 this._initStripOnShare(); 533 this._initPasteAndGo(); 534 } 535 536 addGBrowserListeners() { 537 // The following listeners are only used for the address bar. 538 if (!this.#isAddressbar) { 539 return; 540 } 541 if (this.window.gBrowser && !this.#gBrowserListenersAdded) { 542 this.window.gBrowser.tabContainer.addEventListener("TabSelect", this); 543 this.window.gBrowser.tabContainer.addEventListener("TabClose", this); 544 this.window.gBrowser.addTabsProgressListener(this); 545 this.#gBrowserListenersAdded = true; 546 } 547 } 548 549 #initSmartbarEditor() { 550 const adapter = createEditor(this.inputField); 551 this.#smartbarInputController = new lazy.SmartbarInputController(adapter); 552 this.inputField = adapter.input; 553 this.#smartbarEditor = adapter.editor; 554 } 555 556 #ensureSmartbarEditor() { 557 if (!this.#smartbarInputController) { 558 this.#initSmartbarEditor(); 559 } 560 return this.#smartbarEditor; 561 } 562 563 #setInputValue(val) { 564 if (this.#smartbarInputController) { 565 this.#smartbarInputController.setValue(val); 566 } else { 567 this.inputField.value = val; 568 } 569 } 570 571 #lazy = XPCOMUtils.declareLazy({ 572 valueFormatter: () => new lazy.UrlbarValueFormatter(this), 573 addSearchEngineHelper: () => new AddSearchEngineHelper(this), 574 }); 575 576 /** 577 * Manages the Add Search Engine contextual menu entries. 578 */ 579 get addSearchEngineHelper() { 580 return this.#lazy.addSearchEngineHelper; 581 } 582 583 /** 584 * The search access point name of the SmartbarInput for use with telemetry or 585 * logging, e.g. `urlbar`, `searchbar`. 586 */ 587 get sapName() { 588 return this.#sapName; 589 } 590 591 get smartbarMode() { 592 const mode = this.getAttribute("smartbar-mode") || this.#smartbarMode; 593 return SmartbarInput.#validSmartbarModes.includes(mode) 594 ? mode 595 : SmartbarInput.#validSmartbarModes[0]; 596 } 597 598 set smartbarMode(mode) { 599 if (!SmartbarInput.#validSmartbarModes.includes(mode)) { 600 return; 601 } 602 this.#smartbarMode = mode; 603 this.setAttribute("smartbar-mode", mode); 604 } 605 606 blur() { 607 if (this.#smartbarInputController) { 608 this.#smartbarInputController.blur(); 609 } else { 610 this.inputField.blur(); 611 } 612 } 613 614 /** 615 * @type {typeof HTMLInputElement.prototype.placeholder} 616 */ 617 get placeholder() { 618 return ( 619 this.#smartbarInputController?.placeholder ?? this.inputField?.placeholder 620 ); 621 } 622 623 set placeholder(val) { 624 if (this.#smartbarInputController) { 625 this.#smartbarInputController.placeholder = val; 626 return; 627 } 628 if (this.inputField) { 629 this.inputField.placeholder = val; 630 } 631 } 632 633 /** 634 * @type {typeof HTMLInputElement.prototype.readOnly} 635 */ 636 get readOnly() { 637 return this.#smartbarInputController?.readOnly ?? this.inputField?.readOnly; 638 } 639 640 set readOnly(val) { 641 if (this.#smartbarInputController) { 642 this.#smartbarInputController.readOnly = val; 643 return; 644 } 645 if (this.inputField) { 646 this.inputField.readOnly = val; 647 } 648 } 649 650 /** 651 * @type {typeof HTMLInputElement.prototype.selectionStart} 652 */ 653 get selectionStart() { 654 return ( 655 this.#smartbarInputController?.selectionStart ?? 656 this.inputField?.selectionStart ?? 657 0 658 ); 659 } 660 661 set selectionStart(val) { 662 if (this.#smartbarInputController) { 663 this.#smartbarInputController.selectionStart = val; 664 return; 665 } 666 if (this.inputField) { 667 this.inputField.selectionStart = val; 668 } 669 } 670 671 /** 672 * @type {typeof HTMLInputElement.prototype.selectionEnd} 673 */ 674 get selectionEnd() { 675 return ( 676 this.#smartbarInputController?.selectionEnd ?? 677 this.inputField?.selectionEnd ?? 678 0 679 ); 680 } 681 682 set selectionEnd(val) { 683 if (this.#smartbarInputController) { 684 this.#smartbarInputController.selectionEnd = val; 685 return; 686 } 687 if (this.inputField) { 688 this.inputField.selectionEnd = val; 689 } 690 } 691 692 /** 693 * Called when a urlbar or urlbar related pref changes. 694 * 695 * @param {string} pref 696 * The name of the pref. Relative to `browser.urlbar` for urlbar prefs. 697 */ 698 onPrefChanged(pref) { 699 switch (pref) { 700 case "keyword.enabled": 701 this._updatePlaceholderFromDefaultEngine().catch(e => 702 // This can happen if the search service failed. 703 console.warn("Falied to update urlbar placeholder:", e) 704 ); 705 break; 706 case "browser.search.widget.new": { 707 if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) { 708 if (lazy.UrlbarPrefs.get("browser.search.widget.new")) { 709 // The connectedCallback was skipped. Init now. 710 this.#init(); 711 } else { 712 // Uninit now, the disconnectedCallback will be skipped. 713 this.#uninit(); 714 } 715 } 716 } 717 } 718 } 719 720 /** 721 * Applies styling to the text in the urlbar input, depending on the text. 722 */ 723 formatValue() { 724 // The editor may not exist if the toolbar is not visible. 725 if (this.#isAddressbar && this.editor) { 726 this.#lazy.valueFormatter.update(); 727 } 728 } 729 730 focus() { 731 let beforeFocus = new CustomEvent("beforefocus", { 732 bubbles: true, 733 cancelable: true, 734 }); 735 this.inputField.dispatchEvent(beforeFocus); 736 if (beforeFocus.defaultPrevented) { 737 return; 738 } 739 740 if (this.#smartbarInputController) { 741 this.#smartbarInputController.focus(); 742 } else { 743 this.inputField.focus(); 744 } 745 } 746 747 select() { 748 let beforeSelect = new CustomEvent("beforeselect", { 749 bubbles: true, 750 cancelable: true, 751 }); 752 this.inputField.dispatchEvent(beforeSelect); 753 if (beforeSelect.defaultPrevented) { 754 return; 755 } 756 757 // See _on_select(). HTMLInputElement.select() dispatches a "select" 758 // event but does not set the primary selection. 759 this._suppressPrimaryAdjustment = true; 760 if (this.#smartbarInputController) { 761 this.#smartbarInputController?.select(); 762 } else { 763 this.inputField.select(); 764 } 765 this._suppressPrimaryAdjustment = false; 766 } 767 768 setSelectionRange(selectionStart, selectionEnd) { 769 let beforeSelect = new CustomEvent("beforeselect", { 770 bubbles: true, 771 cancelable: true, 772 }); 773 this.inputField.dispatchEvent(beforeSelect); 774 if (beforeSelect.defaultPrevented) { 775 return; 776 } 777 778 // See _on_select(). HTMLInputElement.select() dispatches a "select" 779 // event but does not set the primary selection. 780 this._suppressPrimaryAdjustment = true; 781 if (this.#smartbarInputController) { 782 this.#smartbarInputController.setSelectionRange( 783 selectionStart, 784 selectionEnd 785 ); 786 } else { 787 this.inputField.setSelectionRange(selectionStart, selectionEnd); 788 } 789 this._suppressPrimaryAdjustment = false; 790 } 791 792 saveSelectionStateForBrowser(browser) { 793 let state = this.getBrowserState(browser); 794 state.selection = { 795 // When the value is empty, we're either on a blank page, or the whole 796 // text has been edited away. In the latter case we'll restore value to 797 // the current URI, and we want to fully select it. 798 start: this.value ? this.selectionStart : 0, 799 end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER, 800 // When restoring a URI from an empty value, we don't want to untrim it. 801 shouldUntrim: this.value && !this._protocolIsTrimmed, 802 }; 803 } 804 805 restoreSelectionStateForBrowser(browser) { 806 // Address bar must be focused to untrim and for selection to make sense. 807 this.focus(); 808 let state = this.getBrowserState(browser); 809 if (state.selection) { 810 if (state.selection.shouldUntrim) { 811 this.#maybeUntrimUrl(); 812 } 813 this.setSelectionRange( 814 state.selection.start, 815 // When selecting all the end value may be larger than the actual value. 816 Math.min(state.selection.end, this.value.length) 817 ); 818 } 819 } 820 821 /** 822 * Sets the URI to display in the location bar. 823 * 824 * @param {object} [options] 825 * @param {?nsIURI} [options.uri] 826 * If this is unspecified, the current URI will be used. 827 * @param {boolean} [options.dueToTabSwitch=false] 828 * Whether this is being called due to switching tabs. 829 * @param {boolean} [options.dueToSessionRestore=false] 830 * Whether this is being called due to session restore. 831 * @param {boolean} [options.hideSearchTerms=false] 832 * True if userTypedValue should not be overidden by search terms 833 * and false otherwise. 834 * @param {boolean} [options.isSameDocument=false] 835 * Whether the caller loaded a new document or not (e.g. location 836 * change from an anchor scroll or a pushState event). 837 */ 838 setURI({ 839 uri = null, 840 dueToTabSwitch = false, 841 dueToSessionRestore = false, 842 hideSearchTerms = false, 843 isSameDocument = false, 844 } = {}) { 845 if (!this.#isAddressbar) { 846 throw new Error( 847 "Cannot set URI for SmartbarInput that is not an address bar" 848 ); 849 } 850 // We only need to update the searchModeUI on tab switch conditionally 851 // as we only persist searchMode with ScotchBonnet enabled. 852 if ( 853 dueToTabSwitch && 854 lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode") 855 ) { 856 this._updateSearchModeUI(this.searchMode); 857 } 858 859 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 860 this.#handlePersistedSearchTerms({ 861 state, 862 uri, 863 dueToTabSwitch, 864 hideSearchTerms, 865 isSameDocument, 866 }); 867 868 let value = this.userTypedValue; 869 let valid = false; 870 let isReverting = !uri; 871 872 // If `value` is null or if it's an empty string and we're switching tabs 873 // set value to the browser's current URI. When a user empties the input, 874 // switches tabs, and switches back, we want the URI to become visible again 875 // so the user knows what URI they're viewing. 876 // An exception to this is made in case of an auth request from a different 877 // base domain. To avoid auth prompt spoofing we already display the url of 878 // the cross domain resource, although the page is not loaded yet. 879 // This url will be set/unset by PromptParent. See bug 791594 for reference. 880 if (value === null || (!value && dueToTabSwitch)) { 881 uri = 882 this.window.gBrowser.selectedBrowser.currentAuthPromptURI || 883 uri || 884 this.#isOpenedPageInBlankTargetLoading || 885 this.window.gBrowser.currentURI; 886 // Strip off usernames and passwords for the location bar 887 try { 888 uri = Services.io.createExposableURI(uri); 889 } catch (e) {} 890 891 let isInitialPageControlledByWebContent = false; 892 893 // Replace initial page URIs with an empty string 894 // only if there's no opener (bug 370555). 895 if ( 896 this.window.isInitialPage(uri) && 897 lazy.BrowserUIUtils.checkEmptyPageOrigin( 898 this.window.gBrowser.selectedBrowser, 899 uri 900 ) 901 ) { 902 value = ""; 903 } else { 904 isInitialPageControlledByWebContent = true; 905 906 // We should deal with losslessDecodeURI throwing for exotic URIs 907 try { 908 value = losslessDecodeURI(uri); 909 } catch (ex) { 910 value = "about:blank"; 911 } 912 } 913 // If we update the URI while restoring a session, set the proxyState to 914 // invalid, because we don't have a valid security state to show via site 915 // identity yet. See Bug 1746383. 916 valid = 917 !dueToSessionRestore && 918 (!this.window.isBlankPageURL(uri.spec) || 919 lazy.ExtensionUtils.isExtensionUrl(uri) || 920 isInitialPageControlledByWebContent); 921 } else if ( 922 this.window.isInitialPage(value) && 923 lazy.BrowserUIUtils.checkEmptyPageOrigin( 924 this.window.gBrowser.selectedBrowser 925 ) 926 ) { 927 value = ""; 928 valid = true; 929 } 930 931 const previousUntrimmedValue = this.untrimmedValue; 932 // When calculating the selection indices we must take into account a 933 // trimmed protocol. 934 let offset = this._protocolIsTrimmed 935 ? lazy.BrowserUIUtils.trimURLProtocol.length 936 : 0; 937 const previousSelectionStart = this.selectionStart + offset; 938 const previousSelectionEnd = this.selectionEnd + offset; 939 940 this._setValue(value, { allowTrim: true, valueIsTyped: !valid }); 941 this.toggleAttribute("usertyping", !valid && value); 942 943 if (this.focused && value != previousUntrimmedValue) { 944 if ( 945 previousSelectionStart != previousSelectionEnd && 946 value.substring(previousSelectionStart, previousSelectionEnd) === 947 previousUntrimmedValue.substring( 948 previousSelectionStart, 949 previousSelectionEnd 950 ) 951 ) { 952 // If the same text is in the same place as the previously selected text, 953 // the selection is kept. 954 this.setSelectionRange( 955 previousSelectionStart - offset, 956 previousSelectionEnd - offset 957 ); 958 } else if ( 959 previousSelectionEnd && 960 (previousUntrimmedValue.length === previousSelectionEnd || 961 value.length <= previousSelectionEnd) 962 ) { 963 // If the previous end caret is not 0 and the caret is at the end of the 964 // input or its position is beyond the end of the new value, keep the 965 // position at the end. 966 this.setSelectionRange(value.length, value.length); 967 } else { 968 // Otherwise clear selection and set the caret position to the previous 969 // caret end position. 970 this.setSelectionRange( 971 previousSelectionEnd - offset, 972 previousSelectionEnd - offset 973 ); 974 } 975 } 976 977 // The proxystate must be set before setting search mode below because 978 // search mode depends on it. 979 this.setPageProxyState( 980 valid ? "valid" : "invalid", 981 dueToTabSwitch, 982 !isReverting && 983 dueToTabSwitch && 984 this.getBrowserState(this.window.gBrowser.selectedBrowser) 985 .isUnifiedSearchButtonAvailable 986 ); 987 988 if ( 989 state.persist?.shouldPersist && 990 !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState( 991 this.searchMode, 992 state 993 ) 994 ) { 995 // When search terms persist, on non-default engine search result pages 996 // the address bar should show the same search mode. For default engines, 997 // search mode should not persist. 998 if (state.persist.isDefaultEngine) { 999 this.searchMode = null; 1000 } else { 1001 this.searchMode = { 1002 engineName: state.persist.originalEngineName, 1003 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 1004 isPreview: false, 1005 }; 1006 } 1007 } else if (dueToTabSwitch && !valid) { 1008 // If we're switching tabs, restore the tab's search mode. 1009 this.restoreSearchModeState(); 1010 } else if (valid) { 1011 // If the URI is valid, exit search mode. This must happen 1012 // after setting proxystate above because search mode depends on it. 1013 this.searchMode = null; 1014 } 1015 1016 // Dispatch URIUpdate event to synchronize the tab status when switching. 1017 let event = new CustomEvent("SetURI", { bubbles: true }); 1018 this.inputField.dispatchEvent(event); 1019 } 1020 1021 /** 1022 * Converts an internal URI (e.g. a URI with a username or password) into one 1023 * which we can expose to the user. 1024 * 1025 * @param {nsIURI} uri 1026 * The URI to be converted 1027 * @returns {nsIURI} 1028 * The converted, exposable URI 1029 */ 1030 makeURIReadable(uri) { 1031 // Avoid copying 'about:reader?url=', and always provide the original URI: 1032 // Reader mode ensures we call createExposableURI itself. 1033 let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay( 1034 uri.displaySpec 1035 ); 1036 if (readerStrippedURI) { 1037 return readerStrippedURI; 1038 } 1039 1040 try { 1041 return Services.io.createExposableURI(uri); 1042 } catch (ex) {} 1043 1044 return uri; 1045 } 1046 1047 /** 1048 * Function for tabs progress listener. 1049 * 1050 * @param {nsIBrowser} browser 1051 * @param {nsIWebProgress} webProgress 1052 * The nsIWebProgress instance that fired the notification. 1053 * @param {nsIRequest} request 1054 * The associated nsIRequest. This may be null in some cases. 1055 * @param {nsIURI} locationURI 1056 * The URI of the location that is being loaded. 1057 */ 1058 onLocationChange(browser, webProgress, request, locationURI) { 1059 if (!webProgress.isTopLevel) { 1060 return; 1061 } 1062 1063 if ( 1064 browser != this.window.gBrowser.selectedBrowser && 1065 !this.window.isBlankPageURL(locationURI.spec) 1066 ) { 1067 // If the page is loaded on background tab, make Unified Search Button 1068 // unavailable when back to the tab. 1069 this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false; 1070 } 1071 1072 // Using browser navigation buttons should potentially trigger a bounce 1073 // telemetry event. 1074 if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) { 1075 this.controller.engagementEvent.handleBounceEventTrigger(browser); 1076 } 1077 } 1078 1079 /** 1080 * Passes DOM events to the _on_<event type> methods. 1081 * 1082 * @param {Event} event The event to handle. 1083 */ 1084 handleEvent(event) { 1085 let methodName = "_on_" + event.type; 1086 if (methodName in this) { 1087 try { 1088 this[methodName](event); 1089 } catch (e) { 1090 console.error(`Error calling SmartbarInput::${methodName}:`, e); 1091 } 1092 } else { 1093 throw new Error("Unrecognized SmartbarInput event: " + event.type); 1094 } 1095 } 1096 1097 /** 1098 * Handles an event which might open text or a URL. If the event requires 1099 * doing so, handleCommand forwards it to handleNavigation. 1100 * 1101 * @param {Event} [event] The event triggering the open. 1102 */ 1103 handleCommand(event = null) { 1104 let isMouseEvent = MouseEvent.isInstance(event); 1105 if (isMouseEvent && event.button == 2) { 1106 // Do nothing for right clicks. 1107 return; 1108 } 1109 1110 // Determine whether to use the selected one-off search button. In 1111 // one-off search buttons parlance, "selected" means that the button 1112 // has been navigated to via the keyboard. So we want to use it if 1113 // the triggering event is not a mouse click -- i.e., it's a Return 1114 // key -- or if the one-off was mouse-clicked. 1115 if (this.view.isOpen) { 1116 let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton; 1117 if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) { 1118 this.view.oneOffSearchButtons.handleSearchCommand(event, { 1119 engineName: selectedOneOff.engine?.name, 1120 source: selectedOneOff.source, 1121 entry: "oneoff", 1122 }); 1123 return; 1124 } 1125 } 1126 1127 this.handleNavigation({ event }); 1128 } 1129 1130 /** 1131 * @typedef {object} HandleNavigationOneOffParams 1132 * 1133 * @property {string} openWhere 1134 * Where we expect the result to be opened. 1135 * @property {object} openParams 1136 * The parameters related to where the result will be opened. 1137 * @property {nsISearchEngine} engine 1138 * The selected one-off's engine. 1139 */ 1140 1141 /** 1142 * Handles an event which would cause a URL or text to be opened. 1143 * 1144 * @param {object} options 1145 * Options for the navigation. 1146 * @param {Event} [options.event] 1147 * The event triggering the open. 1148 * @param {HandleNavigationOneOffParams} [options.oneOffParams] 1149 * Optional. Pass if this navigation was triggered by a one-off. Practically 1150 * speaking, UrlbarSearchOneOffs passes this when the user holds certain key 1151 * modifiers while picking a one-off. In those cases, we do an immediate 1152 * search using the one-off's engine instead of entering search mode. 1153 * @param {object} [options.triggeringPrincipal] 1154 * The principal that the action was triggered from. 1155 */ 1156 handleNavigation({ event, oneOffParams, triggeringPrincipal }) { 1157 if (this.#isSmartbarMode) { 1158 const committedValue = this.untrimmedValue; 1159 const mode = this.smartbarMode; 1160 lazy.logger.debug(`commit (${mode}): ${committedValue}`); 1161 this.#clearSmartbarInput(); 1162 this.dispatchEvent( 1163 new CustomEvent("smartbar-commit", { 1164 detail: { value: committedValue, event, mode }, 1165 }) 1166 ); 1167 if (!this.window.gBrowser) { 1168 return; 1169 } 1170 // Fall through to default navigation behaviour. 1171 } 1172 let element = this.view.selectedElement; 1173 let result = this.view.getResultFromElement(element); 1174 let openParams = oneOffParams?.openParams || { triggeringPrincipal }; 1175 1176 // If the value was submitted during composition, the result may not have 1177 // been updated yet, because the input event happens after composition end. 1178 // We can't trust element nor _resultForCurrentValue targets in that case, 1179 // so we always generate a new heuristic to load. 1180 let isComposing = this.editor.composing; 1181 1182 // Use the selected element if we have one; this is usually the case 1183 // when the view is open. 1184 let selectedPrivateResult = 1185 result && 1186 result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH && 1187 result.payload.inPrivateWindow; 1188 let selectedPrivateEngineResult = 1189 selectedPrivateResult && result.payload.isPrivateEngine; 1190 // Whether the user has been editing the value in the URL bar after selecting 1191 // the result. However, if the result type is tip, pick as it is. The result 1192 // heuristic is also kept the behavior as is for safety. 1193 let safeToPickResult = 1194 result && 1195 (result.heuristic || 1196 !this.valueIsTyped || 1197 result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP || 1198 this.value == this.#getValueFromResult(result)); 1199 if ( 1200 !isComposing && 1201 element && 1202 (!oneOffParams?.engine || selectedPrivateEngineResult) && 1203 safeToPickResult 1204 ) { 1205 this.pickElement(element, event); 1206 return; 1207 } 1208 1209 // Use the hidden heuristic if it exists and there's no selection. 1210 if ( 1211 lazy.UrlbarPrefs.get("experimental.hideHeuristic") && 1212 !element && 1213 !isComposing && 1214 !oneOffParams?.engine && 1215 this._resultForCurrentValue?.heuristic 1216 ) { 1217 this.pickResult(this._resultForCurrentValue, event); 1218 return; 1219 } 1220 1221 // We don't select a heuristic result when we're autofilling a token alias, 1222 // but we want pressing Enter to behave like the first result was selected. 1223 if (!result && this.value.startsWith("@")) { 1224 let tokenAliasResult = this.view.getResultAtIndex(0); 1225 if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) { 1226 this.pickResult(tokenAliasResult, event); 1227 return; 1228 } 1229 } 1230 1231 let url; 1232 let selType = this.controller.engagementEvent.typeFromElement( 1233 result, 1234 element 1235 ); 1236 let typedValue = this.value; 1237 if (oneOffParams?.engine) { 1238 selType = "oneoff"; 1239 typedValue = this._lastSearchString; 1240 // If there's a selected one-off button then load a search using 1241 // the button's engine. 1242 result = this._resultForCurrentValue; 1243 1244 let searchString = 1245 (result && (result.payload.suggestion || result.payload.query)) || 1246 this._lastSearchString; 1247 [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl( 1248 oneOffParams.engine, 1249 searchString 1250 ); 1251 if (oneOffParams.openWhere == "tab") { 1252 this.window.gBrowser.tabContainer.addEventListener( 1253 "TabOpen", 1254 tabEvent => 1255 this._recordSearch( 1256 oneOffParams.engine, 1257 event, 1258 {}, 1259 tabEvent.target.linkedBrowser 1260 ), 1261 { once: true } 1262 ); 1263 } else { 1264 this._recordSearch(oneOffParams.engine, event); 1265 } 1266 1267 lazy.UrlbarUtils.addToFormHistory( 1268 this, 1269 searchString, 1270 oneOffParams.engine.name 1271 ).catch(console.error); 1272 } else { 1273 // Use the current value if we don't have a UrlbarResult e.g. because the 1274 // view is closed. 1275 url = this.untrimmedValue; 1276 openParams.postData = null; 1277 } 1278 1279 if (!url) { 1280 return; 1281 } 1282 1283 // When the user hits enter in a local search mode and there's no selected 1284 // result or one-off, don't do anything. 1285 if ( 1286 this.searchMode && 1287 !this.searchMode.engineName && 1288 !result && 1289 !oneOffParams 1290 ) { 1291 return; 1292 } 1293 1294 let where = oneOffParams?.openWhere || this._whereToOpen(event); 1295 if (selectedPrivateResult) { 1296 where = "window"; 1297 openParams.private = true; 1298 } 1299 openParams.allowInheritPrincipal = false; 1300 url = this._maybeCanonizeURL(event, url) || url.trim(); 1301 1302 let selectedResult = result || this.view.selectedResult; 1303 this.controller.engagementEvent.record(event, { 1304 element, 1305 selType, 1306 searchString: typedValue, 1307 result: selectedResult || this._resultForCurrentValue || null, 1308 }); 1309 1310 if (URL.canParse(url)) { 1311 // Annotate if the untrimmed value contained a scheme, to later potentially 1312 // be upgraded by schemeless HTTPS-First. 1313 openParams.schemelessInput = this.#getSchemelessInput( 1314 this.untrimmedValue 1315 ); 1316 this._loadURL(url, event, where, openParams); 1317 return; 1318 } 1319 1320 // This is not a URL and there's no selected element, because likely the 1321 // view is closed, or paste&go was used. 1322 // We must act consistently here, having or not an open view should not 1323 // make a difference if the search string is the same. 1324 1325 // If we have a result for the current value, we can just use it. 1326 if (!isComposing && this._resultForCurrentValue) { 1327 this.pickResult(this._resultForCurrentValue, event); 1328 return; 1329 } 1330 1331 // Otherwise, we must fetch the heuristic result for the current value. 1332 // TODO (Bug 1604927): If the urlbar results are restricted to a specific 1333 // engine, here we must search with that specific engine; indeed the 1334 // docshell wouldn't know about our engine restriction. 1335 // Also remember to invoke this._recordSearch, after replacing url with 1336 // the appropriate engine submission url. 1337 let browser = this.window.gBrowser.selectedBrowser; 1338 let lastLocationChange = browser.lastLocationChange; 1339 1340 // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit. 1341 Glean.urlbar.heuristicResultMissing.addToDenominator(1); 1342 1343 lazy.UrlbarUtils.getHeuristicResultFor(url, this) 1344 .then(newResult => { 1345 // Because this happens asynchronously, we must verify that the browser 1346 // location did not change in the meanwhile. 1347 if ( 1348 where != "current" || 1349 browser.lastLocationChange == lastLocationChange 1350 ) { 1351 this.pickResult(newResult, event, null, browser); 1352 } 1353 }) 1354 .catch(() => { 1355 if (url) { 1356 // Something went wrong, we should always have a heuristic result, 1357 // otherwise it means we're not able to search at all, maybe because 1358 // some parts of the profile are corrupt. 1359 // The urlbar should still allow to search or visit the typed string, 1360 // so that the user can look for help to resolve the problem. 1361 1362 // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit. 1363 Glean.urlbar.heuristicResultMissing.addToNumerator(1); 1364 1365 let flags = 1366 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 1367 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; 1368 if (this.isPrivate) { 1369 flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; 1370 } 1371 let { 1372 preferredURI: uri, 1373 postData, 1374 keywordAsSent, 1375 } = Services.uriFixup.getFixupURIInfo(url, flags); 1376 if ( 1377 where != "current" || 1378 browser.lastLocationChange == lastLocationChange 1379 ) { 1380 openParams.postData = postData; 1381 if (!keywordAsSent) { 1382 // `uri` is not a search engine url, so we annotate if the untrimmed 1383 // value contained a scheme, to potentially be later upgraded by 1384 // schemeless HTTPS-First. 1385 openParams.schemelessInput = this.#getSchemelessInput( 1386 this.untrimmedValue 1387 ); 1388 } 1389 this._loadURL(uri.spec, event, where, openParams, null, browser); 1390 } 1391 } 1392 }); 1393 // Don't add further handling here, the catch above is our last resort. 1394 } 1395 1396 handleRevert() { 1397 this.userTypedValue = null; 1398 // Nullify search mode before setURI so it won't try to restore it. 1399 this.searchMode = null; 1400 if (this.#isAddressbar) { 1401 this.setURI({ 1402 dueToTabSwitch: true, 1403 hideSearchTerms: true, 1404 }); 1405 } else { 1406 this.value = ""; 1407 } 1408 if (this.value && this.focused) { 1409 this.select(); 1410 } 1411 } 1412 1413 maybeHandleRevertFromPopup(anchorElement) { 1414 if (!this.#isAddressbar) { 1415 return; 1416 } 1417 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 1418 if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) { 1419 this.handleRevert(); 1420 Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1); 1421 } 1422 } 1423 1424 /** 1425 * Called by inputs that resemble search boxes, but actually hand input off 1426 * to the Urlbar. We use these fake inputs on the new tab page and 1427 * about:privatebrowsing. 1428 * 1429 * @param {string} searchString 1430 * The search string to use. 1431 * @param {nsISearchEngine} [searchEngine] 1432 * Optional. If included and the right prefs are set, we will enter search 1433 * mode when handing `searchString` from the fake input to the Urlbar. 1434 * @param {string} [newtabSessionId] 1435 * Optional. The id of the newtab session that handed off this search. 1436 */ 1437 handoff(searchString, searchEngine, newtabSessionId) { 1438 this._isHandoffSession = true; 1439 this._handoffSession = newtabSessionId; 1440 if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) { 1441 this.search(searchString, { 1442 searchEngine, 1443 searchModeEntry: "handoff", 1444 }); 1445 } else { 1446 this.search(searchString); 1447 } 1448 } 1449 1450 /** 1451 * Called when an element of the view is picked. 1452 * 1453 * @param {HTMLElement} element The element that was picked. 1454 * @param {Event} event The event that picked the element. 1455 */ 1456 pickElement(element, event) { 1457 let result = this.view.getResultFromElement(element); 1458 lazy.logger.debug( 1459 `pickElement ${element} with event ${event?.type}, result: ${result}` 1460 ); 1461 if (!result) { 1462 return; 1463 } 1464 this.pickResult(result, event, element); 1465 } 1466 1467 /** 1468 * Called when a result is picked. 1469 * 1470 * @param {UrlbarResult} result The result that was picked. 1471 * @param {Event} event The event that picked the result. 1472 * @param {HTMLElement} element the picked view element, if available. 1473 * @param {object} browser The browser to use for the load. 1474 */ 1475 // eslint-disable-next-line complexity 1476 pickResult( 1477 result, 1478 event, 1479 element = null, 1480 browser = this.window.gBrowser.selectedBrowser 1481 ) { 1482 if (element?.classList.contains("urlbarView-button-menu")) { 1483 this.view.openResultMenu(result, element); 1484 return; 1485 } 1486 1487 if (element?.dataset.command) { 1488 this.#pickMenuResult(result, event, element, browser); 1489 return; 1490 } 1491 1492 if ( 1493 result.providerName == lazy.UrlbarProviderGlobalActions.name && 1494 this.#providesSearchMode(result) 1495 ) { 1496 this.maybeConfirmSearchModeFromResult({ 1497 result, 1498 checkValue: false, 1499 }); 1500 return; 1501 } 1502 1503 // When a one-off is selected, we restyle heuristic results to look like 1504 // search results. In the unlikely event that they are clicked, instead of 1505 // picking the results as usual, we confirm search mode, same as if the user 1506 // had selected them and pressed the enter key. Restyling results in this 1507 // manner was agreed on as a compromise between consistent UX and 1508 // engineering effort. See review discussion at bug 1667766. 1509 if ( 1510 (this.searchMode?.isPreview && 1511 result.providerName == lazy.UrlbarProviderGlobalActions.name) || 1512 (result.heuristic && 1513 this.searchMode?.isPreview && 1514 this.view.oneOffSearchButtons?.selectedButton) 1515 ) { 1516 this.confirmSearchMode(); 1517 this.search(this.value); 1518 return; 1519 } 1520 1521 if ( 1522 result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP && 1523 result.payload.type == "dismissalAcknowledgment" 1524 ) { 1525 // The user clicked the "Got it" button inside the dismissal 1526 // acknowledgment tip. Dismiss the tip. 1527 this.controller.engagementEvent.record(event, { 1528 result, 1529 element, 1530 searchString: this._lastSearchString, 1531 selType: "dismiss", 1532 }); 1533 this.view.onQueryResultRemoved(result.rowIndex); 1534 return; 1535 } 1536 1537 let resultUrl = element?.dataset.url; 1538 let originalUntrimmedValue = this.untrimmedValue; 1539 let isCanonized = this.setValueFromResult({ 1540 result, 1541 event, 1542 element, 1543 urlOverride: resultUrl, 1544 }); 1545 let where = this._whereToOpen(event); 1546 let openParams = { 1547 allowInheritPrincipal: false, 1548 globalHistoryOptions: { 1549 triggeringSource: this.#sapName, 1550 triggeringSearchEngine: result.payload?.engine, 1551 triggeringSponsoredURL: result.payload?.isSponsored 1552 ? result.payload.url 1553 : undefined, 1554 }, 1555 private: this.isPrivate, 1556 }; 1557 1558 if (resultUrl && where == "current") { 1559 // Open help links in a new tab. 1560 where = "tab"; 1561 } 1562 1563 if (!this.#providesSearchMode(result)) { 1564 this.view.close({ elementPicked: true }); 1565 } 1566 1567 if (isCanonized) { 1568 this.controller.engagementEvent.record(event, { 1569 result, 1570 element, 1571 selType: "canonized", 1572 searchString: this._lastSearchString, 1573 }); 1574 this._loadURL(this._untrimmedValue, event, where, openParams, browser); 1575 return; 1576 } 1577 1578 let { url, postData } = resultUrl 1579 ? { url: resultUrl, postData: null } 1580 : lazy.UrlbarUtils.getUrlFromResult(result, { element }); 1581 openParams.postData = postData; 1582 1583 switch (result.type) { 1584 case lazy.UrlbarUtils.RESULT_TYPE.URL: { 1585 if (result.heuristic) { 1586 // Bug 1578856: both the provider and the docshell run heuristics to 1587 // decide how to handle a non-url string, either fixing it to a url, or 1588 // searching for it. 1589 // Some preferences can control the docshell behavior, for example 1590 // if dns_first_for_single_words is true, the docshell looks up the word 1591 // against the dns server, and either loads it as an url or searches for 1592 // it, depending on the lookup result. The provider instead will always 1593 // return a fixed url in this case, because URIFixup is synchronous and 1594 // can't do a synchronous dns lookup. A possible long term solution 1595 // would involve sharing the docshell logic with the provider, along 1596 // with the dns lookup. 1597 // For now, in this specific case, we'll override the result's url 1598 // with the input value, and let it pass through to _loadURL(), and 1599 // finally to the docshell. 1600 // This also means that in some cases the heuristic result will show a 1601 // Visit entry, but the docshell will instead execute a search. It's a 1602 // rare case anyway, most likely to happen for enterprises customizing 1603 // the urifixup prefs. 1604 if ( 1605 lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && 1606 lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) 1607 ) { 1608 url = originalUntrimmedValue; 1609 } 1610 // Annotate if the untrimmed value contained a scheme, to later potentially 1611 // be upgraded by schemeless HTTPS-First. 1612 openParams.schemelessInput = this.#getSchemelessInput( 1613 originalUntrimmedValue 1614 ); 1615 } 1616 break; 1617 } 1618 case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: { 1619 // If this result comes from a bookmark keyword, let it inherit the 1620 // current document's principal, otherwise bookmarklets would break. 1621 openParams.allowInheritPrincipal = true; 1622 break; 1623 } 1624 case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: { 1625 // Behaviour is reversed with SecondaryActions, default behaviour is to navigate 1626 // and button is provided to switch to tab. 1627 if ( 1628 this.hasAttribute("action-override") || 1629 (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") && 1630 element?.dataset.action !== "tabswitch") 1631 ) { 1632 where = "current"; 1633 break; 1634 } 1635 1636 // Keep the searchMode for telemetry since handleRevert sets it to null. 1637 const searchMode = this.searchMode; 1638 this.handleRevert(); 1639 let prevTab = this.window.gBrowser.selectedTab; 1640 let loadOpts = { 1641 adoptIntoActiveWindow: lazy.UrlbarPrefs.get( 1642 "switchTabs.adoptIntoActiveWindow" 1643 ), 1644 }; 1645 1646 // We cache the search string because switching tab may clear it. 1647 let searchString = this._lastSearchString; 1648 this.controller.engagementEvent.record(event, { 1649 result, 1650 element, 1651 searchString, 1652 searchMode, 1653 selType: this.controller.engagementEvent.typeFromElement( 1654 result, 1655 element 1656 ), 1657 }); 1658 1659 let switched = this.window.switchToTabHavingURI( 1660 Services.io.newURI(url), 1661 true, 1662 loadOpts, 1663 lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") && 1664 lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId( 1665 result.payload.userContextId 1666 ) 1667 ? result.payload.userContextId 1668 : null 1669 ); 1670 if (switched && prevTab.isEmpty) { 1671 this.window.gBrowser.removeTab(prevTab); 1672 } 1673 1674 if (switched && !this.isPrivate && !result.heuristic) { 1675 // We don't await for this, because a rejection should not interrupt 1676 // the load. Just reportError it. 1677 lazy.UrlbarUtils.addToInputHistory(url, searchString).catch( 1678 console.error 1679 ); 1680 } 1681 1682 // TODO (Bug 1865757): We should not show a "switchtotab" result for 1683 // tabs that are not currently open. Find out why tabs are not being 1684 // properly unregistered when they are being closed. 1685 if (!switched) { 1686 console.error(`Tried to switch to non-existent tab: ${url}`); 1687 lazy.UrlbarProviderOpenTabs.unregisterOpenTab( 1688 url, 1689 result.payload.userContextId, 1690 result.payload.tabGroup, 1691 this.isPrivate 1692 ); 1693 } 1694 1695 return; 1696 } 1697 case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { 1698 if (result.payload.providesSearchMode) { 1699 this.controller.engagementEvent.record(event, { 1700 result, 1701 element, 1702 searchString: this._lastSearchString, 1703 selType: this.controller.engagementEvent.typeFromElement( 1704 result, 1705 element 1706 ), 1707 }); 1708 this.maybeConfirmSearchModeFromResult({ 1709 result, 1710 checkValue: false, 1711 }); 1712 return; 1713 } 1714 1715 if ( 1716 !this.searchMode && 1717 result.heuristic && 1718 // If we asked the DNS earlier, avoid the post-facto check. 1719 !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && 1720 // TODO (bug 1642623): for now there is no smart heuristic to skip the 1721 // DNS lookup, so any value above 0 will run it. 1722 lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 && 1723 this.window.gKeywordURIFixup && 1724 lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) 1725 ) { 1726 // When fixing a single word to a search, the docShell would also 1727 // query the DNS and if resolved ask the user whether they would 1728 // rather visit that as a host. On a positive answer, it adds the host 1729 // to the list that we use to make decisions. 1730 // Because we are directly asking for a search here, bypassing the 1731 // docShell, we need to do the same ourselves. 1732 // See also URIFixupChild.sys.mjs and keyword-uri-fixup. 1733 let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim()); 1734 if (fixupInfo) { 1735 this.window.gKeywordURIFixup.check( 1736 this.window.gBrowser.selectedBrowser, 1737 fixupInfo 1738 ); 1739 } 1740 } 1741 1742 if (result.payload.inPrivateWindow) { 1743 where = "window"; 1744 openParams.private = true; 1745 } 1746 1747 const actionDetails = { 1748 isSuggestion: !!result.payload.suggestion, 1749 isFormHistory: 1750 result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY, 1751 alias: result.payload.keyword, 1752 }; 1753 const engine = Services.search.getEngineByName(result.payload.engine); 1754 1755 if (where == "tab") { 1756 // The TabOpen event is fired synchronously so tabEvent.target 1757 // is guaranteed to be our new search tab. 1758 this.window.gBrowser.tabContainer.addEventListener( 1759 "TabOpen", 1760 tabEvent => 1761 this._recordSearch( 1762 engine, 1763 event, 1764 actionDetails, 1765 tabEvent.target.linkedBrowser 1766 ), 1767 { once: true } 1768 ); 1769 } else { 1770 this._recordSearch(engine, event, actionDetails); 1771 } 1772 1773 if (!result.payload.inPrivateWindow) { 1774 lazy.UrlbarUtils.addToFormHistory( 1775 this, 1776 result.payload.suggestion || result.payload.query, 1777 engine.name 1778 ).catch(console.error); 1779 } 1780 break; 1781 } 1782 case lazy.UrlbarUtils.RESULT_TYPE.TIP: { 1783 if (url) { 1784 break; 1785 } 1786 this.handleRevert(); 1787 this.controller.engagementEvent.record(event, { 1788 result, 1789 element, 1790 selType: "tip", 1791 searchString: this._lastSearchString, 1792 }); 1793 return; 1794 } 1795 case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: { 1796 if (!url) { 1797 // If we're not loading a URL, the engagement is done. First revert 1798 // and then record the engagement since providers expect the urlbar to 1799 // be reverted when they're notified of the engagement, but before 1800 // reverting, copy the search mode since it's nulled on revert. 1801 const { searchMode } = this; 1802 this.handleRevert(); 1803 this.controller.engagementEvent.record(event, { 1804 result, 1805 element, 1806 searchMode, 1807 searchString: this._lastSearchString, 1808 selType: this.controller.engagementEvent.typeFromElement( 1809 result, 1810 element 1811 ), 1812 }); 1813 return; 1814 } 1815 break; 1816 } 1817 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: { 1818 this.controller.engagementEvent.record(event, { 1819 result, 1820 element, 1821 selType: "extension", 1822 searchString: this._lastSearchString, 1823 }); 1824 1825 // The urlbar needs to revert to the loaded url when a command is 1826 // handled by the extension. 1827 this.handleRevert(); 1828 // We don't directly handle a load when an Omnibox API result is picked, 1829 // instead we forward the request to the WebExtension itself, because 1830 // the value may not even be a url. 1831 // We pass the keyword and content, that actually is the retrieved value 1832 // prefixed by the keyword. ExtensionSearchHandler uses this keyword 1833 // redundancy as a sanity check. 1834 lazy.ExtensionSearchHandler.handleInputEntered( 1835 result.payload.keyword, 1836 result.payload.content, 1837 where 1838 ); 1839 return; 1840 } 1841 case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: { 1842 this.handleRevert(); 1843 this.controller.engagementEvent.record(event, { 1844 result, 1845 element, 1846 searchString: this._lastSearchString, 1847 selType: this.controller.engagementEvent.typeFromElement( 1848 result, 1849 element 1850 ), 1851 }); 1852 this.maybeConfirmSearchModeFromResult({ 1853 result, 1854 checkValue: false, 1855 }); 1856 1857 return; 1858 } 1859 } 1860 1861 if (!url) { 1862 throw new Error(`Invalid url for result ${JSON.stringify(result)}`); 1863 } 1864 1865 // Record input history but only in non-private windows. 1866 if (!this.isPrivate) { 1867 let input; 1868 if (!result.heuristic) { 1869 input = this._lastSearchString; 1870 } else if (result.autofill?.type == "adaptive") { 1871 input = result.autofill.adaptiveHistoryInput; 1872 } 1873 // `input` may be an empty string, so do a strict comparison here. 1874 if (input !== undefined) { 1875 // We don't await for this, because a rejection should not interrupt 1876 // the load. Just reportError it. 1877 lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error); 1878 } 1879 } 1880 1881 this.controller.engagementEvent.startTrackingBounceEvent(browser, event, { 1882 result, 1883 element, 1884 searchString: this._lastSearchString, 1885 selType: this.controller.engagementEvent.typeFromElement(result, element), 1886 searchSource: this.getSearchSource(event), 1887 }); 1888 1889 this.controller.engagementEvent.record(event, { 1890 result, 1891 element, 1892 searchString: this._lastSearchString, 1893 selType: this.controller.engagementEvent.typeFromElement(result, element), 1894 searchSource: this.getSearchSource(event), 1895 }); 1896 1897 if (result.payload.sendAttributionRequest) { 1898 lazy.PartnerLinkAttribution.makeRequest({ 1899 targetURL: result.payload.url, 1900 source: this.#sapName, 1901 campaignID: Services.prefs.getStringPref( 1902 "browser.partnerlink.campaign.topsites" 1903 ), 1904 }); 1905 if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") { 1906 // The position is 1-based for telemetry 1907 const position = result.rowIndex + 1; 1908 Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1); 1909 } 1910 } 1911 1912 this._loadURL( 1913 url, 1914 event, 1915 where, 1916 openParams, 1917 { 1918 source: result.source, 1919 type: result.type, 1920 searchTerm: result.payload.suggestion ?? result.payload.query, 1921 }, 1922 browser 1923 ); 1924 } 1925 1926 #clearSmartbarInput() { 1927 this.value = ""; 1928 this.userTypedValue = ""; 1929 this._lastSearchString = ""; 1930 this._autofillPlaceholder = null; 1931 this._resultForCurrentValue = null; 1932 this.setSelectionRange(0, 0); 1933 } 1934 1935 /** 1936 * Called by the view when moving through results with the keyboard, and when 1937 * picking a result. This sets the input value to the value of the result and 1938 * invalidates the pageproxystate. It also sets the result that is associated 1939 * with the current input value. If you need to set this result but don't 1940 * want to also set the input value, then use setResultForCurrentValue. 1941 * 1942 * @param {object} options 1943 * Options. 1944 * @param {UrlbarResult} [options.result] 1945 * The result that was selected or picked, null if no result was selected. 1946 * @param {Event} [options.event] 1947 * The event that picked the result. 1948 * @param {string} [options.urlOverride] 1949 * Normally the URL is taken from `result.payload.url`, but if `urlOverride` 1950 * is specified, it's used instead. See `#getValueFromResult()`. 1951 * @param {Element} [options.element] 1952 * The element that was selected or picked, if available. For results that 1953 * have multiple selectable children, the value may be taken from a child 1954 * element rather than the result. See `#getValueFromResult()`. 1955 * @returns {boolean} 1956 * Whether the value has been canonized 1957 */ 1958 setValueFromResult({ 1959 result = null, 1960 event = null, 1961 urlOverride = null, 1962 element = null, 1963 } = {}) { 1964 // Usually this is set by a previous input event, but in certain cases, like 1965 // when opening Top Sites on a loaded page, it wouldn't happen. To avoid 1966 // confusing the user, we always enforce it when a result changes our value. 1967 this.setPageProxyState("invalid", true); 1968 1969 // A previous result may have previewed search mode. If we don't expect that 1970 // we might stay in a search mode of some kind, exit it now. 1971 if ( 1972 this.searchMode?.isPreview && 1973 !this.#providesSearchMode(result) && 1974 !this.view.oneOffSearchButtons?.selectedButton 1975 ) { 1976 this.searchMode = null; 1977 } 1978 1979 if (!result) { 1980 // This happens when there's no selection, for example when moving to the 1981 // one-offs search settings button, or to the input field when Top Sites 1982 // are shown; then we must reset the input value. 1983 // Note that for Top Sites the last search string would be empty, thus we 1984 // must restore the last text value. 1985 // Note that unselected autofill results will still arrive in this 1986 // function with a non-null `result`. They are handled below. 1987 this.value = this._lastSearchString || this._valueOnLastSearch; 1988 this.setResultForCurrentValue(result); 1989 return false; 1990 } 1991 1992 // We won't allow trimming when calling _setValue, since it makes too easy 1993 // for the user to wrongly transform `https` into `http`, for example by 1994 // picking a https://site/path_1 result and editing the path to path_2, 1995 // then we'd end up visiting http://site/path_2. 1996 // Trimming `http` would be ok, but there's other cases where it's unsafe, 1997 // like transforming a url into a search. 1998 // This choice also makes it easier to copy the full url of a result. 1999 2000 // We are supporting canonization of any result, in particular this allows 2001 // for single word search suggestions to be converted to a .com URL. 2002 // For autofilled results, the value to canonize is the user typed string, 2003 // not the autofilled value. 2004 let canonizedUrl = this._maybeCanonizeURL( 2005 event, 2006 result.autofill ? this._lastSearchString : this.value 2007 ); 2008 if (canonizedUrl) { 2009 this._setValue(canonizedUrl); 2010 2011 this.setResultForCurrentValue(result); 2012 return true; 2013 } 2014 2015 if (result.autofill) { 2016 this._autofillValue(result.autofill); 2017 } 2018 2019 if (this.#providesSearchMode(result)) { 2020 let enteredSearchMode; 2021 // Only preview search mode if the result is selected. 2022 if (this.view.resultIsSelected(result)) { 2023 // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used 2024 // for selection of the urlbar results, keep the search mode as preview 2025 // mode if there are multiple results. 2026 // If ScotchBonnet is disabled, not starting a query means we will only 2027 // preview search mode. 2028 enteredSearchMode = this.maybeConfirmSearchModeFromResult({ 2029 result, 2030 checkValue: false, 2031 startQuery: 2032 lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") && 2033 this.view.visibleResults.length == 1, 2034 }); 2035 } 2036 if (!enteredSearchMode) { 2037 this._setValue(this.#getValueFromResult(result), { 2038 actionType: this.#getActionTypeFromResult(result), 2039 }); 2040 this.searchMode = null; 2041 } 2042 this.setResultForCurrentValue(result); 2043 return false; 2044 } 2045 2046 if (!result.autofill) { 2047 let value = this.#getValueFromResult(result, { urlOverride, element }); 2048 this._setValue(value, { 2049 actionType: this.#getActionTypeFromResult(result), 2050 }); 2051 } 2052 2053 this.setResultForCurrentValue(result); 2054 2055 // Update placeholder selection and value to the current selected result to 2056 // prevent the on_selectionchange event to detect a "accent-character" 2057 // insertion. 2058 if (!result.autofill && this._autofillPlaceholder) { 2059 this._autofillPlaceholder.value = this.value; 2060 this._autofillPlaceholder.selectionStart = this.value.length; 2061 this._autofillPlaceholder.selectionEnd = this.value.length; 2062 } 2063 return false; 2064 } 2065 2066 /** 2067 * The input keeps track of the result associated with the current input 2068 * value. This result can be set by calling either setValueFromResult or this 2069 * method. Use this method when you need to set the result without also 2070 * setting the input value. This can be the case when either the selection is 2071 * cleared and no other result becomes selected, or when the result is the 2072 * heuristic and we don't want to modify the value the user is typing. 2073 * 2074 * @param {UrlbarResult} result 2075 * The result to associate with the current input value. 2076 */ 2077 setResultForCurrentValue(result) { 2078 this._resultForCurrentValue = result; 2079 } 2080 2081 /** 2082 * Called by the controller when the first result of a new search is received. 2083 * If it's an autofill result, then it may need to be autofilled, subject to a 2084 * few restrictions. 2085 * 2086 * @param {UrlbarResult} result 2087 * The first result. 2088 */ 2089 _autofillFirstResult(result) { 2090 if (!result.autofill) { 2091 return; 2092 } 2093 2094 let isPlaceholderSelected = 2095 this._autofillPlaceholder && 2096 this.selectionEnd == this._autofillPlaceholder.value.length && 2097 this.selectionStart == this._lastSearchString.length && 2098 this._autofillPlaceholder.value 2099 .toLocaleLowerCase() 2100 .startsWith(this._lastSearchString.toLocaleLowerCase()); 2101 2102 // Don't autofill if there's already a selection (with one caveat described 2103 // next) or the cursor isn't at the end of the input. But if there is a 2104 // selection and it's the autofill placeholder value, then do autofill. 2105 if ( 2106 !isPlaceholderSelected && 2107 !this._autofillIgnoresSelection && 2108 (this.selectionStart != this.selectionEnd || 2109 this.selectionEnd != this._lastSearchString.length) 2110 ) { 2111 return; 2112 } 2113 2114 this.setValueFromResult({ result }); 2115 } 2116 /** 2117 * Clears displayed autofill values and unsets the autofill placeholder. 2118 */ 2119 #clearAutofill() { 2120 if (!this._autofillPlaceholder) { 2121 return; 2122 } 2123 let currentSelectionStart = this.selectionStart; 2124 let currentSelectionEnd = this.selectionEnd; 2125 2126 // Overriding this value clears the selection. 2127 this.#setInputValue( 2128 this.value.substring(0, this._autofillPlaceholder.selectionStart) 2129 ); 2130 this._autofillPlaceholder = null; 2131 // Restore selection 2132 this.setSelectionRange(currentSelectionStart, currentSelectionEnd); 2133 } 2134 2135 /** 2136 * Invoked by the controller when the first result is received. 2137 * 2138 * @param {UrlbarResult} firstResult 2139 * The first result received. 2140 * @returns {boolean} 2141 * True if this method canceled the query and started a new one. False 2142 * otherwise. 2143 */ 2144 onFirstResult(firstResult) { 2145 // If the heuristic result has a keyword but isn't a keyword offer, we may 2146 // need to enter search mode. 2147 if ( 2148 firstResult.heuristic && 2149 firstResult.payload.keyword && 2150 !this.#providesSearchMode(firstResult) && 2151 this.maybeConfirmSearchModeFromResult({ 2152 result: firstResult, 2153 entry: "typed", 2154 checkValue: false, 2155 }) 2156 ) { 2157 return true; 2158 } 2159 2160 // To prevent selection flickering, we apply autofill on input through a 2161 // placeholder, without waiting for results. But, if the first result is 2162 // not an autofill one, the autofill prediction was wrong and we should 2163 // restore the original user typed string. 2164 if (firstResult.autofill) { 2165 this._autofillFirstResult(firstResult); 2166 } else if ( 2167 this._autofillPlaceholder && 2168 // Avoid clobbering added spaces (for token aliases, for example). 2169 !this.value.endsWith(" ") 2170 ) { 2171 this._autofillPlaceholder = null; 2172 this._setValue(this.userTypedValue); 2173 } 2174 2175 return false; 2176 } 2177 2178 /** 2179 * Starts a query based on the current input value. 2180 * 2181 * @param {object} [options] 2182 * Object options 2183 * @param {boolean} [options.allowAutofill] 2184 * Whether or not to allow providers to include autofill results. 2185 * @param {boolean} [options.autofillIgnoresSelection] 2186 * Normally we autofill only if the cursor is at the end of the string, 2187 * if this is set we'll autofill regardless of selection. 2188 * @param {string} [options.searchString] 2189 * The search string. If not given, the current input value is used. 2190 * Otherwise, the current input value must start with this value. 2191 * @param {boolean} [options.resetSearchState] 2192 * If this is the first search of a user interaction with the input, set 2193 * this to true (the default) so that search-related state from the previous 2194 * interaction doesn't interfere with the new interaction. Otherwise set it 2195 * to false so that state is maintained during a single interaction. The 2196 * intended use for this parameter is that it should be set to false when 2197 * this method is called due to input events. 2198 * @param {event} [options.event] 2199 * The user-generated event that triggered the query, if any. If given, we 2200 * will record engagement event telemetry for the query. 2201 */ 2202 startQuery({ 2203 allowAutofill, 2204 autofillIgnoresSelection = false, 2205 searchString, 2206 resetSearchState = true, 2207 event, 2208 } = {}) { 2209 if (!searchString) { 2210 searchString = 2211 this.getAttribute("pageproxystate") == "valid" ? "" : this.value; 2212 } else if (!this.value.startsWith(searchString)) { 2213 throw new Error("The current value doesn't start with the search string"); 2214 } 2215 2216 let queryContext = this.#makeQueryContext({ 2217 allowAutofill, 2218 event, 2219 searchString, 2220 }); 2221 2222 if (event) { 2223 this.controller.engagementEvent.start(event, queryContext, searchString); 2224 } 2225 2226 if (this._suppressStartQuery) { 2227 return; 2228 } 2229 2230 this._autofillIgnoresSelection = autofillIgnoresSelection; 2231 if (resetSearchState) { 2232 this._resetSearchState(); 2233 } 2234 2235 if (this.searchMode) { 2236 this.confirmSearchMode(); 2237 } 2238 2239 this._lastSearchString = searchString; 2240 this._valueOnLastSearch = this.value; 2241 2242 // TODO (Bug 1522902): This promise is necessary for tests, because some 2243 // tests are not listening for completion when starting a query through 2244 // other methods than startQuery (input events for example). 2245 this.lastQueryContextPromise = this.controller.startQuery(queryContext); 2246 } 2247 2248 /** 2249 * Sets the input's value, starts a search, and opens the view. 2250 * 2251 * @param {string} value 2252 * The input's value will be set to this value, and the search will 2253 * use it as its query. 2254 * @param {object} [options] 2255 * Object options 2256 * @param {nsISearchEngine} [options.searchEngine] 2257 * Search engine to use when the search is using a known alias. 2258 * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry] 2259 * If provided, we will record this parameter as the search mode entry point 2260 * in Telemetry. Consumers should provide this if they expect their call 2261 * to enter search mode. 2262 * @param {boolean} [options.focus] 2263 * If true, the urlbar will be focused. If false, the focus will remain 2264 * unchanged. 2265 * @param {boolean} [options.startQuery] 2266 * If true, start query to show urlbar result by fireing input event. If 2267 * false, not fire the event. 2268 */ 2269 search(value, options = {}) { 2270 let { searchEngine, searchModeEntry, startQuery = true } = options; 2271 if (options.focus ?? true) { 2272 this.focus(); 2273 } 2274 let trimmedValue = value.trim(); 2275 let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES); 2276 let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end); 2277 // Enter search mode if the string starts with a restriction token. 2278 let searchMode = this.searchModeForToken(firstToken); 2279 let firstTokenIsRestriction = !!searchMode; 2280 if (!searchMode && searchEngine) { 2281 searchMode = { engineName: searchEngine.name }; 2282 firstTokenIsRestriction = searchEngine.aliases.includes(firstToken); 2283 } 2284 2285 if (searchMode) { 2286 searchMode.entry = searchModeEntry; 2287 this.searchMode = searchMode; 2288 if (firstTokenIsRestriction) { 2289 // Remove the restriction token/alias from the string to be searched for 2290 // in search mode. 2291 value = value.replace(firstToken, ""); 2292 } 2293 if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) { 2294 // If there was a trailing space after the restriction token/alias, 2295 // remove it. 2296 value = value.slice(1); 2297 } 2298 } else if ( 2299 Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken) 2300 ) { 2301 this.searchMode = null; 2302 // If the entire value is a restricted token, append a space. 2303 if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) { 2304 value += " "; 2305 } 2306 } 2307 this.#setInputValue(value); 2308 // Avoid selecting the text if this method is called twice in a row. 2309 this.selectionStart = -1; 2310 2311 if (startQuery) { 2312 // Note: proper IME Composition handling depends on the fact this generates 2313 // an input event, rather than directly invoking the controller; everything 2314 // goes through _on_input, that will properly skip the search until the 2315 // composition is committed. _on_input also skips the search when it's the 2316 // same as the previous search, but we want to allow consecutive searches 2317 // with the same string. So clear _lastSearchString first. 2318 this._lastSearchString = ""; 2319 let event = new UIEvent("input", { 2320 bubbles: true, 2321 cancelable: false, 2322 view: this.window, 2323 detail: 0, 2324 }); 2325 this.inputField.dispatchEvent(event); 2326 } 2327 } 2328 2329 /** 2330 * Returns a search mode object if a token should enter search mode when 2331 * typed. This does not handle engine aliases. 2332 * 2333 * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token 2334 * A restriction token to convert to search mode. 2335 * @returns {?object} 2336 * A search mode object. Null if search mode should not be entered. See 2337 * setSearchMode documentation for details. 2338 */ 2339 searchModeForToken(token) { 2340 if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) { 2341 return { 2342 engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate) 2343 ?.name, 2344 }; 2345 } 2346 2347 let mode = 2348 this.#isAddressbar && 2349 lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token); 2350 if (mode) { 2351 // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES. 2352 return { ...mode }; 2353 } 2354 2355 return null; 2356 } 2357 2358 /** 2359 * Opens a search page if the value is non-empty, otherwise opens the 2360 * search engine homepage (searchform). 2361 * 2362 * @param {string} value 2363 * @param {object} options 2364 * @param {nsISearchEngine} options.searchEngine 2365 */ 2366 openEngineHomePage(value, { searchEngine }) { 2367 if (!searchEngine) { 2368 console.warn("No searchEngine parameter"); 2369 return; 2370 } 2371 2372 let trimmedValue = value.trim(); 2373 let url; 2374 if (trimmedValue) { 2375 url = searchEngine.getSubmission(trimmedValue, null).uri.spec; 2376 // TODO: record SAP telemetry, see Bug 1961789. 2377 } else { 2378 url = searchEngine.searchForm; 2379 lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName); 2380 } 2381 2382 this._lastSearchString = ""; 2383 if (this.#isAddressbar) { 2384 this.#setInputValue(url); 2385 } 2386 this.selectionStart = -1; 2387 2388 this.window.openTrustedLinkIn(url, "current"); 2389 } 2390 2391 /** 2392 * Focus without the focus styles. 2393 * This is used by Activity Stream and about:privatebrowsing for search hand-off. 2394 */ 2395 setHiddenFocus() { 2396 this._hideFocus = true; 2397 if (this.focused) { 2398 this.removeAttribute("focused"); 2399 } else { 2400 this.focus(); 2401 } 2402 } 2403 2404 /** 2405 * Restore focus styles. 2406 * This is used by Activity Stream and about:privatebrowsing for search hand-off. 2407 * 2408 * @param {boolean} forceSuppressFocusBorder 2409 * Set true to suppress-focus-border attribute if this flag is true. 2410 */ 2411 removeHiddenFocus(forceSuppressFocusBorder = false) { 2412 this._hideFocus = false; 2413 if (this.focused) { 2414 this.toggleAttribute("focused", true); 2415 2416 if (forceSuppressFocusBorder) { 2417 this.toggleAttribute("suppress-focus-border", true); 2418 } 2419 } 2420 } 2421 2422 /** 2423 * Addressbar: Gets the search mode for a specific browser instance. 2424 * Searchbar: Gets the window-global search mode. 2425 * 2426 * @param {MozBrowser} browser 2427 * The search mode for this browser will be returned. 2428 * @param {boolean} [confirmedOnly] 2429 * Normally, if the browser has both preview and confirmed modes, preview 2430 * mode will be returned since it takes precedence. If this argument is 2431 * true, then only confirmed search mode will be returned, or null if 2432 * search mode hasn't been confirmed. 2433 * @returns {?object} 2434 * A search mode object or null if the browser/window is not in search mode. 2435 * See setSearchMode documentation. 2436 */ 2437 getSearchMode(browser, confirmedOnly = false) { 2438 let modes = this.#getSearchModesObject(browser); 2439 2440 // Return copies so that callers don't modify the stored values. 2441 if (!confirmedOnly && modes.preview) { 2442 return { ...modes.preview }; 2443 } 2444 if (modes.confirmed) { 2445 return { ...modes.confirmed }; 2446 } 2447 return null; 2448 } 2449 2450 /** 2451 * Addressbar: Sets the search mode for a specific browser instance. 2452 * Searchbar: Sets the window-global search mode. 2453 * If the given browser is selected, then this will also enter search mode. 2454 * 2455 * @param {object} searchMode 2456 * A search mode object. 2457 * @param {string} searchMode.engineName 2458 * The name of the search engine to restrict to. 2459 * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source 2460 * A result source to restrict to. 2461 * @param {string} searchMode.entry 2462 * How search mode was entered. This is recorded in event telemetry. One of 2463 * the values in UrlbarUtils.SEARCH_MODE_ENTRY. 2464 * @param {boolean} [searchMode.isPreview] 2465 * If true, we will preview search mode. Search mode preview does not record 2466 * telemetry and has slighly different UI behavior. The preview is exited in 2467 * favor of full search mode when a query is executed. False should be 2468 * passed if the caller needs to enter search mode but expects it will not 2469 * be interacted with right away. Defaults to true. 2470 * @param {MozBrowser} browser 2471 * The browser for which to set search mode. 2472 * Pass the selected browser for the searchbar. 2473 */ 2474 async setSearchMode(searchMode, browser) { 2475 if (this.#isSmartbarMode) { 2476 return; 2477 } 2478 let currentSearchMode = this.getSearchMode(browser); 2479 let areSearchModesSame = 2480 (!currentSearchMode && !searchMode) || 2481 lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode); 2482 2483 // Exit search mode if the passed-in engine is invalid or hidden. 2484 let engine; 2485 if (searchMode?.engineName) { 2486 if (!Services.search.isInitialized) { 2487 await Services.search.init(); 2488 } 2489 engine = Services.search.getEngineByName(searchMode.engineName); 2490 if (!engine || engine.hidden) { 2491 searchMode = null; 2492 } 2493 } 2494 2495 let { 2496 engineName, 2497 source, 2498 entry, 2499 restrictType, 2500 isPreview = true, 2501 } = searchMode || {}; 2502 2503 searchMode = null; 2504 2505 if (engineName) { 2506 searchMode = { 2507 engineName, 2508 isGeneralPurposeEngine: engine.isGeneralPurposeEngine, 2509 }; 2510 if (source) { 2511 searchMode.source = source; 2512 } else if (searchMode.isGeneralPurposeEngine) { 2513 // History results for general-purpose search engines are often not 2514 // useful, so we hide them in search mode. See bug 1658646 for 2515 // discussion. 2516 searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; 2517 } 2518 } else if (source) { 2519 let sourceName = lazy.UrlbarUtils.getResultSourceName(source); 2520 if (sourceName) { 2521 searchMode = { source }; 2522 } else { 2523 console.error(`Unrecognized source: ${source}`); 2524 } 2525 } 2526 2527 let modes = this.#getSearchModesObject(browser); 2528 2529 if (searchMode) { 2530 searchMode.isPreview = isPreview; 2531 if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) { 2532 searchMode.entry = entry; 2533 } else { 2534 // If we see this value showing up in telemetry, we should review 2535 // search mode's entry points. 2536 searchMode.entry = "other"; 2537 } 2538 2539 if (!searchMode.isPreview) { 2540 modes.confirmed = searchMode; 2541 delete modes.preview; 2542 } else { 2543 modes.preview = searchMode; 2544 } 2545 } else { 2546 delete modes.preview; 2547 delete modes.confirmed; 2548 } 2549 2550 if (restrictType) { 2551 searchMode.restrictType = restrictType; 2552 } 2553 2554 // Enter search mode if the browser is selected. 2555 if (browser == this.window.gBrowser.selectedBrowser) { 2556 this._updateSearchModeUI(searchMode); 2557 if (searchMode) { 2558 // Set userTypedValue to the query string so that it's properly restored 2559 // when switching back to the current tab and across sessions. 2560 this.userTypedValue = this.untrimmedValue; 2561 this.valueIsTyped = true; 2562 if (!searchMode.isPreview && !areSearchModesSame) { 2563 try { 2564 lazy.BrowserSearchTelemetry.recordSearchMode(searchMode); 2565 } catch (ex) { 2566 console.error(ex); 2567 } 2568 } 2569 } 2570 } 2571 } 2572 2573 /** 2574 * @typedef {object} SearchModesObject 2575 * 2576 * @property {object} [preview] preview search mode 2577 * @property {object} [confirmed] confirmed search mode 2578 */ 2579 2580 /** 2581 * @type {SearchModesObject|undefined} 2582 * 2583 * The (lazily initialized) search mode object for the searchbar. 2584 * This is needed because the searchbar has one search mode per window that 2585 * shouldn't change when switching tabs. For the address bar, the search mode 2586 * is stored per browser in #browserStates and this is always undefined. 2587 */ 2588 #searchbarSearchModes; 2589 2590 /** 2591 * Addressbar: Gets the search modes object for a specific browser instance. 2592 * Searchbar: Gets the window-global search modes object. 2593 * 2594 * @param {MozBrowser} browser 2595 * The browser to get the search modes object for. 2596 * Pass the selected browser for the searchbar. 2597 * @returns {SearchModesObject} 2598 */ 2599 #getSearchModesObject(browser) { 2600 if (!this.#isAddressbar) { 2601 // The passed browser doesn't matter here, but it does in setSearchMode. 2602 this.#searchbarSearchModes ??= {}; 2603 return this.#searchbarSearchModes; 2604 } 2605 2606 let state = this.getBrowserState(browser); 2607 state.searchModes ??= {}; 2608 return state.searchModes; 2609 } 2610 2611 /** 2612 * Restores the current browser search mode from a previously stored state. 2613 */ 2614 restoreSearchModeState() { 2615 if (this.#isSmartbarMode) { 2616 return; 2617 } 2618 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 2619 this.searchMode = state.searchModes?.confirmed; 2620 } 2621 2622 /** 2623 * Enters search mode with the default engine. 2624 */ 2625 searchModeShortcut() { 2626 // We restrict to search results when entering search mode from this 2627 // shortcut to honor historical behaviour. 2628 this.searchMode = { 2629 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 2630 engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name, 2631 entry: "shortcut", 2632 }; 2633 // The searchMode setter clears the input if pageproxystate is valid, so 2634 // we know at this point this.value will either be blank or the user's 2635 // typed string. 2636 this.search(this.value); 2637 this.select(); 2638 } 2639 2640 /** 2641 * Confirms the current search mode. 2642 */ 2643 confirmSearchMode() { 2644 let searchMode = this.searchMode; 2645 if (searchMode?.isPreview) { 2646 searchMode.isPreview = false; 2647 this.searchMode = searchMode; 2648 2649 // Unselect the one-off search button to ensure UI consistency. 2650 if (this.view.oneOffSearchButtons) { 2651 this.view.oneOffSearchButtons.selectedButton = null; 2652 } 2653 } 2654 } 2655 2656 // Getters and Setters below. 2657 2658 get editor() { 2659 if (this.#isSmartbarMode) { 2660 return this.#ensureSmartbarEditor(); 2661 } 2662 return this.inputField.editor; 2663 } 2664 2665 get focused() { 2666 return ( 2667 this.document.activeElement == 2668 (this.#smartbarInputController?.input ?? this.inputField) 2669 ); 2670 } 2671 2672 get goButton() { 2673 return this.querySelector(".urlbar-go-button"); 2674 } 2675 2676 get value() { 2677 return this.#smartbarInputController?.value ?? this.inputField.value; 2678 } 2679 2680 set value(val) { 2681 this._setValue(val, { allowTrim: true }); 2682 } 2683 2684 get untrimmedValue() { 2685 return this._untrimmedValue; 2686 } 2687 2688 get userTypedValue() { 2689 return this.#isAddressbar 2690 ? this.window.gBrowser.userTypedValue 2691 : this._userTypedValue; 2692 } 2693 2694 set userTypedValue(val) { 2695 if (this.#isAddressbar) { 2696 this.window.gBrowser.userTypedValue = val; 2697 } else { 2698 this._userTypedValue = val; 2699 } 2700 } 2701 2702 get lastSearchString() { 2703 return this._lastSearchString; 2704 } 2705 2706 get searchMode() { 2707 if (this.#isSmartbarMode) { 2708 return null; 2709 } 2710 if (!this.window.gBrowser) { 2711 // This only happens before DOMContentLoaded. 2712 return null; 2713 } 2714 return this.getSearchMode(this.window.gBrowser.selectedBrowser); 2715 } 2716 2717 set searchMode(searchMode) { 2718 if (this.#isSmartbarMode) { 2719 return; 2720 } 2721 this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser); 2722 this.searchModeSwitcher?.onSearchModeChanged(); 2723 lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window); 2724 } 2725 2726 getBrowserState(browser) { 2727 let state = this.#browserStates.get(browser); 2728 if (!state) { 2729 state = {}; 2730 this.#browserStates.set(browser, state); 2731 } 2732 return state; 2733 } 2734 2735 async #updateLayoutBreakout() { 2736 if (!this.#allowBreakout) { 2737 return; 2738 } 2739 if (this.document.fullscreenElement) { 2740 // Toolbars are hidden in DOM fullscreen mode, so we can't get proper 2741 // layout information and need to retry after leaving that mode. 2742 this.window.addEventListener( 2743 "fullscreen", 2744 () => { 2745 this.#updateLayoutBreakout(); 2746 }, 2747 { once: true } 2748 ); 2749 return; 2750 } 2751 await this.#updateLayoutBreakoutDimensions(); 2752 } 2753 2754 startLayoutExtend() { 2755 if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) { 2756 // Do not expand if the Urlbar does not support being expanded or it is 2757 // already expanded. 2758 return; 2759 } 2760 if (!this.view.isOpen) { 2761 return; 2762 } 2763 2764 this.#updateTextboxPosition(); 2765 2766 this.setAttribute("breakout-extend", "true"); 2767 2768 // Enable the animation only after the first extend call to ensure it 2769 // doesn't run when opening a new window. 2770 if (!this.hasAttribute("breakout-extend-animate")) { 2771 this.window.promiseDocumentFlushed(() => { 2772 this.window.requestAnimationFrame(() => { 2773 this.setAttribute("breakout-extend-animate", "true"); 2774 }); 2775 }); 2776 } 2777 } 2778 2779 endLayoutExtend() { 2780 // If reduce motion is enabled, we want to collapse the Urlbar here so the 2781 // user sees only sees two states: not expanded, and expanded with the view 2782 // open. 2783 if (!this.hasAttribute("breakout-extend") || this.view.isOpen) { 2784 return; 2785 } 2786 2787 this.removeAttribute("breakout-extend"); 2788 this.#updateTextboxPosition(); 2789 } 2790 2791 /** 2792 * Updates the user interface to indicate whether the URI in the address bar 2793 * is different than the loaded page, because it's being edited or because a 2794 * search result is currently selected and is displayed in the location bar. 2795 * 2796 * @param {string} state 2797 * The string "valid" indicates that the security indicators and other 2798 * related user interface elments should be shown because the URI in 2799 * the location bar matches the loaded page. The string "invalid" 2800 * indicates that the URI in the location bar is different than the 2801 * loaded page. 2802 * @param {boolean} [updatePopupNotifications] 2803 * Indicates whether we should update the PopupNotifications 2804 * visibility due to this change, otherwise avoid doing so as it is 2805 * being handled somewhere else. 2806 * @param {boolean} [forceUnifiedSearchButtonAvailable] 2807 * If this parameter is true, force to make Unified Search Button available. 2808 * Otherwise, the availability will be depedent on the proxy state. 2809 * Default value is false. 2810 */ 2811 setPageProxyState( 2812 state, 2813 updatePopupNotifications, 2814 forceUnifiedSearchButtonAvailable = false 2815 ) { 2816 if (!this.#isAddressbar) { 2817 return; 2818 } 2819 let prevState = this.getAttribute("pageproxystate"); 2820 2821 this.setAttribute("pageproxystate", state); 2822 this._inputContainer.setAttribute("pageproxystate", state); 2823 this._identityBox?.setAttribute("pageproxystate", state); 2824 this.setUnifiedSearchButtonAvailability( 2825 forceUnifiedSearchButtonAvailable || state == "invalid" 2826 ); 2827 2828 if (state == "valid") { 2829 this._lastValidURLStr = this.value; 2830 } 2831 2832 if ( 2833 updatePopupNotifications && 2834 prevState != state && 2835 this.window.UpdatePopupNotificationsVisibility 2836 ) { 2837 this.window.UpdatePopupNotificationsVisibility(); 2838 } 2839 } 2840 2841 /** 2842 * When switching tabs quickly, TabSelect sometimes happens before 2843 * _adjustFocusAfterTabSwitch and due to the focus still being on the old 2844 * tab, we end up flickering the results pane briefly. 2845 */ 2846 afterTabSwitchFocusChange() { 2847 this._gotFocusChange = true; 2848 this._afterTabSelectAndFocusChange(); 2849 } 2850 2851 /** 2852 * Confirms search mode and starts a new search if appropriate for the given 2853 * result. See also _searchModeForResult. 2854 * 2855 * @param {object} options 2856 * Options object. 2857 * @param {string} [options.entry] 2858 * If provided, this will be recorded as the entry point into search mode. 2859 * See setSearchMode documentation for details. 2860 * @param {UrlbarResult} [options.result] 2861 * The result to confirm. Defaults to the currently selected result. 2862 * @param {boolean} [options.checkValue] 2863 * If true, the trimmed input value must equal the result's keyword in order 2864 * to enter search mode. 2865 * @param {boolean} [options.startQuery] 2866 * If true, start a query after entering search mode. Defaults to true. 2867 * @returns {boolean} 2868 * True if we entered search mode and false if not. 2869 */ 2870 maybeConfirmSearchModeFromResult({ 2871 entry, 2872 result = this._resultForCurrentValue, 2873 checkValue = true, 2874 startQuery = true, 2875 }) { 2876 if ( 2877 !result || 2878 (checkValue && 2879 this.value.trim() != result.payload.keyword?.trim() && 2880 this.value.trim() != result.payload.autofillKeyword?.trim()) 2881 ) { 2882 return false; 2883 } 2884 2885 let searchMode = this._searchModeForResult(result, entry); 2886 if (!searchMode) { 2887 return false; 2888 } 2889 2890 this.searchMode = searchMode; 2891 2892 let value = result.payload.query?.trimStart() || ""; 2893 this._setValue(value); 2894 2895 if (startQuery) { 2896 this.startQuery({ allowAutofill: false }); 2897 } 2898 2899 return true; 2900 } 2901 2902 observe(subject, topic, data) { 2903 switch (topic) { 2904 case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: { 2905 let engine = subject.QueryInterface(Ci.nsISearchEngine); 2906 switch (data) { 2907 case lazy.SearchUtils.MODIFIED_TYPE.CHANGED: 2908 case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: { 2909 let searchMode = this.searchMode; 2910 if (searchMode?.engineName == engine.name) { 2911 // Exit search mode if the current search mode engine was removed. 2912 this.searchMode = searchMode; 2913 } 2914 break; 2915 } 2916 case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT: 2917 if (!this.isPrivate) { 2918 this._updatePlaceholder(engine.name); 2919 } 2920 break; 2921 case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE: 2922 if (this.isPrivate) { 2923 this._updatePlaceholder(engine.name); 2924 } 2925 break; 2926 } 2927 break; 2928 } 2929 } 2930 } 2931 2932 /** 2933 * Get search source. 2934 * 2935 * @param {Event} event 2936 * The event that triggered this query. 2937 * @returns {string} 2938 * The source name. 2939 */ 2940 getSearchSource(event) { 2941 if (this.#isAddressbar) { 2942 if (this._isHandoffSession) { 2943 return "urlbar-handoff"; 2944 } 2945 2946 const isOneOff = 2947 this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); 2948 if (this.searchMode && !isOneOff) { 2949 // Without checking !isOneOff, we might record the string 2950 // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to 2951 // oneoff_urlbar and oneoff_searchbar). The extra information is not 2952 // necessary; the intent is the same regardless of whether the user is 2953 // in search mode when they do a key-modified click/enter on a one-off. 2954 return "urlbar-searchmode"; 2955 } 2956 2957 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 2958 if (state.persist?.searchTerms && !isOneOff) { 2959 // Normally, we use state.persist.shouldPersist to check if search terms 2960 // persisted. However when the user modifies the search term, the boolean 2961 // will become false. Thus, we check the presence of the search terms to 2962 // know whether or not search terms ever persisted in the address bar. 2963 return "urlbar-persisted"; 2964 } 2965 } 2966 return this.#sapName; 2967 } 2968 2969 // Private methods below. 2970 2971 /* 2972 * Actions can have several buttons in the same result where not all 2973 * will provide a searchMode so check the currently selected button 2974 * in that case. 2975 */ 2976 #providesSearchMode(result) { 2977 if (!result) { 2978 return false; 2979 } 2980 if ( 2981 this.view.selectedElement && 2982 result.providerName == lazy.UrlbarProviderGlobalActions.name 2983 ) { 2984 return this.view.selectedElement.dataset.providesSearchmode == "true"; 2985 } 2986 return result.payload.providesSearchMode; 2987 } 2988 2989 _addObservers() { 2990 this._observer ??= { 2991 observe: this.observe.bind(this), 2992 QueryInterface: ChromeUtils.generateQI([ 2993 "nsIObserver", 2994 "nsISupportsWeakReference", 2995 ]), 2996 }; 2997 Services.obs.addObserver( 2998 this._observer, 2999 lazy.SearchUtils.TOPIC_ENGINE_MODIFIED, 3000 true 3001 ); 3002 } 3003 3004 _removeObservers() { 3005 if (this._observer) { 3006 Services.obs.removeObserver( 3007 this._observer, 3008 lazy.SearchUtils.TOPIC_ENGINE_MODIFIED 3009 ); 3010 this._observer = null; 3011 } 3012 } 3013 3014 _getURIFixupInfo(searchString) { 3015 let flags = 3016 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 3017 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; 3018 if (this.isPrivate) { 3019 flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; 3020 } 3021 try { 3022 return Services.uriFixup.getFixupURIInfo(searchString, flags); 3023 } catch (ex) { 3024 console.error( 3025 `An error occured while trying to fixup "${searchString}"`, 3026 ex 3027 ); 3028 } 3029 return null; 3030 } 3031 3032 _afterTabSelectAndFocusChange() { 3033 // We must have seen both events to proceed safely. 3034 if (!this._gotFocusChange || !this._gotTabSelect) { 3035 return; 3036 } 3037 this._gotFocusChange = this._gotTabSelect = false; 3038 3039 this.formatValue(); 3040 this._resetSearchState(); 3041 3042 // We don't use the original TabSelect event because caching it causes 3043 // leaks on MacOS. 3044 const event = new CustomEvent("tabswitch"); 3045 // If the urlbar is focused after a tab switch, record a potential 3046 // engagement event. When switching from a focused to a non-focused urlbar, 3047 // the blur event would record the abandonment. When switching from an 3048 // unfocused to a focused urlbar, there should be no search session ongoing, 3049 // so this will be a no-op. 3050 if (this.focused) { 3051 this.controller.engagementEvent.record(event, { 3052 searchString: this._lastSearchString, 3053 searchSource: this.getSearchSource(event), 3054 }); 3055 } 3056 3057 // Switching tabs doesn't always change urlbar focus, so we must try to 3058 // reopen here too, not just on focus. 3059 if (this.view.autoOpen({ event })) { 3060 return; 3061 } 3062 // The input may retain focus when switching tabs in which case we 3063 // need to close the view and search mode switcher popup explicitly. 3064 this.searchModeSwitcher.closePanel(); 3065 this.view.close(); 3066 } 3067 3068 #updateTextboxPosition() { 3069 if (!this.view.isOpen) { 3070 this.style.top = ""; 3071 return; 3072 } 3073 this.style.top = px( 3074 this.parentNode.getBoxQuads({ 3075 ignoreTransforms: true, 3076 flush: false, 3077 })[0].p1.y 3078 ); 3079 } 3080 3081 #updateTextboxPositionNextFrame() { 3082 if (!this.hasAttribute("breakout")) { 3083 return; 3084 } 3085 // Allow for any layout changes to take place (e.g. when the menubar becomes 3086 // inactive) before re-measuring to position the textbox 3087 this.window.requestAnimationFrame(() => { 3088 this.window.requestAnimationFrame(() => { 3089 this.#updateTextboxPosition(); 3090 }); 3091 }); 3092 } 3093 3094 #stopBreakout() { 3095 this.removeAttribute("breakout"); 3096 this.parentNode.removeAttribute("breakout"); 3097 this.style.top = ""; 3098 try { 3099 this.hidePopover(); 3100 } catch (ex) { 3101 // No big deal if not a popover already. 3102 } 3103 this._layoutBreakoutUpdateKey = {}; 3104 } 3105 3106 incrementBreakoutBlockerCount() { 3107 this.#breakoutBlockerCount++; 3108 if (this.#breakoutBlockerCount == 1) { 3109 this.#stopBreakout(); 3110 } 3111 } 3112 3113 decrementBreakoutBlockerCount() { 3114 if (this.#breakoutBlockerCount > 0) { 3115 this.#breakoutBlockerCount--; 3116 } 3117 if (this.#breakoutBlockerCount === 0) { 3118 this.#updateLayoutBreakout(); 3119 } 3120 } 3121 3122 async #updateLayoutBreakoutDimensions() { 3123 this.#stopBreakout(); 3124 3125 // When this method gets called a second time before the first call 3126 // finishes, we need to disregard the first one. 3127 let updateKey = {}; 3128 this._layoutBreakoutUpdateKey = updateKey; 3129 await this.window.promiseDocumentFlushed(() => {}); 3130 await new Promise(resolve => { 3131 this.window.requestAnimationFrame(() => { 3132 if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) { 3133 return; 3134 } 3135 3136 this.parentNode.style.setProperty( 3137 "--urlbar-container-height", 3138 px(getBoundsWithoutFlushing(this.parentNode).height) 3139 ); 3140 this.style.setProperty( 3141 "--urlbar-height", 3142 px(getBoundsWithoutFlushing(this).height) 3143 ); 3144 3145 if (this.#breakoutBlockerCount) { 3146 return; 3147 } 3148 3149 this.setAttribute("breakout", "true"); 3150 this.parentNode.setAttribute("breakout", "true"); 3151 this.showPopover(); 3152 this.#updateTextboxPosition(); 3153 3154 resolve(); 3155 }); 3156 }); 3157 } 3158 3159 /** 3160 * Sets the input field value. 3161 * 3162 * @param {string} val The new value to set. 3163 * @param {object} [options] Options for setting. 3164 * @param {boolean} [options.allowTrim] Whether the value can be trimmed. 3165 * @param {string} [options.untrimmedValue] Override for this._untrimmedValue. 3166 * @param {boolean} [options.valueIsTyped] Override for this.valueIsTypede. 3167 * @param {string} [options.actionType] Value for the `actiontype` attribute. 3168 * 3169 * @returns {string} The set value. 3170 */ 3171 _setValue( 3172 val, 3173 { 3174 allowTrim = false, 3175 untrimmedValue = null, 3176 valueIsTyped = false, 3177 actionType = undefined, 3178 } = {} 3179 ) { 3180 // Don't expose internal about:reader URLs to the user. 3181 let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val); 3182 if (originalUrl) { 3183 val = originalUrl.displaySpec; 3184 } 3185 this._untrimmedValue = untrimmedValue ?? val; 3186 this._protocolIsTrimmed = false; 3187 if (allowTrim) { 3188 let oldVal = val; 3189 val = this._trimValue(val); 3190 this._protocolIsTrimmed = 3191 oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && 3192 !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol); 3193 } 3194 3195 this.valueIsTyped = valueIsTyped; 3196 this._resultForCurrentValue = null; 3197 this.#setInputValue(val); 3198 this.formatValue(); 3199 3200 if (actionType !== undefined) { 3201 this.setAttribute("actiontype", actionType); 3202 } else { 3203 this.removeAttribute("actiontype"); 3204 } 3205 3206 // Dispatch ValueChange event for accessibility. 3207 let event = this.document.createEvent("Events"); 3208 event.initEvent("ValueChange", true, true); 3209 this.inputField.dispatchEvent(event); 3210 3211 return val; 3212 } 3213 3214 /** 3215 * Extracts a input value from a UrlbarResult, used when filling the input 3216 * field on selecting a result. 3217 * 3218 * Some examples: 3219 * - If the result is a bookmark keyword or dynamic, the value will be 3220 * its `input` property. 3221 * - If the result is search, the value may be `keyword` combined with 3222 * `suggestion` or `query`. 3223 * - If the result is WebExtension Omnibox, the value will be extracted 3224 * from `content`. 3225 * - For results returning URLs the value may be `urlOverride` or `url`. 3226 * 3227 * @param {UrlbarResult} result 3228 * The result to extract the value from. 3229 * @param {object} options 3230 * Options object. 3231 * @param {string} [options.urlOverride] 3232 * For results normally returning a url string, this allows to override 3233 * it. A blank string may passed-in to clear the input. 3234 * @param {HTMLElement} [options.element] 3235 * The element that was selected or picked, if available. For results that 3236 * have multiple selectable children, the value may be taken from a child 3237 * element rather than the result. 3238 * @returns {string} The value. 3239 */ 3240 #getValueFromResult(result, { urlOverride = null, element = null } = {}) { 3241 switch (result.type) { 3242 case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: 3243 return result.payload.input; 3244 case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { 3245 let value = ""; 3246 if (result.payload.keyword) { 3247 value += result.payload.keyword + " "; 3248 } 3249 value += result.payload.suggestion || result.payload.query; 3250 return value; 3251 } 3252 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: 3253 return result.payload.content; 3254 case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: 3255 return ( 3256 element?.dataset.query || 3257 element?.dataset.url || 3258 result.payload.input || 3259 result.payload.query || 3260 "" 3261 ); 3262 case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: 3263 return result.payload.autofillKeyword + " "; 3264 case lazy.UrlbarUtils.RESULT_TYPE.TIP: { 3265 let value = element?.dataset.url || element?.dataset.input; 3266 if (value) { 3267 return value; 3268 } 3269 break; 3270 } 3271 } 3272 3273 // Always respect a set urlOverride property. 3274 if (urlOverride !== null) { 3275 // This returns null for the empty string, allowing callers to clear the 3276 // input by passing an empty string as urlOverride. 3277 let url = URL.parse(urlOverride); 3278 return url ? losslessDecodeURI(url.URI) : ""; 3279 } 3280 3281 let parsedUrl = URL.parse(result.payload.url); 3282 // If the url is not parsable, just return an empty string; 3283 if (!parsedUrl) { 3284 return ""; 3285 } 3286 3287 let url = losslessDecodeURI(parsedUrl.URI); 3288 // If the user didn't originally type a protocol, and we generated one, 3289 // trim the http protocol from the input value, as https-first may upgrade 3290 // it to https, breaking user expectations. 3291 let stripHttp = 3292 result.heuristic && 3293 result.payload.url.startsWith("http://") && 3294 this.userTypedValue && 3295 this.#getSchemelessInput(this.userTypedValue) == 3296 Ci.nsILoadInfo.SchemelessInputTypeSchemeless; 3297 if (!stripHttp) { 3298 return url; 3299 } 3300 // Attempt to trim the url. If doing so results in a string that is 3301 // interpreted as search (e.g. unknown single word host, or domain suffix), 3302 // use the unmodified url instead. Otherwise, if the user edits the url 3303 // and confirms the new value, we may transform the url into a search. 3304 let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0]; 3305 let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent; 3306 if (isSearch) { 3307 // Although https-first might not respect the shown protocol, converting 3308 // the result to a search would be more disruptive. 3309 return url; 3310 } 3311 return trimmedUrl; 3312 } 3313 3314 /** 3315 * Extracts from a result the value to use for the `actiontype` attribute. 3316 * 3317 * @param {UrlbarResult} result The UrlbarResult to consider. 3318 * 3319 * @returns {string} The `actiontype` value, or undefined. 3320 */ 3321 #getActionTypeFromResult(result) { 3322 switch (result.type) { 3323 case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: 3324 return "switchtab"; 3325 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: 3326 return "extension"; 3327 default: 3328 return undefined; 3329 } 3330 } 3331 3332 /** 3333 * Resets some state so that searches from the user's previous interaction 3334 * with the input don't interfere with searches from a new interaction. 3335 */ 3336 _resetSearchState() { 3337 this._lastSearchString = this.value; 3338 this._autofillPlaceholder = null; 3339 } 3340 3341 /** 3342 * Autofills the autofill placeholder string if appropriate, and determines 3343 * whether autofill should be allowed for the new search started by an input 3344 * event. 3345 * 3346 * @param {string} value 3347 * The new search string. 3348 * @returns {boolean} 3349 * Whether autofill should be allowed in the new search. 3350 */ 3351 _maybeAutofillPlaceholder(value) { 3352 // We allow autofill in local but not remote search modes. 3353 let allowAutofill = 3354 this.selectionEnd == value.length && 3355 !this.searchMode?.engineName && 3356 this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; 3357 3358 if (!allowAutofill) { 3359 this.#clearAutofill(); 3360 return false; 3361 } 3362 3363 // Determine whether we can autofill the placeholder. The placeholder is a 3364 // value that we autofill now, when the search starts and before we wait on 3365 // its first result, in order to prevent a flicker in the input caused by 3366 // the previous autofilled substring disappearing and reappearing when the 3367 // first result arrives. Of course we can only autofill the placeholder if 3368 // it starts with the new search string, and we shouldn't autofill anything 3369 // if the caret isn't at the end of the input. 3370 let canAutofillPlaceholder = false; 3371 if (this._autofillPlaceholder) { 3372 if (this._autofillPlaceholder.type == "adaptive") { 3373 canAutofillPlaceholder = 3374 value.length >= 3375 this._autofillPlaceholder.adaptiveHistoryInput.length && 3376 this._autofillPlaceholder.value 3377 .toLocaleLowerCase() 3378 .startsWith(value.toLocaleLowerCase()); 3379 } else { 3380 canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL( 3381 this._autofillPlaceholder.value, 3382 value 3383 ); 3384 } 3385 } 3386 3387 if (!canAutofillPlaceholder) { 3388 this._autofillPlaceholder = null; 3389 } else if ( 3390 this._autofillPlaceholder && 3391 this.selectionEnd == this.value.length && 3392 this._enableAutofillPlaceholder 3393 ) { 3394 let autofillValue = 3395 value + this._autofillPlaceholder.value.substring(value.length); 3396 if ( 3397 this.value === autofillValue && 3398 this.selectionStart === value.length && 3399 this.selectionEnd === autofillValue.length 3400 ) { 3401 return true; 3402 } 3403 this._autofillValue({ 3404 value: autofillValue, 3405 selectionStart: value.length, 3406 selectionEnd: autofillValue.length, 3407 type: this._autofillPlaceholder.type, 3408 adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput, 3409 untrimmedValue: this._autofillPlaceholder.untrimmedValue, 3410 }); 3411 } 3412 3413 return true; 3414 } 3415 3416 /** 3417 * Invoked on overflow/underflow/scrollend events to update attributes 3418 * related to the input text directionality. Overflow fade masks use these 3419 * attributes to appear at the proper side of the urlbar. 3420 */ 3421 updateTextOverflow() { 3422 if (!this._overflowing) { 3423 this.removeAttribute("textoverflow"); 3424 return; 3425 } 3426 3427 let isRTL = 3428 this.getAttribute("domaindir") === "rtl" && 3429 lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window); 3430 3431 this.window.promiseDocumentFlushed(() => { 3432 // Check overflow again to ensure it didn't change in the meanwhile. 3433 let input = this.inputField; 3434 if (input && this._overflowing) { 3435 // Normally we overflow at the end side of the text direction, though 3436 // RTL domains may cause us to overflow at the opposite side. 3437 // The outcome differs depending on the input field contents and applied 3438 // formatting, and reports the final state of all the scrolling into an 3439 // attribute available to css rules. 3440 // Note it's also possible to scroll an unfocused input field using 3441 // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad 3442 // scroll (without modifiers) on Mac. 3443 let side = "both"; 3444 if (isRTL) { 3445 if (input.scrollLeft == 0) { 3446 side = "left"; 3447 } else if (input.scrollLeft == input.scrollLeftMin) { 3448 side = "right"; 3449 } 3450 } else if (input.scrollLeft == 0) { 3451 side = "right"; 3452 } else if (input.scrollLeft == input.scrollLeftMax) { 3453 side = "left"; 3454 } 3455 3456 this.window.requestAnimationFrame(() => { 3457 // And check once again, since we might have stopped overflowing 3458 // since the promiseDocumentFlushed callback fired. 3459 if (this._overflowing) { 3460 this.setAttribute("textoverflow", side); 3461 } 3462 }); 3463 } 3464 }); 3465 } 3466 3467 _updateUrlTooltip() { 3468 if (this.focused || !this._overflowing) { 3469 this.inputField.removeAttribute("title"); 3470 } else { 3471 this.inputField.setAttribute("title", this.untrimmedValue); 3472 } 3473 } 3474 3475 _getSelectedValueForClipboard() { 3476 let selectedVal = this.#selectedText; 3477 3478 // Handle multiple-range selection as a string for simplicity. 3479 if (this.editor.selection.rangeCount > 1) { 3480 return selectedVal; 3481 } 3482 3483 // If the selection doesn't start at the beginning or doesn't span the 3484 // full domain or the URL bar is modified or there is no text at all, 3485 // nothing else to do here. 3486 // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often 3487 // it doesn't really indicate a user typed a value, it's rather used as 3488 // a way to tell if the value was modified. 3489 if ( 3490 this.selectionStart > 0 || 3491 selectedVal == "" || 3492 (this.valueIsTyped && !this._protocolIsTrimmed) 3493 ) { 3494 return selectedVal; 3495 } 3496 3497 // The selection doesn't span the full domain if it doesn't contain a slash and is 3498 // followed by some character other than a slash. 3499 if (!selectedVal.includes("/")) { 3500 let remainder = this.value.replace(selectedVal, ""); 3501 if (remainder != "" && remainder[0] != "/") { 3502 return selectedVal; 3503 } 3504 } 3505 3506 let uri; 3507 if (this.getAttribute("pageproxystate") == "valid") { 3508 uri = this.#isOpenedPageInBlankTargetLoading 3509 ? this.window.gBrowser.selectedBrowser.browsingContext 3510 .nonWebControlledBlankURI 3511 : this.window.gBrowser.currentURI; 3512 } else { 3513 // The value could be: 3514 // 1. a trimmed url, set by selecting a result 3515 // 2. a search string set by selecting a result 3516 // 3. a url that was confirmed but didn't finish loading yet 3517 // If it's an url the untrimmedValue should resolve to a valid URI, 3518 // otherwise it's a search string that should be copied as-is. 3519 3520 // If the copied text is that autofilled value, return the url including 3521 // the protocol from its suggestion. 3522 let result = this._resultForCurrentValue; 3523 3524 if (result?.autofill?.value == selectedVal) { 3525 return result.payload.url; 3526 } 3527 3528 uri = URL.parse(this._untrimmedValue)?.URI; 3529 if (!uri) { 3530 return selectedVal; 3531 } 3532 } 3533 uri = this.makeURIReadable(uri); 3534 let displaySpec = uri.displaySpec; 3535 3536 // If the entire URL is selected, just use the actual loaded URI, 3537 // unless we want a decoded URI, or it's a data: or javascript: URI, 3538 // since those are hard to read when encoded. 3539 if ( 3540 this.value == selectedVal && 3541 !uri.schemeIs("javascript") && 3542 !uri.schemeIs("data") && 3543 !lazy.UrlbarPrefs.get("decodeURLsOnCopy") 3544 ) { 3545 return displaySpec; 3546 } 3547 3548 // Just the beginning of the URL is selected, or we want a decoded 3549 // url. First check for a trimmed value. 3550 3551 if ( 3552 !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && 3553 // Note _trimValue may also trim a trailing slash, thus we can't just do 3554 // a straight string compare to tell if the protocol was trimmed. 3555 !displaySpec.startsWith(this._trimValue(displaySpec)) 3556 ) { 3557 selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal; 3558 } 3559 3560 // If selection starts from the beginning and part or all of the URL 3561 // is selected, we check for decoded characters and encode them. 3562 // Unless decodeURLsOnCopy is set. Do not encode data: URIs. 3563 if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) { 3564 try { 3565 if (URL.canParse(selectedVal)) { 3566 // Use encodeURI instead of URL.href because we don't want 3567 // trailing slash. 3568 selectedVal = encodeURI(selectedVal); 3569 } 3570 } catch (ex) { 3571 // URL is invalid. Return original selected value. 3572 } 3573 } 3574 3575 return selectedVal; 3576 } 3577 3578 _toggleActionOverride(event) { 3579 if ( 3580 event.keyCode == KeyEvent.DOM_VK_SHIFT || 3581 event.keyCode == KeyEvent.DOM_VK_ALT || 3582 event.keyCode == 3583 (AppConstants.platform == "macosx" 3584 ? KeyEvent.DOM_VK_META 3585 : KeyEvent.DOM_VK_CONTROL) 3586 ) { 3587 if (event.type == "keydown") { 3588 this._actionOverrideKeyCount++; 3589 this.toggleAttribute("action-override", true); 3590 this.view.panel.setAttribute("action-override", true); 3591 } else if ( 3592 this._actionOverrideKeyCount && 3593 --this._actionOverrideKeyCount == 0 3594 ) { 3595 this._clearActionOverride(); 3596 } 3597 } 3598 } 3599 3600 _clearActionOverride() { 3601 this._actionOverrideKeyCount = 0; 3602 this.removeAttribute("action-override"); 3603 this.view.panel.removeAttribute("action-override"); 3604 } 3605 3606 /** 3607 * Records in telemetry that a search is being loaded, 3608 * updates an incremental total number of searches in a pref, 3609 * and informs ASRouter that a search has occurred via a trigger send 3610 * 3611 * @param {nsISearchEngine} engine 3612 * The engine to generate the query for. 3613 * @param {Event} event 3614 * The event that triggered this query. 3615 * @param {object} [searchActionDetails] 3616 * The details associated with this search query. 3617 * @param {boolean} [searchActionDetails.isSuggestion] 3618 * True if this query was initiated from a suggestion from the search engine. 3619 * @param {boolean} [searchActionDetails.alias] 3620 * True if this query was initiated via a search alias. 3621 * @param {boolean} [searchActionDetails.isFormHistory] 3622 * True if this query was initiated from a form history result. 3623 * @param {string} [searchActionDetails.url] 3624 * The url this query was triggered with. 3625 * @param {MozBrowser} [browser] 3626 * The browser where the search is being opened. 3627 * Defaults to the window's selected browser. 3628 */ 3629 _recordSearch( 3630 engine, 3631 event, 3632 searchActionDetails = {}, 3633 browser = this.window.gBrowser.selectedBrowser 3634 ) { 3635 const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); 3636 const searchSource = this.getSearchSource(event); 3637 3638 // Record when the user uses the search bar to be 3639 // used for message targeting. This is arbitrarily capped 3640 // at 100, only to prevent the number from growing ifinitely. 3641 const totalSearches = Services.prefs.getIntPref( 3642 "browser.search.totalSearches" 3643 ); 3644 const totalSearchesCap = 100; 3645 if (totalSearches < totalSearchesCap) { 3646 Services.prefs.setIntPref( 3647 "browser.search.totalSearches", 3648 totalSearches + 1 3649 ); 3650 } 3651 3652 // Sending a trigger to ASRouter when a search happens 3653 lazy.ASRouter.sendTriggerMessage({ 3654 browser, 3655 id: "onSearch", 3656 context: { 3657 isSuggestion: searchActionDetails.isSuggestion || false, 3658 searchSource, 3659 isOneOff, 3660 }, 3661 }); 3662 3663 lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, { 3664 ...searchActionDetails, 3665 isOneOff, 3666 newtabSessionId: this._handoffSession, 3667 }); 3668 } 3669 3670 /** 3671 * Shortens the given value, usually by removing http:// and trailing slashes. 3672 * 3673 * @param {string} val 3674 * The string to be trimmed if it appears to be URI 3675 * @returns {string} 3676 * The trimmed string 3677 */ 3678 _trimValue(val) { 3679 if (!this.#isAddressbar) { 3680 return val; 3681 } 3682 let trimmedValue = lazy.UrlbarPrefs.get("trimURLs") 3683 ? lazy.BrowserUIUtils.trimURL(val) 3684 : val; 3685 // Only trim value if the directionality doesn't change to RTL and we're not 3686 // showing a strikeout https protocol. 3687 return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) || 3688 this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val) 3689 ? val 3690 : trimmedValue; 3691 } 3692 3693 /** 3694 * Returns whether the passed-in event may represents a canonization request. 3695 * 3696 * @param {Event} event 3697 * An Event to examine. 3698 * @returns {boolean} 3699 * Whether the event is a KeyboardEvent that triggers canonization. 3700 */ 3701 #isCanonizeKeyboardEvent(event) { 3702 return ( 3703 KeyboardEvent.isInstance(event) && 3704 event.keyCode == KeyEvent.DOM_VK_RETURN && 3705 (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) && 3706 !event._disableCanonization && 3707 lazy.UrlbarPrefs.get("ctrlCanonizesURLs") 3708 ); 3709 } 3710 3711 /** 3712 * If appropriate, this prefixes a search string with 'www.' and suffixes it 3713 * with browser.fixup.alternate.suffix prior to navigating. 3714 * 3715 * @param {Event} event 3716 * The event that triggered this query. 3717 * @param {string} value 3718 * The search string that should be canonized. 3719 * @returns {string} 3720 * Returns the canonized URL if available and null otherwise. 3721 */ 3722 _maybeCanonizeURL(event, value) { 3723 // Only add the suffix when the URL bar value isn't already "URL-like", 3724 // and only if we get a keyboard event, to match user expectations. 3725 if ( 3726 this.sapName == "searchbar" || 3727 !this.#isCanonizeKeyboardEvent(event) || 3728 !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value) 3729 ) { 3730 return null; 3731 } 3732 3733 let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix"); 3734 if (!suffix.endsWith("/")) { 3735 suffix += "/"; 3736 } 3737 3738 // trim leading/trailing spaces (bug 233205) 3739 value = value.trim(); 3740 3741 // Tack www. and suffix on. If user has appended directories, insert 3742 // suffix before them (bug 279035). Be careful not to get two slashes. 3743 let firstSlash = value.indexOf("/"); 3744 if (firstSlash >= 0) { 3745 value = 3746 value.substring(0, firstSlash) + 3747 suffix + 3748 value.substring(firstSlash + 1); 3749 } else { 3750 value = value + suffix; 3751 } 3752 3753 try { 3754 const info = Services.uriFixup.getFixupURIInfo( 3755 value, 3756 Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI 3757 ); 3758 value = info.fixedURI.spec; 3759 } catch (ex) { 3760 console.error(`An error occured while trying to fixup "${value}"`, ex); 3761 } 3762 3763 this.value = value; 3764 return value; 3765 } 3766 3767 /** 3768 * Autofills a value into the input. The value will be autofilled regardless 3769 * of the input's current value. 3770 * 3771 * @param {object} options 3772 * The options object. 3773 * @param {string} options.value 3774 * The value to autofill. 3775 * @param {number} options.selectionStart 3776 * The new selectionStart. 3777 * @param {number} options.selectionEnd 3778 * The new selectionEnd. 3779 * @param {"origin" | "url" | "adaptive"} options.type 3780 * The autofill type, one of: "origin", "url", "adaptive" 3781 * @param {string} options.adaptiveHistoryInput 3782 * If the autofill type is "adaptive", this is the matching `input` value 3783 * from adaptive history. 3784 * @param {string} [options.untrimmedValue] 3785 * Untrimmed value including a protocol. 3786 */ 3787 _autofillValue({ 3788 value, 3789 selectionStart, 3790 selectionEnd, 3791 type, 3792 adaptiveHistoryInput, 3793 untrimmedValue, 3794 }) { 3795 const valueMatches = this.value === value; 3796 const selectionMatches = 3797 this.selectionStart === selectionStart && 3798 this.selectionEnd === selectionEnd; 3799 if (valueMatches && selectionMatches) { 3800 return; 3801 } 3802 // The autofilled value may be a URL that includes a scheme at the 3803 // beginning. Do not allow it to be trimmed. 3804 if (!valueMatches) { 3805 this._setValue(value, { untrimmedValue }); 3806 } 3807 this.setSelectionRange(selectionStart, selectionEnd); 3808 // Ensure selection state is cached for contenteditable and events fire. 3809 if (!selectionMatches) { 3810 if (this.#smartbarInputController) { 3811 this.#smartbarInputController.dispatchSelectionChange(); 3812 } else { 3813 this.inputField.dispatchEvent( 3814 new Event("selectionchange", { bubbles: true, cancelable: false }) 3815 ); 3816 } 3817 } 3818 this._autofillPlaceholder = { 3819 value, 3820 type, 3821 adaptiveHistoryInput, 3822 selectionStart, 3823 selectionEnd, 3824 untrimmedValue, 3825 }; 3826 } 3827 3828 /** 3829 * Called when a menu item from results menu is picked. 3830 * 3831 * @param {UrlbarResult} result The result that was picked. 3832 * @param {Event} event The event that picked the result. 3833 * @param {HTMLElement} element the picked view element, if available. 3834 * @param {object} browser The browser to use for the load. 3835 */ 3836 #pickMenuResult(result, event, element, browser) { 3837 this.controller.engagementEvent.record(event, { 3838 result, 3839 element, 3840 searchString: this._lastSearchString, 3841 selType: element.dataset.command, 3842 }); 3843 3844 if (element.dataset.command == "manage") { 3845 this.window.openPreferences("search-locationBar"); 3846 return; 3847 } 3848 3849 let url; 3850 if (element.dataset.command == "help") { 3851 url = result.payload.helpUrl; 3852 } 3853 url ||= element.dataset.url; 3854 3855 if (!url) { 3856 return; 3857 } 3858 3859 let where = this._whereToOpen(event); 3860 if (element.dataset.command == "help" && where == "current") { 3861 // Open help links in a new tab. 3862 where = "tab"; 3863 } 3864 3865 this.view.close({ elementPicked: true }); 3866 3867 this._loadURL( 3868 url, 3869 event, 3870 where, 3871 { 3872 allowInheritPrincipal: false, 3873 private: this.isPrivate, 3874 }, 3875 { 3876 source: result.source, 3877 type: result.type, 3878 }, 3879 browser 3880 ); 3881 } 3882 3883 /** 3884 * Loads the url in the appropriate place. 3885 * 3886 * @param {string} url 3887 * The URL to open. 3888 * @param {string} openUILinkWhere 3889 * Where we expect the result to be opened. 3890 * @param {object} params 3891 * The parameters related to how and where the result will be opened. 3892 * Further supported paramters are listed in _loadURL. 3893 * @param {object} [params.triggeringPrincipal] 3894 * The principal that the action was triggered from. 3895 * @param {object} [resultDetails] 3896 * Details of the selected result, if any. 3897 * Further supported details are listed in _loadURL. 3898 * @param {string} [resultDetails.searchTerm] 3899 * Search term of the result source, if any. 3900 * @param {object} browser the browser to use for the load. 3901 */ 3902 #prepareAddressbarLoad( 3903 url, 3904 openUILinkWhere, 3905 params, 3906 resultDetails = null, 3907 browser 3908 ) { 3909 if (!this.#isAddressbar) { 3910 throw new Error( 3911 "Can't prepare addressbar load when this isn't an addressbar input" 3912 ); 3913 } 3914 3915 // No point in setting these because we'll handleRevert() a few rows below. 3916 if (openUILinkWhere == "current") { 3917 // Make sure URL is formatted properly (don't show punycode). 3918 let formattedURL = url; 3919 try { 3920 formattedURL = losslessDecodeURI(new URL(url).URI); 3921 } catch {} 3922 3923 this.value = 3924 lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() && 3925 resultDetails?.searchTerm 3926 ? resultDetails.searchTerm 3927 : formattedURL; 3928 browser.userTypedValue = this.value; 3929 } 3930 3931 // No point in setting this if we are loading in a new window. 3932 if ( 3933 openUILinkWhere != "window" && 3934 this.window.gInitialPages.includes(url) 3935 ) { 3936 browser.initialPageLoadedFromUserAction = url; 3937 } 3938 3939 try { 3940 lazy.UrlbarUtils.addToUrlbarHistory(url, this.window); 3941 } catch (ex) { 3942 // Things may go wrong when adding url to session history, 3943 // but don't let that interfere with the loading of the url. 3944 console.error(ex); 3945 } 3946 3947 // TODO: When bug 1498553 is resolved, we should be able to 3948 // remove the !triggeringPrincipal condition here. 3949 if ( 3950 !params.triggeringPrincipal || 3951 params.triggeringPrincipal.isSystemPrincipal 3952 ) { 3953 // Reset DOS mitigations for the basic auth prompt. 3954 delete browser.authPromptAbuseCounter; 3955 3956 // Reset temporary permissions on the current tab if the user reloads 3957 // the tab via the urlbar. 3958 if ( 3959 openUILinkWhere == "current" && 3960 browser.currentURI && 3961 url === browser.currentURI.spec 3962 ) { 3963 this.window.SitePermissions.clearTemporaryBlockPermissions(browser); 3964 } 3965 } 3966 3967 // Specifies that the URL load was initiated by the URL bar. 3968 params.initiatedByURLBar = true; 3969 } 3970 3971 /** 3972 * Loads the url in the appropriate place. 3973 * 3974 * @param {string} url 3975 * The URL to open. 3976 * @param {Event} event 3977 * The event that triggered to load the url. 3978 * @param {string} openUILinkWhere 3979 * Where we expect the result to be opened. 3980 * @param {object} params 3981 * The parameters related to how and where the result will be opened. 3982 * Further supported parameters are listed in utilityOverlay.js#openUILinkIn. 3983 * @param {object} [params.triggeringPrincipal] 3984 * The principal that the action was triggered from. 3985 * @param {nsIInputStream} [params.postData] 3986 * The POST data associated with a search submission. 3987 * @param {boolean} [params.allowInheritPrincipal] 3988 * Whether the principal can be inherited. 3989 * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput] 3990 * Whether the search/URL term was without an explicit scheme. 3991 * @param {object} [resultDetails] 3992 * Details of the selected result, if any. 3993 * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type] 3994 * Details of the result type, if any. 3995 * @param {string} [resultDetails.searchTerm] 3996 * Search term of the result source, if any. 3997 * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source] 3998 * Details of the result source, if any. 3999 * @param {object} browser [optional] the browser to use for the load. 4000 */ 4001 _loadURL( 4002 url, 4003 event, 4004 openUILinkWhere, 4005 params, 4006 resultDetails = null, 4007 browser = this.window.gBrowser.selectedBrowser 4008 ) { 4009 if (this.#isAddressbar) { 4010 this.#prepareAddressbarLoad( 4011 url, 4012 openUILinkWhere, 4013 params, 4014 resultDetails, 4015 browser 4016 ); 4017 } 4018 4019 params.allowThirdPartyFixup = true; 4020 4021 if (openUILinkWhere == "current") { 4022 params.targetBrowser = browser; 4023 params.indicateErrorPageLoad = true; 4024 params.allowPinnedTabHostChange = true; 4025 params.allowPopups = url.startsWith("javascript:"); 4026 } else { 4027 params.initiatingDoc = this.window.document; 4028 } 4029 4030 if ( 4031 this._keyDownEnterDeferred && 4032 event?.keyCode === KeyEvent.DOM_VK_RETURN && 4033 openUILinkWhere === "current" 4034 ) { 4035 // In this case, we move the focus to the browser that loads the content 4036 // upon key up the enter key. 4037 // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid 4038 // focusing on the browser in the function. And also, set loadedContent 4039 // flag that whether the content is loaded in the current tab by this enter 4040 // key. _keyDownEnterDeferred promise is processed at key up the enter, 4041 // focus on the browser passed by _keyDownEnterDeferred.resolve(). 4042 params.avoidBrowserFocus = true; 4043 this._keyDownEnterDeferred.loadedContent = true; 4044 this._keyDownEnterDeferred.resolve(browser); 4045 } 4046 4047 // Ensure the window gets the `private` feature if the current window 4048 // is private, unless the caller explicitly requested not to. 4049 if (this.isPrivate && !("private" in params)) { 4050 params.private = true; 4051 } 4052 4053 // Focus the content area before triggering loads, since if the load 4054 // occurs in a new tab, we want focus to be restored to the content 4055 // area when the current tab is re-selected. 4056 if (!params.avoidBrowserFocus) { 4057 browser.focus(); 4058 // Make sure the domain name stays visible for spoof protection and usability. 4059 this.setSelectionRange(0, 0); 4060 } 4061 4062 if (openUILinkWhere != "current") { 4063 this.handleRevert(); 4064 } 4065 4066 // Notify about the start of navigation. 4067 this.#notifyStartNavigation(resultDetails); 4068 4069 try { 4070 this.window.openTrustedLinkIn(url, openUILinkWhere, params); 4071 } catch (ex) { 4072 // This load can throw an exception in certain cases, which means 4073 // we'll want to replace the URL with the loaded URL: 4074 if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) { 4075 this.handleRevert(); 4076 } 4077 } 4078 4079 // If we show the focus border after closing the view, it would appear to 4080 // flash since this._on_blur would remove it immediately after. 4081 this.view.close({ showFocusBorder: false }); 4082 } 4083 4084 /** 4085 * Determines where a URL/page should be opened. 4086 * 4087 * @param {Event} event the event triggering the opening. 4088 * @returns {"current" | "tabshifted" | "tab" | "save" | "window"} 4089 */ 4090 _whereToOpen(event) { 4091 let isKeyboardEvent = KeyboardEvent.isInstance(event); 4092 let reuseEmpty = isKeyboardEvent; 4093 let where = undefined; 4094 if ( 4095 isKeyboardEvent && 4096 (event.altKey || event.getModifierState("AltGraph")) 4097 ) { 4098 // We support using 'alt' to open in a tab, because ctrl/shift 4099 // might be used for canonizing URLs: 4100 where = event.shiftKey ? "tabshifted" : "tab"; 4101 } else if (this.#isCanonizeKeyboardEvent(event)) { 4102 // If we're allowing canonization, and this is a canonization key event, 4103 // open in current tab to avoid handling as new tab modifier. 4104 where = "current"; 4105 } else { 4106 where = lazy.BrowserUtils.whereToOpenLink(event, false, false); 4107 } 4108 if (lazy.UrlbarPrefs.get("openintab")) { 4109 if (where == "current") { 4110 where = "tab"; 4111 } else if (where == "tab") { 4112 where = "current"; 4113 } 4114 reuseEmpty = true; 4115 } 4116 if ( 4117 where == "tab" && 4118 reuseEmpty && 4119 this.window.gBrowser.selectedTab.isEmpty 4120 ) { 4121 where = "current"; 4122 } 4123 return where; 4124 } 4125 4126 _initCopyCutController() { 4127 // Clipboard handling is managed by the multiline editor in smartbar mode. 4128 if (this.#isSmartbarMode) { 4129 return; 4130 } 4131 if (this._copyCutController) { 4132 return; 4133 } 4134 this._copyCutController = new CopyCutController(this); 4135 this.inputField.controllers.insertControllerAt(0, this._copyCutController); 4136 } 4137 4138 /** 4139 * Searches the context menu for the location of a specific command. 4140 * 4141 * @param {string} menuItemCommand 4142 * The command to search for. 4143 * @returns {HTMLElement} 4144 * Html element that matches the command or 4145 * the last element if we could not find the command. 4146 */ 4147 #findMenuItemLocation(menuItemCommand) { 4148 let inputBox = this.querySelector("moz-input-box"); 4149 let contextMenu = inputBox.menupopup; 4150 let insertLocation = contextMenu.firstElementChild; 4151 // find the location of the command 4152 while ( 4153 insertLocation.nextElementSibling && 4154 insertLocation.getAttribute("cmd") != menuItemCommand 4155 ) { 4156 insertLocation = insertLocation.nextElementSibling; 4157 } 4158 4159 return insertLocation; 4160 } 4161 4162 /** 4163 * Strips known tracking query parameters/ link decorators. 4164 * 4165 * @returns {nsIURI} 4166 * The stripped URI or original URI, if nothing can be 4167 * stripped 4168 */ 4169 #stripURI() { 4170 let copyString = this._getSelectedValueForClipboard(); 4171 if (!copyString) { 4172 return null; 4173 } 4174 let strippedURI = null; 4175 4176 // Error check occurs during isClipboardURIValid 4177 let uri = Services.io.newURI(copyString); 4178 try { 4179 strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri); 4180 } catch (e) { 4181 console.warn(`stripForCopyOrShare: ${e.message}`); 4182 return uri; 4183 } 4184 4185 if (strippedURI) { 4186 return this.makeURIReadable(strippedURI); 4187 } 4188 return uri; 4189 } 4190 4191 /** 4192 * Checks if the clipboard contains a valid URI 4193 * 4194 * @returns {true|false} 4195 */ 4196 #isClipboardURIValid() { 4197 let copyString = this._getSelectedValueForClipboard(); 4198 if (!copyString) { 4199 return false; 4200 } 4201 4202 return URL.canParse(copyString); 4203 } 4204 4205 /** 4206 * Checks if there is a query parameter that can be stripped 4207 * 4208 * @returns {true|false} 4209 */ 4210 #canStrip() { 4211 let copyString = this._getSelectedValueForClipboard(); 4212 if (!copyString) { 4213 return false; 4214 } 4215 // throws if the selected string is not a valid URI 4216 try { 4217 let uri = Services.io.newURI(copyString); 4218 return lazy.QueryStringStripper.canStripForShare(uri); 4219 } catch (e) { 4220 console.warn("canStrip failed!", e); 4221 return false; 4222 } 4223 } 4224 4225 /** 4226 * Restores the untrimmed value in the urlbar. 4227 * 4228 * @param {object} [options] 4229 * Options for untrimming. 4230 * @param {boolean} [options.moveCursorToStart] 4231 * Whether the cursor should be moved at position 0 after untrimming. 4232 * @param {boolean} [options.ignoreSelection] 4233 * Whether this should untrim, regardless of the current selection state. 4234 */ 4235 #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) { 4236 // Check if we can untrim the current value. 4237 if ( 4238 !lazy.UrlbarPrefs.getScotchBonnetPref( 4239 "untrimOnUserInteraction.featureGate" 4240 ) || 4241 !this._protocolIsTrimmed || 4242 !this.focused || 4243 (!ignoreSelection && this.#allTextSelected) 4244 ) { 4245 return; 4246 } 4247 4248 let selectionStart = this.selectionStart; 4249 let selectionEnd = this.selectionEnd; 4250 4251 // Correct the selection taking the trimmed protocol into account. 4252 let offset = lazy.BrowserUIUtils.trimURLProtocol.length; 4253 4254 // In case of autofill, we may have to adjust its boundaries. 4255 if (this._autofillPlaceholder) { 4256 this._autofillPlaceholder.selectionStart += offset; 4257 this._autofillPlaceholder.selectionEnd += offset; 4258 } 4259 4260 if (moveCursorToStart) { 4261 this._setValue(this._untrimmedValue, { 4262 valueIsTyped: this.valueIsTyped, 4263 }); 4264 this.setSelectionRange(0, 0); 4265 return; 4266 } 4267 4268 if (selectionStart == selectionEnd) { 4269 // When cursor is at the end of the string, untrimming may 4270 // reintroduced a trailing slash and we want to move past it. 4271 if (selectionEnd == this.value.length) { 4272 offset += 1; 4273 } 4274 selectionStart = selectionEnd += offset; 4275 } else { 4276 // There's a selection, so we must calculate both the initial 4277 // protocol and the eventual trailing slash. 4278 if (selectionStart != 0) { 4279 selectionStart += offset; 4280 } else { 4281 // When selection starts at the beginning, the adjusted selection will 4282 // include the protocol only if the selected text includes the host. 4283 // The port is left out, as one may want to exclude it from the copy. 4284 let prePathMinusPort; 4285 try { 4286 let uri = Services.io.newURI(this._untrimmedValue); 4287 prePathMinusPort = [uri.userPass, uri.displayHost] 4288 .filter(Boolean) 4289 .join("@"); 4290 } catch (ex) { 4291 lazy.logger.error("Should only try to untrim valid URLs"); 4292 } 4293 if (!this.#selectedText.startsWith(prePathMinusPort)) { 4294 selectionStart += offset; 4295 } 4296 } 4297 if (selectionEnd == this.value.length) { 4298 offset += 1; 4299 } 4300 selectionEnd += offset; 4301 } 4302 4303 this._setValue(this._untrimmedValue, { 4304 valueIsTyped: this.valueIsTyped, 4305 }); 4306 4307 this.setSelectionRange(selectionStart, selectionEnd); 4308 } 4309 4310 // The strip-on-share feature will strip known tracking/decorational 4311 // query params from the URI and copy the stripped version to the clipboard. 4312 _initStripOnShare() { 4313 let contextMenu = this.querySelector("moz-input-box").menupopup; 4314 let insertLocation = this.#findMenuItemLocation("cmd_copy"); 4315 // set up the menu item 4316 let stripOnShare = this.document.createXULElement("menuitem"); 4317 this.document.l10n.setAttributes( 4318 stripOnShare, 4319 "text-action-copy-clean-link" 4320 ); 4321 stripOnShare.setAttribute("anonid", "strip-on-share"); 4322 stripOnShare.id = "strip-on-share"; 4323 4324 insertLocation.insertAdjacentElement("afterend", stripOnShare); 4325 4326 // Register listener that returns the stripped url or falls back 4327 // to the original url if nothing can be stripped. 4328 stripOnShare.addEventListener("command", () => { 4329 let strippedURI = this.#stripURI(); 4330 lazy.ClipboardHelper.copyString(strippedURI.displaySpec); 4331 }); 4332 4333 // Register a listener that hides the menu item if there is nothing to copy. 4334 contextMenu.addEventListener("popupshowing", () => { 4335 // feature is not enabled 4336 if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) { 4337 stripOnShare.setAttribute("hidden", true); 4338 return; 4339 } 4340 let controller = 4341 this.document.commandDispatcher.getControllerForCommand("cmd_copy"); 4342 if ( 4343 !controller.isCommandEnabled("cmd_copy") || 4344 !this.#isClipboardURIValid() 4345 ) { 4346 stripOnShare.setAttribute("hidden", true); 4347 return; 4348 } 4349 stripOnShare.removeAttribute("hidden"); 4350 if (!this.#canStrip()) { 4351 stripOnShare.setAttribute("disabled", true); 4352 return; 4353 } 4354 stripOnShare.removeAttribute("disabled"); 4355 }); 4356 } 4357 4358 _initPasteAndGo() { 4359 let inputBox = this.querySelector("moz-input-box"); 4360 let contextMenu = inputBox.menupopup; 4361 let insertLocation = this.#findMenuItemLocation("cmd_paste"); 4362 if (!insertLocation) { 4363 return; 4364 } 4365 4366 let pasteAndGo = this.document.createXULElement("menuitem"); 4367 pasteAndGo.id = "paste-and-go"; 4368 let label = Services.strings 4369 .createBundle("chrome://browser/locale/browser.properties") 4370 .GetStringFromName("pasteAndGo.label"); 4371 pasteAndGo.setAttribute("label", label); 4372 pasteAndGo.setAttribute("anonid", "paste-and-go"); 4373 pasteAndGo.addEventListener("command", () => { 4374 this._suppressStartQuery = true; 4375 4376 this.select(); 4377 this.window.goDoCommand("cmd_paste"); 4378 this.setResultForCurrentValue(null); 4379 this.handleCommand(); 4380 this.controller.clearLastQueryContextCache(); 4381 4382 this._suppressStartQuery = false; 4383 }); 4384 4385 contextMenu.addEventListener("popupshowing", () => { 4386 // Close the results pane when the input field contextual menu is open, 4387 // because paste and go doesn't want a result selection. 4388 this.view.close(); 4389 4390 let controller = 4391 this.document.commandDispatcher.getControllerForCommand("cmd_paste"); 4392 let enabled = controller.isCommandEnabled("cmd_paste"); 4393 if (enabled) { 4394 pasteAndGo.removeAttribute("disabled"); 4395 } else { 4396 pasteAndGo.setAttribute("disabled", "true"); 4397 } 4398 }); 4399 4400 insertLocation.insertAdjacentElement("afterend", pasteAndGo); 4401 } 4402 4403 /** 4404 * This notifies observers that the user has entered or selected something in 4405 * the URL bar which will cause navigation. 4406 * 4407 * We use the observer service, so that we don't need to load extra facilities 4408 * if they aren't being used, e.g. WebNavigation. 4409 * 4410 * @param {UrlbarResult} result 4411 * Details of the result that was selected, if any. 4412 */ 4413 #notifyStartNavigation(result) { 4414 if (this.#isAddressbar) { 4415 Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation"); 4416 } 4417 } 4418 4419 /** 4420 * Returns a search mode object if a result should enter search mode when 4421 * selected. 4422 * 4423 * @param {UrlbarResult} result 4424 * The result to check. 4425 * @param {string} [entry] 4426 * If provided, this will be recorded as the entry point into search mode. 4427 * See setSearchMode() documentation for details. 4428 * @returns {object} A search mode object. Null if search mode should not be 4429 * entered. See setSearchMode documentation for details. 4430 */ 4431 _searchModeForResult(result, entry = null) { 4432 // Search mode is determined by the result's keyword or engine. 4433 if (!result.payload.keyword && !result.payload.engine) { 4434 return null; 4435 } 4436 4437 let searchMode = this.searchModeForToken(result.payload.keyword); 4438 // If result.originalEngine is set, then the user is Alt+Tabbing 4439 // through the one-offs, so the keyword doesn't match the engine. 4440 if ( 4441 !searchMode && 4442 result.payload.engine && 4443 (!result.payload.originalEngine || 4444 result.payload.engine == result.payload.originalEngine) 4445 ) { 4446 searchMode = { engineName: result.payload.engine }; 4447 } 4448 4449 if (searchMode) { 4450 if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) { 4451 searchMode.restrictType = "keyword"; 4452 } else if ( 4453 lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword) 4454 ) { 4455 searchMode.restrictType = "symbol"; 4456 } 4457 if (entry) { 4458 searchMode.entry = entry; 4459 } else { 4460 switch (result.providerName) { 4461 case "UrlbarProviderTopSites": 4462 searchMode.entry = "topsites_urlbar"; 4463 break; 4464 case "UrlbarProviderTabToSearch": 4465 if (result.payload.dynamicType) { 4466 searchMode.entry = "tabtosearch_onboard"; 4467 } else { 4468 searchMode.entry = "tabtosearch"; 4469 } 4470 break; 4471 default: 4472 searchMode.entry = "keywordoffer"; 4473 break; 4474 } 4475 } 4476 } 4477 4478 return searchMode; 4479 } 4480 4481 /** 4482 * Updates the UI so that search mode is either entered or exited. 4483 * 4484 * @param {object} searchMode 4485 * See setSearchMode documentation. If null, then search mode is exited. 4486 */ 4487 _updateSearchModeUI(searchMode) { 4488 let { engineName, source, isGeneralPurposeEngine } = searchMode || {}; 4489 4490 // As an optimization, bail if the given search mode is null but search mode 4491 // is already inactive. Otherwise, browser_preferences_usage.js fails due to 4492 // accessing the browser.urlbar.placeholderName pref (via the call to 4493 // initPlaceHolder below) too many times. That test does not enter search mode, 4494 // but it triggers many calls to this method with a null search mode, via setURI. 4495 if (!engineName && !source && !this.hasAttribute("searchmode")) { 4496 return; 4497 } 4498 4499 if (this._searchModeIndicatorTitle) { 4500 this._searchModeIndicatorTitle.textContent = ""; 4501 this._searchModeIndicatorTitle.removeAttribute("data-l10n-id"); 4502 } 4503 4504 if (!engineName && !source) { 4505 this.removeAttribute("searchmode"); 4506 this.initPlaceHolder(true); 4507 return; 4508 } 4509 4510 if (this.#isAddressbar) { 4511 if (engineName) { 4512 // Set text content for the search mode indicator. 4513 this._searchModeIndicatorTitle.textContent = engineName; 4514 this.document.l10n.setAttributes( 4515 this.inputField, 4516 isGeneralPurposeEngine 4517 ? "urlbar-placeholder-search-mode-web-2" 4518 : "urlbar-placeholder-search-mode-other-engine", 4519 { name: engineName } 4520 ); 4521 } else if (source) { 4522 const messageIDs = { 4523 actions: "urlbar-placeholder-search-mode-other-actions", 4524 bookmarks: "urlbar-placeholder-search-mode-other-bookmarks", 4525 engine: "urlbar-placeholder-search-mode-other-engine", 4526 history: "urlbar-placeholder-search-mode-other-history", 4527 tabs: "urlbar-placeholder-search-mode-other-tabs", 4528 }; 4529 let sourceName = lazy.UrlbarUtils.getResultSourceName(source); 4530 let l10nID = `urlbar-search-mode-${sourceName}`; 4531 this.document.l10n.setAttributes( 4532 this._searchModeIndicatorTitle, 4533 l10nID 4534 ); 4535 this.document.l10n.setAttributes( 4536 this.inputField, 4537 messageIDs[sourceName] 4538 ); 4539 } 4540 } 4541 4542 this.toggleAttribute("searchmode", true); 4543 // Clear autofill. 4544 if (this._autofillPlaceholder && this.userTypedValue) { 4545 this.value = this.userTypedValue; 4546 } 4547 // Search mode should only be active when pageproxystate is invalid. 4548 if (this.getAttribute("pageproxystate") == "valid") { 4549 this.value = ""; 4550 this.setPageProxyState("invalid", true); 4551 } 4552 4553 this.searchModeSwitcher?.onSearchModeChanged(); 4554 } 4555 4556 /** 4557 * Handles persisted search terms logic for the current browser. This manages 4558 * state and updates the UI accordingly. 4559 * 4560 * @param {object} options 4561 * @param {object} options.state 4562 * The state object for the currently viewed browser. 4563 * @param {boolean} options.hideSearchTerms 4564 * True if we must hide the search terms and instead show the page URL. 4565 * @param {boolean} options.dueToTabSwitch 4566 * True if the browser was revealed again due to a tab switch. 4567 * @param {boolean} options.isSameDocument 4568 * True if the page load was same document. 4569 * @param {nsIURI} [options.uri] 4570 * The latest URI of the page. 4571 * @returns {boolean} 4572 * Whether search terms should persist. 4573 */ 4574 #handlePersistedSearchTerms({ 4575 state, 4576 hideSearchTerms, 4577 dueToTabSwitch, 4578 isSameDocument, 4579 uri, 4580 }) { 4581 if (!this.#isAddressbar) { 4582 return false; 4583 } 4584 if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) { 4585 if (state.persist) { 4586 this.removeAttribute("persistsearchterms"); 4587 delete state.persist; 4588 } 4589 return false; 4590 } 4591 4592 // The first time the browser URI has been loaded to the input. If 4593 // persist is not defined, it is likely due to the tab being created in 4594 // the background or an existing tab moved to a new window and we have to 4595 // do the work for the first time. 4596 let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist; 4597 4598 let cachedUriDidChange = 4599 state.persist?.originalURI && 4600 (!this.window.gBrowser.selectedBrowser.originalURI || 4601 !state.persist.originalURI.equals( 4602 this.window.gBrowser.selectedBrowser.originalURI 4603 )); 4604 4605 // Capture the shouldPersist property if it exists before 4606 // setPersistenceState potentially modifies it. 4607 let wasPersisting = state.persist?.shouldPersist ?? false; 4608 4609 if (firstView || cachedUriDidChange) { 4610 lazy.UrlbarSearchTermsPersistence.setPersistenceState( 4611 state, 4612 this.window.gBrowser.selectedBrowser.originalURI 4613 ); 4614 } 4615 let shouldPersist = 4616 !hideSearchTerms && 4617 lazy.UrlbarSearchTermsPersistence.shouldPersist(state, { 4618 dueToTabSwitch, 4619 isSameDocument, 4620 uri: uri ?? this.window.gBrowser.currentURI, 4621 userTypedValue: this.userTypedValue, 4622 firstView, 4623 }); 4624 // When persisting, userTypedValue should have a value consistent with the 4625 // search terms to mimic a user typing the search terms. 4626 // When turning off persist, check if the userTypedValue needs to be 4627 // removed in order for the URL to return to the address bar. Single page 4628 // application SERPs will load secondary search pages (e.g. Maps, Images) 4629 // with the same document, which won't unset userTypedValue. 4630 if (shouldPersist) { 4631 this.userTypedValue = state.persist.searchTerms; 4632 } else if (wasPersisting && !shouldPersist) { 4633 this.userTypedValue = null; 4634 } 4635 4636 state.persist.shouldPersist = shouldPersist; 4637 this.toggleAttribute("persistsearchterms", state.persist.shouldPersist); 4638 4639 if (state.persist.shouldPersist && !isSameDocument) { 4640 Glean.urlbarPersistedsearchterms.viewCount.add(1); 4641 } 4642 4643 return shouldPersist; 4644 } 4645 4646 /** 4647 * Initializes the urlbar placeholder to the pre-saved engine name. We do this 4648 * via a preference, to avoid needing to synchronously init the search service. 4649 * 4650 * This should be called around the time of DOMContentLoaded, so that it is 4651 * initialized quickly before the user sees anything. 4652 * 4653 * Note: If the preference doesn't exist, we don't do anything as the default 4654 * placeholder is a string which doesn't have the engine name; however, this 4655 * can be overridden using the `force` parameter. 4656 * 4657 * @param {boolean} force If true and the preference doesn't exist, the 4658 * placeholder will be set to the default version 4659 * without an engine name ("Search or enter address"). 4660 */ 4661 initPlaceHolder(force = false) { 4662 if (!this.#isAddressbar) { 4663 return; 4664 } 4665 4666 let prefName = 4667 "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : ""); 4668 let engineName = Services.prefs.getStringPref(prefName, ""); 4669 if (engineName || force) { 4670 // We can do this directly, since we know we're at DOMContentLoaded. 4671 this._setPlaceholder(engineName || null); 4672 } 4673 } 4674 4675 /** 4676 * Asynchronously changes the urlbar placeholder to the name of the default 4677 * engine according to the search service when it is initialized. 4678 * 4679 * This should be called around the time of MozAfterPaint. Since the 4680 * placeholder was already initialized to the pre-saved engine name by 4681 * initPlaceHolder when this is called, the update is delayed to avoid 4682 * confusing the user. 4683 */ 4684 async delayedStartupInit() { 4685 // Only delay if requested, and we're not displaying text in the URL bar 4686 // currently. 4687 if (!this.value) { 4688 // Delays changing the URL Bar placeholder and Unified Search Button icon 4689 // until the user is not going to be seeing it, e.g. when there is a value 4690 // entered in the bar, or if there is a tab switch to a tab which has a url 4691 // loaded. We delay the update until the user is out of search mode since 4692 // an alternative placeholder is used in search mode. 4693 let updateListener = () => { 4694 if (this.value && !this.searchMode) { 4695 // By the time the user has switched, they may have changed the engine 4696 // again, so we need to call this function again but with the 4697 // new engine name. 4698 // No need to await for this to finish, we're in a listener here anyway. 4699 this.searchModeSwitcher.updateSearchIcon(); 4700 this._updatePlaceholderFromDefaultEngine(); 4701 this.inputField.removeEventListener("input", updateListener); 4702 this.window.gBrowser.tabContainer.removeEventListener( 4703 "TabSelect", 4704 updateListener 4705 ); 4706 } 4707 }; 4708 4709 this.inputField.addEventListener("input", updateListener); 4710 this.window.gBrowser.tabContainer.addEventListener( 4711 "TabSelect", 4712 updateListener 4713 ); 4714 } else { 4715 await this._updatePlaceholderFromDefaultEngine(); 4716 } 4717 4718 // If we haven't finished initializing, ensure the placeholder 4719 // preference is set for the next startup. 4720 if (this.#isAddressbar) { 4721 lazy.SearchUIUtils.updatePlaceholderNamePreference( 4722 await this._getDefaultSearchEngine(), 4723 this.isPrivate 4724 ); 4725 } 4726 } 4727 4728 /** 4729 * Set Unified Search Button availability. 4730 * 4731 * @param {boolean} available If true Unified Search Button will be available. 4732 */ 4733 setUnifiedSearchButtonAvailability(available) { 4734 if (this.#isSmartbarMode) { 4735 return; 4736 } 4737 this.toggleAttribute("unifiedsearchbutton-available", available); 4738 this.getBrowserState( 4739 this.window.gBrowser.selectedBrowser 4740 ).isUnifiedSearchButtonAvailable = available; 4741 } 4742 4743 /** 4744 * Returns a Promise that resolves with default search engine. 4745 * 4746 * @returns {Promise<nsISearchEngine>} 4747 */ 4748 _getDefaultSearchEngine() { 4749 return this.isPrivate 4750 ? Services.search.getDefaultPrivate() 4751 : Services.search.getDefault(); 4752 } 4753 4754 /** 4755 * This is a wrapper around '_updatePlaceholder' that uses the appropriate 4756 * default engine to get the engine name. 4757 */ 4758 async _updatePlaceholderFromDefaultEngine() { 4759 const defaultEngine = await this._getDefaultSearchEngine(); 4760 this._updatePlaceholder(defaultEngine.name); 4761 } 4762 4763 /** 4764 * Updates the URLBar placeholder for the specified engine, delaying the 4765 * update if required. 4766 * 4767 * Note: The engine name will only be displayed for application-provided 4768 * engines, as we know they should have short names. 4769 * 4770 * @param {string} engineName The search engine name to use for the update. 4771 */ 4772 _updatePlaceholder(engineName) { 4773 if (!engineName) { 4774 throw new Error("Expected an engineName to be specified"); 4775 } 4776 4777 if (this.searchMode || !this.#isAddressbar) { 4778 return; 4779 } 4780 4781 let engine = Services.search.getEngineByName(engineName); 4782 if (engine.isConfigEngine) { 4783 this._setPlaceholder(engineName); 4784 } else { 4785 // Display the default placeholder string. 4786 this._setPlaceholder(null); 4787 } 4788 } 4789 4790 /** 4791 * Sets the URLBar placeholder to either something based on the engine name, 4792 * or the default placeholder. 4793 * 4794 * @param {?string} engineName 4795 * The name of the engine or null to use the default placeholder. 4796 */ 4797 _setPlaceholder(engineName) { 4798 if (this.#isSmartbarMode) { 4799 this.document.l10n.setAttributes(this.inputField, "smartbar-placeholder"); 4800 return; 4801 } 4802 4803 if (!this.#isAddressbar) { 4804 this.document.l10n.setAttributes(this.inputField, "searchbar-input"); 4805 return; 4806 } 4807 4808 let l10nId; 4809 if (lazy.UrlbarPrefs.get("keyword.enabled")) { 4810 l10nId = engineName 4811 ? "urlbar-placeholder-with-name" 4812 : "urlbar-placeholder"; 4813 } else { 4814 l10nId = "urlbar-placeholder-keyword-disabled"; 4815 } 4816 4817 this.document.l10n.setAttributes( 4818 this.inputField, 4819 l10nId, 4820 l10nId == "urlbar-placeholder-with-name" 4821 ? { name: engineName } 4822 : undefined 4823 ); 4824 } 4825 4826 /** 4827 * Determines if we should select all the text in the Urlbar based on the 4828 * Urlbar state, and whether the selection is empty. 4829 */ 4830 #maybeSelectAll() { 4831 if ( 4832 !this._preventClickSelectsAll && 4833 this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING && 4834 this.focused && 4835 this.selectionStart == this.selectionEnd 4836 ) { 4837 this.select(); 4838 } 4839 } 4840 4841 // Event handlers below. 4842 4843 _on_command(event) { 4844 // Something is executing a command, likely causing a focus change. This 4845 // should not be recorded as an abandonment. If the user is selecting a 4846 // result menu item or entering search mode from a one-off, then they are 4847 // in the same engagement and we should not discard. 4848 if ( 4849 !event.target.classList.contains("urlbarView-result-menuitem") && 4850 (!event.target.classList.contains("searchbar-engine-one-off-item") || 4851 this.searchMode?.entry != "oneoff") 4852 ) { 4853 this.controller.engagementEvent.discard(); 4854 } 4855 } 4856 4857 _on_blur(event) { 4858 lazy.logger.debug("Blur Event"); 4859 // We cannot count every blur events after a missed engagement as abandoment 4860 // because the user may have clicked on some view element that executes 4861 // a command causing a focus change. For example opening preferences from 4862 // the oneoff settings button. 4863 // For now we detect that case by discarding the event on command, but we 4864 // may want to figure out a more robust way to detect abandonment. 4865 this.controller.engagementEvent.record(event, { 4866 searchString: this._lastSearchString, 4867 searchSource: this.getSearchSource(event), 4868 }); 4869 4870 this.focusedViaMousedown = false; 4871 this._handoffSession = undefined; 4872 this._isHandoffSession = false; 4873 this.removeAttribute("focused"); 4874 4875 if (this._autofillPlaceholder && this.userTypedValue) { 4876 // If we were autofilling, remove the autofilled portion, by restoring 4877 // the value to the last typed one. 4878 this.value = this.userTypedValue; 4879 } else if ( 4880 this.value == this._untrimmedValue && 4881 !this.userTypedValue && 4882 !this.focused 4883 ) { 4884 // If the value was untrimmed by _on_focus and didn't change, trim it. 4885 this.value = this._untrimmedValue; 4886 } else { 4887 // We're not updating the value, so just format it. 4888 this.formatValue(); 4889 } 4890 4891 this._resetSearchState(); 4892 4893 // In certain cases, like holding an override key and confirming an entry, 4894 // we don't key a keyup event for the override key, thus we make this 4895 // additional cleanup on blur. 4896 this._clearActionOverride(); 4897 4898 // The extension input sessions depends more on blur than on the fact we 4899 // actually cancel a running query, so we do it here. 4900 if (lazy.ExtensionSearchHandler.hasActiveInputSession()) { 4901 lazy.ExtensionSearchHandler.handleInputCancelled(); 4902 } 4903 4904 // Respect the autohide preference for easier inspecting/debugging via 4905 // the browser toolbox. 4906 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 4907 this.view.close(); 4908 } 4909 4910 // We may have hidden popup notifications, show them again if necessary. 4911 if ( 4912 this.getAttribute("pageproxystate") != "valid" && 4913 this.window.UpdatePopupNotificationsVisibility 4914 ) { 4915 this.window.UpdatePopupNotificationsVisibility(); 4916 } 4917 4918 // If user move the focus to another component while pressing Enter key, 4919 // then keyup at that component, as we can't get the event, clear the promise. 4920 if (this._keyDownEnterDeferred) { 4921 this._keyDownEnterDeferred.resolve(); 4922 this._keyDownEnterDeferred = null; 4923 } 4924 this._isKeyDownWithCtrl = false; 4925 this._isKeyDownWithMeta = false; 4926 this._isKeyDownWithMetaAndLeft = false; 4927 4928 Services.obs.notifyObservers(null, "urlbar-blur"); 4929 } 4930 4931 _on_click(event) { 4932 switch (event.target) { 4933 case this.inputField: 4934 case this._inputContainer: 4935 this.#maybeSelectAll(); 4936 this.#maybeUntrimUrl(); 4937 break; 4938 4939 case this._searchModeIndicatorClose: 4940 if (event.button != 2) { 4941 this.searchMode = null; 4942 if (this.view.oneOffSearchButtons) { 4943 this.view.oneOffSearchButtons.selectedButton = null; 4944 } 4945 if (this.view.isOpen) { 4946 this.startQuery({ 4947 event, 4948 }); 4949 } 4950 } 4951 break; 4952 4953 case this._revertButton: 4954 this.handleRevert(); 4955 this.select(); 4956 break; 4957 4958 case this.goButton: 4959 this.handleCommand(event); 4960 break; 4961 } 4962 } 4963 4964 _on_contextmenu(event) { 4965 this.#lazy.addSearchEngineHelper.refreshContextMenu(event); 4966 4967 // Context menu opened via keyboard shortcut. 4968 if (!event.button) { 4969 return; 4970 } 4971 4972 this.#maybeSelectAll(); 4973 } 4974 4975 _on_focus(event) { 4976 lazy.logger.debug("Focus Event"); 4977 if (!this._hideFocus) { 4978 this.toggleAttribute("focused", true); 4979 } 4980 4981 // If the value was trimmed, check whether we should untrim it. 4982 // This is necessary when a protocol was typed, but the whole url has 4983 // invalid parts, like the origin, then editing and confirming the trimmed 4984 // value would execute a search instead of visiting the typed url. 4985 if (this._protocolIsTrimmed) { 4986 let untrim = false; 4987 let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI; 4988 if (fixedURI) { 4989 try { 4990 let expectedURI = Services.io.newURI(this._untrimmedValue); 4991 if ( 4992 lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") && 4993 this._untrimmedValue.startsWith("https://") 4994 ) { 4995 untrim = 4996 fixedURI.displaySpec.replace("http://", "https://") != 4997 expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI. 4998 } else { 4999 untrim = fixedURI.displaySpec != expectedURI.displaySpec; 5000 } 5001 } catch (ex) { 5002 untrim = true; 5003 } 5004 } 5005 if (untrim) { 5006 this._setValue(this._untrimmedValue); 5007 } 5008 } 5009 5010 if (this.focusedViaMousedown) { 5011 this.view.autoOpen({ event }); 5012 } else { 5013 if (this._untrimOnFocusAfterKeydown) { 5014 // While the mousedown focus has more complex implications due to drag 5015 // and double-click select, we can untrim immediately when the urlbar is 5016 // focused by a keyboard shortcut. 5017 this.#maybeUntrimUrl({ ignoreSelection: true }); 5018 } 5019 5020 if (this.inputField.hasAttribute("refocused-by-panel")) { 5021 this.#maybeSelectAll(); 5022 } 5023 } 5024 5025 this._updateUrlTooltip(); 5026 this.formatValue(); 5027 5028 // Hide popup notifications, to reduce visual noise. 5029 if ( 5030 this.getAttribute("pageproxystate") != "valid" && 5031 this.window.UpdatePopupNotificationsVisibility 5032 ) { 5033 this.window.UpdatePopupNotificationsVisibility(); 5034 } 5035 5036 Services.obs.notifyObservers(null, "urlbar-focus"); 5037 } 5038 5039 _on_mouseover() { 5040 this._updateUrlTooltip(); 5041 } 5042 5043 _on_draggableregionleftmousedown() { 5044 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 5045 this.view.close(); 5046 } 5047 } 5048 5049 _on_mousedown(event) { 5050 switch (event.currentTarget) { 5051 case this: { 5052 this._mousedownOnUrlbarDescendant = true; 5053 if ( 5054 event.composedTarget != this.inputField && 5055 event.composedTarget != this._inputContainer 5056 ) { 5057 break; 5058 } 5059 5060 this.focusedViaMousedown = !this.focused; 5061 this._preventClickSelectsAll = this.focused; 5062 5063 // Keep the focus status, since the attribute may be changed 5064 // upon calling this.focus(). 5065 const hasFocus = this.hasAttribute("focused"); 5066 if (event.composedTarget != this.inputField) { 5067 this.focus(); 5068 } 5069 5070 // The rest of this case only cares about left clicks. 5071 if (event.button != 0) { 5072 break; 5073 } 5074 5075 // Clear any previous selection unless we are focused, to ensure it 5076 // doesn't affect drag selection. 5077 if (this.focusedViaMousedown) { 5078 this.setSelectionRange(0, 0); 5079 } 5080 5081 // Do not suppress the focus border if we are already focused. If we 5082 // did, we'd hide the focus border briefly then show it again if the 5083 // user has Top Sites disabled, creating a flashing effect. 5084 this.view.autoOpen({ 5085 event, 5086 suppressFocusBorder: !hasFocus, 5087 }); 5088 break; 5089 } 5090 case this.window: 5091 if (this._mousedownOnUrlbarDescendant) { 5092 this._mousedownOnUrlbarDescendant = false; 5093 break; 5094 } 5095 // Don't close the view when clicking on a tab; we may want to keep the 5096 // view open on tab switch, and the TabSelect event arrived earlier. 5097 if (event.target.closest("tab")) { 5098 break; 5099 } 5100 5101 // Close the view when clicking on toolbars and other UI pieces that 5102 // might not automatically remove focus from the input. 5103 // Respect the autohide preference for easier inspecting/debugging via 5104 // the browser toolbox. 5105 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 5106 if (this.view.isOpen && !this.hasAttribute("focused")) { 5107 // In this case, as blur event never happen from the inputField, we 5108 // record abandonment event explicitly. 5109 let blurEvent = new FocusEvent("blur", { 5110 relatedTarget: this.inputField, 5111 }); 5112 this.controller.engagementEvent.record(blurEvent, { 5113 searchString: this._lastSearchString, 5114 searchSource: this.getSearchSource(blurEvent), 5115 }); 5116 } 5117 5118 this.view.close(); 5119 } 5120 break; 5121 } 5122 } 5123 5124 _on_input(event) { 5125 if ( 5126 this._autofillPlaceholder && 5127 this.value === this.userTypedValue && 5128 (event.inputType === "deleteContentBackward" || 5129 event.inputType === "deleteContentForward") 5130 ) { 5131 // Take a telemetry if user deleted whole autofilled value. 5132 Glean.urlbar.autofillDeletion.add(1); 5133 } 5134 5135 let value = this.value; 5136 this.valueIsTyped = true; 5137 this._untrimmedValue = value; 5138 this._protocolIsTrimmed = false; 5139 this._resultForCurrentValue = null; 5140 5141 this.userTypedValue = value; 5142 // Unset userSelectionBehavior because the user is modifying the search 5143 // string, thus there's no valid selection. This is also used by the view 5144 // to set "aria-activedescendant", thus it should never get stale. 5145 this.controller.userSelectionBehavior = "none"; 5146 5147 let compositionState = this.#compositionState; 5148 let compositionClosedPopup = this.#compositionClosedPopup; 5149 5150 // Clear composition values if we're no more composing. 5151 if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 5152 this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; 5153 this.#compositionClosedPopup = false; 5154 } 5155 5156 this.toggleAttribute("usertyping", value); 5157 this.removeAttribute("actiontype"); 5158 5159 if ( 5160 this.getAttribute("pageproxystate") == "valid" && 5161 this.value != this._lastValidURLStr 5162 ) { 5163 this.setPageProxyState("invalid", true); 5164 } 5165 5166 if (this.#isAddressbar) { 5167 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 5168 if ( 5169 state.persist?.shouldPersist && 5170 this.value !== state.persist.searchTerms 5171 ) { 5172 state.persist.shouldPersist = false; 5173 this.removeAttribute("persistsearchterms"); 5174 } 5175 } 5176 5177 if (this.view.isOpen) { 5178 if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) { 5179 // SmartbarView rolls up all popups when it opens, but we should 5180 // do the same for SmartbarInput when it's already open in case 5181 // a tab preview was opened 5182 this.window.docShell.treeOwner 5183 .QueryInterface(Ci.nsIInterfaceRequestor) 5184 .getInterface(Ci.nsIAppWindow) 5185 .rollupAllPopups(); 5186 } 5187 if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) { 5188 this.view.clear(); 5189 if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) { 5190 this.view.close(); 5191 return; 5192 } 5193 } 5194 } else { 5195 this.view.clear(); 5196 } 5197 5198 this.view.removeAccessibleFocus(); 5199 5200 // During composition with an IME, the following events happen in order: 5201 // 1. a compositionstart event 5202 // 2. some input events 5203 // 3. a compositionend event 5204 // 4. an input event 5205 5206 // We should do nothing during composition or if composition was canceled 5207 // and we didn't close the popup on composition start. 5208 if ( 5209 !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") && 5210 (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING || 5211 (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED && 5212 !compositionClosedPopup)) 5213 ) { 5214 return; 5215 } 5216 5217 // Autofill only when text is inserted (i.e., event.data is not empty) and 5218 // it's not due to pasting. 5219 const allowAutofill = 5220 (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") || 5221 compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) && 5222 !!event.data && 5223 !lazy.UrlbarUtils.isPasteEvent(event) && 5224 this._maybeAutofillPlaceholder(value); 5225 5226 this.startQuery({ 5227 searchString: value, 5228 allowAutofill, 5229 resetSearchState: false, 5230 event, 5231 }); 5232 } 5233 5234 _on_selectionchange() { 5235 // Confirm placeholder as user text if it gets explicitly deselected. This 5236 // happens when the user wants to modify the autofilled text by either 5237 // clicking on it, or pressing HOME, END, RIGHT, … 5238 if ( 5239 this._autofillPlaceholder && 5240 this._autofillPlaceholder.value == this.value && 5241 (this._autofillPlaceholder.selectionStart != this.selectionStart || 5242 this._autofillPlaceholder.selectionEnd != this.selectionEnd) 5243 ) { 5244 this._autofillPlaceholder = null; 5245 this.userTypedValue = this.value; 5246 } 5247 } 5248 5249 _on_select() { 5250 // On certain user input, AutoCopyListener::OnSelectionChange() updates 5251 // the primary selection with user-selected text (when supported). 5252 // Selection::NotifySelectionListeners() then dispatches a "select" event 5253 // under similar conditions via TextInputListener::OnSelectionChange(). 5254 // This event is received here in order to replace the primary selection 5255 // from the editor with text having the adjustments of 5256 // _getSelectedValueForClipboard(), such as adding the scheme for the url. 5257 // 5258 // Other "select" events are also received, however, and must be excluded. 5259 if ( 5260 // _suppressPrimaryAdjustment is set during select(). Don't update 5261 // the primary selection because that is not the intent of user input, 5262 // which may be new tab or urlbar focus. 5263 this._suppressPrimaryAdjustment || 5264 // The check on isHandlingUserInput filters out async "select" events 5265 // from setSelectionRange(), which occur when autofill text is selected. 5266 !this.window.windowUtils.isHandlingUserInput || 5267 !Services.clipboard.isClipboardTypeSupported( 5268 Services.clipboard.kSelectionClipboard 5269 ) 5270 ) { 5271 return; 5272 } 5273 5274 let val = this._getSelectedValueForClipboard(); 5275 if (!val) { 5276 return; 5277 } 5278 5279 lazy.ClipboardHelper.copyStringToClipboard( 5280 val, 5281 Services.clipboard.kSelectionClipboard 5282 ); 5283 } 5284 5285 _on_overflow(event) { 5286 const targetIsPlaceholder = 5287 event.originalTarget.implementedPseudoElement == "::placeholder"; 5288 // We only care about the non-placeholder text. 5289 // This shouldn't be needed, see bug 1487036. 5290 if (targetIsPlaceholder) { 5291 return; 5292 } 5293 this._overflowing = true; 5294 this.updateTextOverflow(); 5295 } 5296 5297 _on_underflow(event) { 5298 const targetIsPlaceholder = 5299 event.originalTarget.implementedPseudoElement == "::placeholder"; 5300 // We only care about the non-placeholder text. 5301 // This shouldn't be needed, see bug 1487036. 5302 if (targetIsPlaceholder) { 5303 return; 5304 } 5305 this._overflowing = false; 5306 5307 this.updateTextOverflow(); 5308 5309 this._updateUrlTooltip(); 5310 } 5311 5312 _on_paste(event) { 5313 let originalPasteData = event.clipboardData.getData("text/plain"); 5314 if (!originalPasteData) { 5315 return; 5316 } 5317 5318 let oldValue = this.value; 5319 let oldStart = oldValue.substring(0, this.selectionStart); 5320 // If there is already non-whitespace content in the URL bar 5321 // preceding the pasted content, it's not necessary to check 5322 // protocols used by the pasted content: 5323 if (oldStart.trim()) { 5324 return; 5325 } 5326 let oldEnd = oldValue.substring(this.selectionEnd); 5327 5328 const pasteData = this.sanitizeTextFromClipboard(originalPasteData); 5329 5330 if (originalPasteData != pasteData) { 5331 // Unfortunately we're not allowed to set the bits being pasted 5332 // so cancel this event: 5333 event.preventDefault(); 5334 event.stopImmediatePropagation(); 5335 5336 const value = oldStart + pasteData + oldEnd; 5337 this._setValue(value, { valueIsTyped: true }); 5338 this.userTypedValue = value; 5339 5340 // Since we prevent the default paste event, we have to ensure the 5341 // pageproxystate is updated. The paste event replaces the actual current 5342 // page's URL with user-typed content, so we should set pageproxystate to 5343 // invalid. 5344 if (this.getAttribute("pageproxystate") == "valid") { 5345 this.setPageProxyState("invalid"); 5346 } 5347 this.toggleAttribute("usertyping", this._untrimmedValue); 5348 5349 // Fix up cursor/selection: 5350 let newCursorPos = oldStart.length + pasteData.length; 5351 this.setSelectionRange(newCursorPos, newCursorPos); 5352 5353 this.startQuery({ 5354 searchString: this.value, 5355 allowAutofill: false, 5356 resetSearchState: false, 5357 event, 5358 }); 5359 } 5360 } 5361 5362 /** 5363 * Sanitize and process data retrieved from the clipboard 5364 * 5365 * @param {string} clipboardData 5366 * The original data retrieved from the clipboard. 5367 * @returns {string} 5368 * The sanitized paste data, ready to use. 5369 */ 5370 sanitizeTextFromClipboard(clipboardData) { 5371 let fixedURI, keywordAsSent; 5372 try { 5373 ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo( 5374 clipboardData, 5375 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 5376 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP 5377 )); 5378 } catch (e) {} 5379 5380 let pasteData; 5381 if (keywordAsSent) { 5382 // For performance reasons, we don't want to beautify a long string. 5383 if (clipboardData.length < 500) { 5384 // For only keywords, replace any white spaces including line break 5385 // with white space. 5386 pasteData = clipboardData.replace(/\s/g, " "); 5387 } else { 5388 pasteData = clipboardData; 5389 } 5390 } else if ( 5391 fixedURI?.scheme == "data" && 5392 !fixedURI.spec.match(/^data:.+;base64,/) 5393 ) { 5394 // For data url without base64, replace line break with white space. 5395 pasteData = clipboardData.replace(/[\r\n]/g, " "); 5396 } else { 5397 // For normal url or data url having basic64, or if fixup failed, just 5398 // remove line breaks. 5399 pasteData = clipboardData.replace(/[\r\n]/g, ""); 5400 } 5401 5402 return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData); 5403 } 5404 5405 /** 5406 * Generate a UrlbarQueryContext from the current context. 5407 * 5408 * @param {object} [options] 5409 * Optional params 5410 * @param {boolean} [options.allowAutofill] 5411 * Whether autofill is enabled. 5412 * @param {string} [options.searchString] 5413 * The string being searched. 5414 * @param {object} [options.event] 5415 * The event triggering the query. 5416 * @returns {UrlbarQueryContext} 5417 * The queryContext object. 5418 */ 5419 #makeQueryContext({ 5420 allowAutofill = true, 5421 searchString = null, 5422 event = null, 5423 } = {}) { 5424 // When we are in actions search mode we can show more results so 5425 // increase the limit. 5426 let maxResults = 5427 this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS 5428 ? lazy.UrlbarPrefs.get("maxRichResults") 5429 : UNLIMITED_MAX_RESULTS; 5430 let options = { 5431 allowAutofill, 5432 isPrivate: this.isPrivate, 5433 sapName: this.sapName, 5434 maxResults, 5435 searchString, 5436 prohibitRemoteResults: 5437 event && 5438 lazy.UrlbarUtils.isPasteEvent(event) && 5439 lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") < 5440 event.data?.length, 5441 }; 5442 5443 // Only add gBrowser-dependent properties if gBrowser exists. 5444 if (this.window.gBrowser) { 5445 options.userContextId = parseInt( 5446 this.window.gBrowser.selectedBrowser?.getAttribute("usercontextid") ?? 0 5447 ); 5448 options.tabGroup = this.window.gBrowser.selectedTab.group?.id ?? null; 5449 options.currentPage = this.window.gBrowser.currentURI?.spec ?? ""; 5450 } 5451 5452 if (this.searchMode) { 5453 options.searchMode = this.searchMode; 5454 if (this.searchMode.source) { 5455 options.sources = [this.searchMode.source]; 5456 } 5457 } 5458 5459 return new lazy.UrlbarQueryContext(options); 5460 } 5461 5462 _on_scrollend() { 5463 this.updateTextOverflow(); 5464 } 5465 5466 _on_TabSelect() { 5467 // TabSelect may be activated by a keyboard shortcut and cause the urlbar 5468 // to take focus, in this case we should not untrim. 5469 this._untrimOnFocusAfterKeydown = false; 5470 this._gotTabSelect = true; 5471 this._afterTabSelectAndFocusChange(); 5472 } 5473 5474 _on_TabClose(event) { 5475 this.controller.engagementEvent.handleBounceEventTrigger( 5476 event.target.linkedBrowser 5477 ); 5478 5479 if (this.view.isOpen) { 5480 // Refresh results when a tab is closed while the results view is open. 5481 // This prevents switch-to-tab results from remaining in the results 5482 // list after their tab is closed. 5483 this.startQuery(); 5484 } 5485 } 5486 5487 _on_beforeinput(event) { 5488 if (event.data && this._keyDownEnterDeferred) { 5489 // Ignore char key input while processing enter key. 5490 event.preventDefault(); 5491 } 5492 } 5493 5494 _on_keydown(event) { 5495 if (event.currentTarget == this.window) { 5496 // It would be great if we could more easily detect the user focusing the 5497 // address bar through a keyboard shortcut, but F6 and TAB bypass are 5498 // not going through commands handling. 5499 // Also note we'll unset this on TabSelect, as it can focus the address 5500 // bar but we should not untrim in that case. 5501 this._untrimOnFocusAfterKeydown = !this.focused; 5502 return; 5503 } 5504 5505 if ( 5506 this.#isSmartbarMode && 5507 event.keyCode === KeyEvent.DOM_VK_RETURN && 5508 event.shiftKey 5509 ) { 5510 event.preventDefault(); 5511 return; 5512 } 5513 5514 if (!this.controller) { 5515 return; 5516 } 5517 5518 // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if 5519 // not properly handled, so let's first handle things that should not be 5520 // evaluated repeatedly. 5521 if (!event.repeat) { 5522 this.#allTextSelectedOnKeyDown = this.#allTextSelected; 5523 5524 if (event.keyCode === KeyEvent.DOM_VK_RETURN) { 5525 if (this._keyDownEnterDeferred) { 5526 this._keyDownEnterDeferred.reject(); 5527 } 5528 this._keyDownEnterDeferred = Promise.withResolvers(); 5529 event._disableCanonization = 5530 AppConstants.platform == "macosx" 5531 ? this._isKeyDownWithMeta 5532 : this._isKeyDownWithCtrl; 5533 } 5534 5535 // Now set the keydown trackers for the current event, anything that wants 5536 // to check the previous events should have happened before this point. 5537 // The previously value is persisted until keyup, as we check if the 5538 // modifiers were down, even if other keys are pressed in the meanwhile. 5539 if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) { 5540 this._isKeyDownWithCtrl = true; 5541 } 5542 if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) { 5543 this._isKeyDownWithMeta = true; 5544 } 5545 // This is used in keyup, so it can be set every time. 5546 this._isKeyDownWithMetaAndLeft = 5547 this._isKeyDownWithMeta && 5548 !event.shiftKey && 5549 event.keyCode == KeyEvent.DOM_VK_LEFT; 5550 5551 this._toggleActionOverride(event); 5552 } 5553 5554 // Due to event deferring, it's possible preventDefault() won't be invoked 5555 // soon enough to actually prevent some of the default behaviors, thus we 5556 // have to handle the event "twice". This first immediate call passes false 5557 // as second argument so that handleKeyNavigation will only simulate the 5558 // event handling, without actually executing actions. 5559 // TODO (Bug 1541806): improve this handling, maybe by delaying actions 5560 // instead of events. 5561 if (this.eventBufferer.shouldDeferEvent(event)) { 5562 this.controller.handleKeyNavigation(event, false); 5563 } 5564 this.eventBufferer.maybeDeferEvent(event, () => { 5565 this.controller.handleKeyNavigation(event); 5566 }); 5567 } 5568 5569 async _on_keyup(event) { 5570 if (event.currentTarget == this.window) { 5571 this._untrimOnFocusAfterKeydown = false; 5572 return; 5573 } 5574 5575 if (this.#allTextSelectedOnKeyDown) { 5576 let moveCursorToStart = this.#isHomeKeyUpEvent(event); 5577 // We must set the selection immediately because: 5578 // - on Mac Fn + Left is not handled properly as Home 5579 // - untrim depends on text not being fully selected. 5580 if (moveCursorToStart) { 5581 this.selectionStart = this.selectionEnd = 0; 5582 } 5583 this.#maybeUntrimUrl({ moveCursorToStart }); 5584 } 5585 if (event.keyCode === KeyEvent.DOM_VK_META) { 5586 this._isKeyDownWithMeta = false; 5587 this._isKeyDownWithMetaAndLeft = false; 5588 } 5589 if (event.keyCode === KeyEvent.DOM_VK_CONTROL) { 5590 this._isKeyDownWithCtrl = false; 5591 } 5592 5593 this._toggleActionOverride(event); 5594 5595 // Pressing Enter key while pressing Meta key, and next, even when releasing 5596 // Enter key before releasing Meta key, the keyup event is not fired. 5597 // Therefore, if Enter keydown is detecting, continue the post processing 5598 // for Enter key when any keyup event is detected. 5599 if (this._keyDownEnterDeferred) { 5600 if (this._keyDownEnterDeferred.loadedContent) { 5601 try { 5602 const loadingBrowser = await this._keyDownEnterDeferred.promise; 5603 // Ensure the selected browser didn't change in the meanwhile. 5604 if (this.window.gBrowser.selectedBrowser === loadingBrowser) { 5605 loadingBrowser.focus(); 5606 // Make sure the domain name stays visible for spoof protection and usability. 5607 this.setSelectionRange(0, 0); 5608 } 5609 } catch (ex) { 5610 // Not all the Enter actions in the urlbar will cause a navigation, then it 5611 // is normal for this to be rejected. 5612 // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here 5613 // to ensure not overwriting the new value created by keydown. 5614 } 5615 } else { 5616 // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately. 5617 this._keyDownEnterDeferred.resolve(); 5618 } 5619 5620 this._keyDownEnterDeferred = null; 5621 } 5622 } 5623 5624 _on_compositionstart() { 5625 if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 5626 throw new Error("Trying to start a nested composition?"); 5627 } 5628 this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING; 5629 5630 if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { 5631 return; 5632 } 5633 5634 // Close the view. This will also stop searching. 5635 if (this.view.isOpen) { 5636 // We're closing the view, but we want to retain search mode if the 5637 // selected result was previewing it. 5638 if (this.searchMode) { 5639 // If we entered search mode with an empty string, clear userTypedValue, 5640 // otherwise confirmSearchMode may try to set it as value. 5641 // This can happen for example if we entered search mode typing a 5642 // a partial engine domain and selecting a tab-to-search result. 5643 if (!this.value) { 5644 this.userTypedValue = null; 5645 } 5646 this.confirmSearchMode(); 5647 } 5648 this.#compositionClosedPopup = true; 5649 this.view.close(); 5650 } else { 5651 this.#compositionClosedPopup = false; 5652 } 5653 } 5654 5655 _on_compositionend(event) { 5656 if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 5657 throw new Error("Trying to stop a non existing composition?"); 5658 } 5659 5660 if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { 5661 // Clear the selection and the cached result, since they refer to the 5662 // state before this composition. A new input even will be generated 5663 // after this. 5664 this.view.clearSelection(); 5665 this._resultForCurrentValue = null; 5666 } 5667 5668 // We can't yet retrieve the committed value from the editor, since it isn't 5669 // completely committed yet. We'll handle it at the next input event. 5670 this.#compositionState = event.data 5671 ? lazy.UrlbarUtils.COMPOSITION.COMMIT 5672 : lazy.UrlbarUtils.COMPOSITION.CANCELED; 5673 } 5674 5675 _on_dragstart(event) { 5676 // Drag only if the gesture starts from the input field. 5677 let nodePosition = this.inputField.compareDocumentPosition( 5678 event.originalTarget 5679 ); 5680 if ( 5681 event.target != this.inputField && 5682 !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY) 5683 ) { 5684 return; 5685 } 5686 5687 // Don't cover potential drop targets on the toolbars or in content. 5688 this.view.close(); 5689 5690 // Only customize the drag data if the entire value is selected and it's a 5691 // loaded URI. Use default behavior otherwise. 5692 if ( 5693 !this.#allTextSelected || 5694 this.getAttribute("pageproxystate") != "valid" 5695 ) { 5696 return; 5697 } 5698 5699 let uri = this.makeURIReadable(this.window.gBrowser.currentURI); 5700 let href = uri.displaySpec; 5701 let title = this.window.gBrowser.contentTitle || href; 5702 5703 event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`); 5704 event.dataTransfer.setData("text/plain", href); 5705 event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`); 5706 event.dataTransfer.effectAllowed = "copyLink"; 5707 event.stopPropagation(); 5708 } 5709 5710 /** 5711 * Handles dragover events for the input. 5712 * 5713 * @param {DragEvent} event 5714 */ 5715 _on_dragover(event) { 5716 if (!getDroppableData(event)) { 5717 event.dataTransfer.dropEffect = "none"; 5718 } 5719 } 5720 5721 /** 5722 * Handles dropping of data on the input. 5723 * 5724 * @param {DragEvent} event 5725 */ 5726 _on_drop(event) { 5727 let droppedItem = getDroppableData(event); 5728 let droppedURL = URL.isInstance(droppedItem) 5729 ? droppedItem.href 5730 : droppedItem; 5731 if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) { 5732 let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event); 5733 this.value = droppedURL; 5734 this.setPageProxyState("invalid"); 5735 this.focus(); 5736 // To simplify tracking of events, register an initial event for event 5737 // telemetry, to replace the missing input event. 5738 let queryContext = this.#makeQueryContext({ searchString: droppedURL }); 5739 this.controller.setLastQueryContextCache(queryContext); 5740 this.controller.engagementEvent.start(event, queryContext); 5741 this.handleNavigation({ triggeringPrincipal: principal }); 5742 if (this.#isAddressbar) { 5743 // For safety reasons, in the drop case we don't want to immediately show 5744 // the dropped value, instead we want to keep showing the current page 5745 // url until an onLocationChange happens. 5746 // See the handling in `setURI` for further details. 5747 this.userTypedValue = null; 5748 this.setURI({ dueToTabSwitch: true }); 5749 } 5750 } 5751 } 5752 5753 _on_customizationstarting() { 5754 this.incrementBreakoutBlockerCount(); 5755 this.blur(); 5756 } 5757 5758 _on_aftercustomization() { 5759 this.decrementBreakoutBlockerCount(); 5760 this.#updateLayoutBreakout(); 5761 } 5762 5763 uiDensityChanged() { 5764 if (this.#breakoutBlockerCount) { 5765 return; 5766 } 5767 this.#updateLayoutBreakout(); 5768 } 5769 5770 _on_toolbarvisibilitychange() { 5771 this.#updateTextboxPositionNextFrame(); 5772 } 5773 5774 _on_DOMMenuBarActive() { 5775 this.#updateTextboxPositionNextFrame(); 5776 } 5777 5778 _on_DOMMenuBarInactive() { 5779 this.#updateTextboxPositionNextFrame(); 5780 } 5781 5782 #allTextSelectedOnKeyDown = false; 5783 get #allTextSelected() { 5784 return this.selectionStart == 0 && this.selectionEnd == this.value.length; 5785 } 5786 5787 /** 5788 * @param {string} value 5789 * A untrimmed address bar input. 5790 * @returns {nsILoadInfo.SchemelessInputType} 5791 * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input 5792 * doesn't start with a scheme relevant for schemeless HTTPS-First 5793 * (http://, https:// and file://). 5794 * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme. 5795 */ 5796 #getSchemelessInput(value) { 5797 return ["http://", "https://", "file://"].every( 5798 scheme => !value.trim().startsWith(scheme) 5799 ) 5800 ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless 5801 : Ci.nsILoadInfo.SchemelessInputTypeSchemeful; 5802 } 5803 5804 get #isOpenedPageInBlankTargetLoading() { 5805 return ( 5806 this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory 5807 ?.count === 0 && 5808 this.window.gBrowser.selectedBrowser.browsingContext 5809 .nonWebControlledBlankURI 5810 ); 5811 } 5812 5813 // Search modes are per browser and are stored in the `searchModes` property of this map. 5814 // For a browser, search mode can be in preview mode, confirmed, or both. 5815 // Typically, search mode is entered in preview mode with a particular 5816 // source and is confirmed with the same source once a query starts. It's 5817 // also possible for a confirmed search mode to be replaced with a preview 5818 // mode with a different source, and in those cases, we need to re-confirm 5819 // search mode when preview mode is exited. In addition, only confirmed 5820 // search modes should be restored across sessions. We therefore need to 5821 // keep track of both the current confirmed and preview modes, per browser. 5822 // 5823 // For each browser with a search mode, this maps the browser to an object 5824 // like this: { preview, confirmed }. Both `preview` and `confirmed` are 5825 // search mode objects; see the setSearchMode documentation. Either one may 5826 // be undefined if that particular mode is not active for the browser. 5827 5828 /** 5829 * Tracks a state object per browser. 5830 */ 5831 #browserStates = new WeakMap(); 5832 5833 get #selectedText() { 5834 return this.editor.selection.toStringWithFormat( 5835 "text/plain", 5836 Ci.nsIDocumentEncoder.OutputPreformatted | 5837 Ci.nsIDocumentEncoder.OutputRaw, 5838 0 5839 ); 5840 } 5841 5842 /** 5843 * Check whether a key event has a similar effect as the Home key. 5844 * 5845 * @param {KeyboardEvent} event A Keyboard event 5846 * @returns {boolean} Whether the even will act like the Home key. 5847 */ 5848 #isHomeKeyUpEvent(event) { 5849 let isMac = AppConstants.platform === "macosx"; 5850 return ( 5851 // On MacOS this can be generated with Fn + Left. 5852 event.keyCode == KeyEvent.DOM_VK_HOME || 5853 // Windows and Linux also support Ctrl + Left. 5854 (!isMac && 5855 event.keyCode == KeyboardEvent.DOM_VK_LEFT && 5856 event.ctrlKey && 5857 !event.shiftKey) || 5858 // MacOS supports other combos to move cursor at the start of the line. 5859 // For example Ctrl + A. 5860 (isMac && 5861 event.keyCode == KeyboardEvent.DOM_VK_A && 5862 event.ctrlKey && 5863 !event.shiftKey) || 5864 // And also Cmd (Meta) + Left. 5865 // Unfortunately on MacOS it's not possible to detect combos with the meta 5866 // key during the keyup event, due to how the OS handles events. Thus we 5867 // record the combo on keydown, and check for it here. 5868 (isMac && 5869 event.keyCode == KeyEvent.DOM_VK_META && 5870 this._isKeyDownWithMetaAndLeft) 5871 ); 5872 } 5873 } 5874 5875 /** 5876 * Tries to extract droppable data from a DND event. 5877 * 5878 * @param {DragEvent} event The DND event to examine. 5879 * @returns {URL|string|null} 5880 * null if there's a security reason for which we should do nothing. 5881 * A URL object if it's a value we can load. 5882 * A string value otherwise. 5883 */ 5884 function getDroppableData(event) { 5885 let links; 5886 try { 5887 links = Services.droppedLinkHandler.dropLinks(event); 5888 } catch (ex) { 5889 // This is either an unexpected failure or a security exception; in either 5890 // case we should always return null. 5891 return null; 5892 } 5893 // The URL bar automatically handles inputs with newline characters, 5894 // so we can get away with treating text/x-moz-url flavours as text/plain. 5895 if (links[0]?.url) { 5896 event.preventDefault(); 5897 let href = links[0].url; 5898 if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) { 5899 // We may have stripped an unsafe protocol like javascript: and if so 5900 // there's no point in handling a partial drop. 5901 event.stopImmediatePropagation(); 5902 return null; 5903 } 5904 5905 // If this fails, checkLoadURIStrWithPrincipal would also fail, 5906 // as that's what it does with things that don't pass the IO 5907 // service's newURI constructor without fixup. It's conceivable we 5908 // may want to relax this check in the future (so e.g. www.foo.com 5909 // gets fixed up), but not right now. 5910 let url = URL.parse(href); 5911 if (url) { 5912 // If we succeed, try to pass security checks. If this works, return the 5913 // URL object. If the *security checks* fail, return null. 5914 try { 5915 let principal = 5916 Services.droppedLinkHandler.getTriggeringPrincipal(event); 5917 Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( 5918 principal, 5919 url.href, 5920 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL 5921 ); 5922 return url; 5923 } catch (ex) { 5924 return null; 5925 } 5926 } 5927 // We couldn't make a URL out of this. Continue on, and return text below. 5928 } 5929 // Handle as text. 5930 return event.dataTransfer.getData("text/plain"); 5931 } 5932 5933 /** 5934 * Decodes the given URI for displaying it in the address bar without losing 5935 * information, such that hitting Enter again will load the same URI. 5936 * 5937 * @param {nsIURI} aURI 5938 * The URI to decode 5939 * @returns {string} 5940 * The decoded URI 5941 */ 5942 function losslessDecodeURI(aURI) { 5943 let scheme = aURI.scheme; 5944 let value = aURI.displaySpec; 5945 5946 // Try to decode as UTF-8 if there's no encoding sequence that we would break. 5947 if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { 5948 let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme); 5949 if (decodeASCIIOnly) { 5950 // This only decodes ascii characters (hex) 20-7e, except 25 (%). 5951 // This avoids both cases stipulated below (%-related issues, and \r, \n 5952 // and \t, which would be %0d, %0a and %09, respectively) as well as any 5953 // non-US-ascii characters. 5954 value = value.replace( 5955 /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, 5956 decodeURI 5957 ); 5958 } else { 5959 try { 5960 value = decodeURI(value) 5961 // decodeURI decodes %25 to %, which creates unintended encoding 5962 // sequences. Re-encode it, unless it's part of a sequence that 5963 // survived decodeURI, i.e. one for: 5964 // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' 5965 // (RFC 3987 section 3.2) 5966 .replace( 5967 /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi, 5968 encodeURIComponent 5969 ); 5970 } catch (e) {} 5971 } 5972 } 5973 5974 // IMPORTANT: The following regular expressions are Unicode-aware due to /v. 5975 // Avoid matching high or low surrogate pairs directly, always work with 5976 // full Unicode scalar values. 5977 5978 // Encode potentially invisible characters: 5979 // U+0000-001F: C0/C1 control characters 5980 // U+007F-009F: commands 5981 // U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces 5982 // U+2028-2029: line and paragraph separators 5983 // U+2800: braille empty pattern 5984 // U+FFFC: object replacement character 5985 // Encode any trailing whitespace that may be part of a pasted URL, so that it 5986 // doesn't get eaten away by the location bar (bug 410726). 5987 // Encode all adjacent space chars (U+0020), to prevent spoofing attempts 5988 // where they would push part of the URL to overflow the location bar 5989 // (bug 1395508). A single space, or the last space if the are many, is 5990 // preserved to maintain readability of certain urls if it's not followed by a 5991 // control or separator character. We only do this for the common space, 5992 // because others may be eaten when copied to the clipboard,so it's safer to 5993 // preserve them encoded. 5994 value = value.replace( 5995 // eslint-disable-next-line no-control-regex 5996 /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv, 5997 encodeURIComponent 5998 ); 5999 6000 // Encode characters that are ignorable, can't be rendered usefully, or may 6001 // confuse users. 6002 // 6003 // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded 6004 // per bug 582186: 6005 // U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E, 6006 // U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0, 6007 // U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF 6008 // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6): 6009 // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069 6010 // Other format characters in the Cf category that are unlikely to be rendered 6011 // usefully: 6012 // U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3 6013 // Mimicking UI parts: 6014 // U+1F50F-1F513, U+1F6E1 6015 // Unassigned codepoints, sometimes shown as empty glyphs. 6016 value = value.replace( 6017 // eslint-disable-next-line no-misleading-character-class 6018 /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv, 6019 encodeURIComponent 6020 ); 6021 return value; 6022 } 6023 6024 /** 6025 * Handles copy and cut commands for the urlbar. 6026 */ 6027 class CopyCutController { 6028 /** 6029 * @param {SmartbarInput} urlbar 6030 * The SmartbarInput instance to use this controller for. 6031 */ 6032 constructor(urlbar) { 6033 this.urlbar = urlbar; 6034 } 6035 6036 /** 6037 * @param {string} command 6038 * The name of the command to handle. 6039 */ 6040 doCommand(command) { 6041 let urlbar = this.urlbar; 6042 let val = urlbar._getSelectedValueForClipboard(); 6043 if (!val) { 6044 return; 6045 } 6046 6047 if (command == "cmd_cut" && this.isCommandEnabled(command)) { 6048 let start = urlbar.selectionStart; 6049 let end = urlbar.selectionEnd; 6050 urlbar.inputField.value = 6051 urlbar.inputField.value.substring(0, start) + 6052 urlbar.inputField.value.substring(end); 6053 urlbar.inputField.setSelectionRange(start, start); 6054 6055 let event = new UIEvent("input", { 6056 bubbles: true, 6057 cancelable: false, 6058 view: urlbar.window, 6059 detail: 0, 6060 }); 6061 urlbar.inputField.dispatchEvent(event); 6062 } 6063 6064 lazy.ClipboardHelper.copyString(val); 6065 } 6066 6067 /** 6068 * @param {string} command 6069 * The name of the command to check. 6070 * @returns {boolean} 6071 * Whether the command is handled by this controller. 6072 */ 6073 supportsCommand(command) { 6074 switch (command) { 6075 case "cmd_copy": 6076 case "cmd_cut": 6077 return true; 6078 } 6079 return false; 6080 } 6081 6082 /** 6083 * @param {string} command 6084 * The name of the command to check. 6085 * @returns {boolean} 6086 * Whether the command should be enabled. 6087 */ 6088 isCommandEnabled(command) { 6089 return ( 6090 this.supportsCommand(command) && 6091 (command != "cmd_cut" || !this.urlbar.readOnly) && 6092 this.urlbar.selectionStart < this.urlbar.selectionEnd 6093 ); 6094 } 6095 6096 onEvent() {} 6097 } 6098 6099 /** 6100 * Manages the Add Search Engine contextual menu entries. 6101 * 6102 * Note: setEnginesFromBrowser must be invoked from the outside when the 6103 * page provided engines list changes. 6104 * refreshContextMenu must be invoked when the context menu is opened. 6105 */ 6106 class AddSearchEngineHelper { 6107 /** 6108 * @type {UrlbarSearchOneOffs} 6109 */ 6110 shortcutButtons; 6111 6112 /** 6113 * @param {SmartbarInput} input The parent SmartbarInput. 6114 */ 6115 constructor(input) { 6116 this.input = input; 6117 this.shortcutButtons = input.view.oneOffSearchButtons; 6118 } 6119 6120 /** 6121 * If there's more than this number of engines, the context menu offers 6122 * them in a submenu. 6123 * 6124 * @returns {number} 6125 */ 6126 get maxInlineEngines() { 6127 return this.shortcutButtons._maxInlineAddEngines; 6128 } 6129 6130 /** 6131 * Invoked by OpenSearchManager when the list of available engines changes. 6132 * 6133 * @param {object} browser The current browser. 6134 * @param {object} engines The updated list of available engines. 6135 */ 6136 setEnginesFromBrowser(browser, engines) { 6137 this.browsingContext = browser.browsingContext; 6138 // Make a copy of the array for state comparison. 6139 engines = engines.slice(); 6140 if (!this._sameEngines(this.engines, engines)) { 6141 this.engines = engines; 6142 this.shortcutButtons?.updateWebEngines(); 6143 } 6144 } 6145 6146 _sameEngines(engines1, engines2) { 6147 if (engines1?.length != engines2?.length) { 6148 return false; 6149 } 6150 return lazy.ObjectUtils.deepEqual( 6151 engines1.map(e => e.title), 6152 engines2.map(e => e.title) 6153 ); 6154 } 6155 6156 _createMenuitem(engine, index) { 6157 let elt = this.input.document.createXULElement("menuitem"); 6158 elt.setAttribute("anonid", `add-engine-${index}`); 6159 elt.classList.add("menuitem-iconic"); 6160 elt.classList.add("context-menu-add-engine"); 6161 this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", { 6162 engineName: engine.title, 6163 }); 6164 elt.setAttribute("uri", engine.uri); 6165 if (engine.icon) { 6166 elt.setAttribute("image", engine.icon); 6167 } else { 6168 elt.removeAttribute("image"); 6169 } 6170 elt.addEventListener("command", this._onCommand.bind(this)); 6171 return elt; 6172 } 6173 6174 _createMenu(engine) { 6175 let elt = this.input.document.createXULElement("menu"); 6176 elt.setAttribute("anonid", "add-engine-menu"); 6177 elt.classList.add("menu-iconic"); 6178 elt.classList.add("context-menu-add-engine"); 6179 this.input.document.l10n.setAttributes( 6180 elt, 6181 "search-one-offs-add-engine-menu" 6182 ); 6183 if (engine.icon) { 6184 elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon)); 6185 } 6186 let popup = this.input.document.createXULElement("menupopup"); 6187 elt.appendChild(popup); 6188 return elt; 6189 } 6190 6191 refreshContextMenu() { 6192 let engines = this.engines; 6193 let contextMenu = this.input.querySelector("moz-input-box").menupopup; 6194 6195 // Certain operations, like customization, destroy and recreate widgets, 6196 // so we cannot rely on cached elements. 6197 if (!contextMenu.querySelector(".menuseparator-add-engine")) { 6198 this.contextSeparator = 6199 this.input.document.createXULElement("menuseparator"); 6200 this.contextSeparator.setAttribute("anonid", "add-engine-separator"); 6201 this.contextSeparator.classList.add("menuseparator-add-engine"); 6202 this.contextSeparator.collapsed = true; 6203 contextMenu.appendChild(this.contextSeparator); 6204 } 6205 6206 this.contextSeparator.collapsed = !engines.length; 6207 let curElt = this.contextSeparator; 6208 // Remove the previous items, if any. 6209 for (let elt = curElt.nextElementSibling; elt; ) { 6210 let nextElementSibling = elt.nextElementSibling; 6211 elt.remove(); 6212 elt = nextElementSibling; 6213 } 6214 6215 // If the page provides too many engines, we only show a single menu entry 6216 // with engines in a submenu. 6217 if (engines.length > this.maxInlineEngines) { 6218 // Set the menu button's image to the image of the first engine. The 6219 // offered engines may have differing images, so there's no perfect 6220 // choice here. 6221 let elt = this._createMenu(engines[0]); 6222 this.contextSeparator.insertAdjacentElement("afterend", elt); 6223 curElt = elt.lastElementChild; 6224 } 6225 6226 // Insert the engines, either in the contextual menu or the sub menu. 6227 for (let i = 0; i < engines.length; ++i) { 6228 let elt = this._createMenuitem(engines[i], i); 6229 if (curElt.localName == "menupopup") { 6230 curElt.appendChild(elt); 6231 } else { 6232 curElt.insertAdjacentElement("afterend", elt); 6233 } 6234 curElt = elt; 6235 } 6236 } 6237 6238 async _onCommand(event) { 6239 let added = await lazy.SearchUIUtils.addOpenSearchEngine( 6240 event.target.getAttribute("uri"), 6241 event.target.getAttribute("image"), 6242 this.browsingContext 6243 ).catch(console.error); 6244 if (added) { 6245 // Remove the offered engine from the list. The browser updated the 6246 // engines list at this point, so we just have to refresh the menu.) 6247 this.refreshContextMenu(); 6248 } 6249 } 6250 } 6251 6252 customElements.define("moz-smartbar", SmartbarInput);