searchbar.js (34250B)
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 "use strict"; 6 7 /* globals XULCommandEvent */ 8 9 // This is loaded into chrome windows with the subscript loader. Wrap in 10 // a block to prevent accidentally leaking globals onto `window`. 11 { 12 const lazy = {}; 13 14 ChromeUtils.defineESModuleGetters(lazy, { 15 BrowserSearchTelemetry: 16 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 17 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 18 FormHistory: "resource://gre/modules/FormHistory.sys.mjs", 19 SearchSuggestionController: 20 "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs", 21 }); 22 23 /** 24 * Defines the search bar element. 25 */ 26 class MozSearchbar extends MozXULElement { 27 static get inheritedAttributes() { 28 return { 29 ".searchbar-textbox": 30 "disabled,disableautocomplete,searchengine,src,newlines", 31 ".searchbar-search-button": "addengines", 32 }; 33 } 34 35 static get markup() { 36 return ` 37 <stringbundle src="chrome://browser/locale/search.properties"></stringbundle> 38 <hbox class="searchbar-search-button" data-l10n-id="searchbar-icon" role="button" keyNav="false" aria-expanded="false" aria-controls="PopupSearchAutoComplete" aria-haspopup="true"> 39 <image class="searchbar-search-icon"></image> 40 <image class="searchbar-search-icon-overlay"></image> 41 </hbox> 42 <html:input class="searchbar-textbox" is="autocomplete-input" type="search" data-l10n-id="searchbar-input" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0"/> 43 <menupopup class="textbox-contextmenu"></menupopup> 44 <hbox class="search-go-container" align="center"> 45 <image class="search-go-button urlbar-icon" role="button" keyNav="false" hidden="true" data-l10n-id="searchbar-submit"></image> 46 </hbox> 47 `; 48 } 49 50 constructor() { 51 super(); 52 MozXULElement.insertFTLIfNeeded("browser/search.ftl"); 53 54 this._setupEventListeners(); 55 let searchbar = this; 56 this.observer = { 57 observe(aEngine, aTopic, aData) { 58 if (aTopic == "browser-search-engine-modified") { 59 // Make sure the engine list is refetched next time it's needed 60 searchbar._engines = null; 61 62 // Update the popup header and update the display after any modification. 63 searchbar._textbox.popup.updateHeader(); 64 searchbar.updateDisplay(); 65 } else if ( 66 aData == "browser.search.widget.new" && 67 searchbar.isConnected 68 ) { 69 if (Services.prefs.getBoolPref("browser.search.widget.new")) { 70 searchbar.disconnectedCallback(); 71 } else { 72 searchbar.connectedCallback(); 73 } 74 } 75 }, 76 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 77 }; 78 79 Services.prefs.addObserver("browser.search.widget.new", this.observer); 80 81 window.addEventListener("unload", () => { 82 this.destroy(); 83 Services.prefs.removeObserver( 84 "browser.search.widget.new", 85 this.observer 86 ); 87 }); 88 89 this._ignoreFocus = false; 90 this._engines = null; 91 this.telemetrySelectedIndex = -1; 92 } 93 94 connectedCallback() { 95 // Don't initialize if this isn't going to be visible. 96 if ( 97 this.closest("#BrowserToolbarPalette") || 98 Services.prefs.getBoolPref("browser.search.widget.new") 99 ) { 100 return; 101 } 102 103 this.appendChild(this.constructor.fragment); 104 this.initializeAttributeInheritance(); 105 106 // Don't go further if in Customize mode. 107 if (this.parentNode.parentNode.localName == "toolbarpaletteitem") { 108 return; 109 } 110 111 // Ensure we get persisted widths back, if we've been in the palette: 112 let storedWidth = Services.xulStore.getValue( 113 document.documentURI, 114 this.parentNode.id, 115 "width" 116 ); 117 if (storedWidth) { 118 this.parentNode.setAttribute("width", storedWidth); 119 this.parentNode.style.width = storedWidth + "px"; 120 } 121 122 this._stringBundle = this.querySelector("stringbundle"); 123 this._textbox = this.querySelector(".searchbar-textbox"); 124 125 this._menupopup = null; 126 this._pasteAndSearchMenuItem = null; 127 128 this._setupTextboxEventListeners(); 129 this._initTextbox(); 130 131 Services.obs.addObserver(this.observer, "browser-search-engine-modified"); 132 133 this._initialized = true; 134 135 (window.delayedStartupPromise || Promise.resolve()).then(() => { 136 window.requestIdleCallback(() => { 137 Services.search 138 .init() 139 .then(() => { 140 // Bail out if the binding's been destroyed 141 if (!this._initialized) { 142 return; 143 } 144 145 // Ensure the popup header is updated if the user has somehow 146 // managed to open the popup before the search service has finished 147 // initializing. 148 this._textbox.popup.updateHeader(); 149 // Refresh the display (updating icon, etc) 150 this.updateDisplay(); 151 OpenSearchManager.updateOpenSearchBadge(window); 152 }) 153 .catch(status => 154 console.error( 155 "Cannot initialize search service, bailing out:", 156 status 157 ) 158 ); 159 }); 160 }); 161 162 // Wait until the popupshowing event to avoid forcing immediate 163 // attachment of the search-one-offs binding. 164 this.textbox.popup.addEventListener( 165 "popupshowing", 166 () => { 167 let oneOffButtons = this.textbox.popup.oneOffButtons; 168 // Some accessibility tests create their own <searchbar> that doesn't 169 // use the popup binding below, so null-check oneOffButtons. 170 if (oneOffButtons) { 171 oneOffButtons.telemetryOrigin = "searchbar"; 172 // Set .textbox first, since the popup setter will cause 173 // a _rebuild call that uses it. 174 oneOffButtons.textbox = this.textbox; 175 oneOffButtons.popup = this.textbox.popup; 176 } 177 }, 178 { capture: true, once: true } 179 ); 180 181 this.querySelector(".search-go-button").addEventListener("click", event => 182 this.handleSearchCommand(event) 183 ); 184 } 185 186 async getEngines() { 187 if (!this._engines) { 188 this._engines = await Services.search.getVisibleEngines(); 189 } 190 return this._engines; 191 } 192 193 set currentEngine(val) { 194 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 195 Services.search.setDefaultPrivate( 196 val, 197 Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR 198 ); 199 } else { 200 Services.search.setDefault( 201 val, 202 Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR 203 ); 204 } 205 } 206 207 get currentEngine() { 208 let currentEngine; 209 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 210 currentEngine = Services.search.defaultPrivateEngine; 211 } else { 212 currentEngine = Services.search.defaultEngine; 213 } 214 // Return a dummy engine if there is no currentEngine 215 return currentEngine || { name: "", uri: null }; 216 } 217 218 /** 219 * textbox is used by sanitize.js to clear the undo history when 220 * clearing form information. 221 * 222 * @returns {HTMLInputElement} 223 */ 224 get textbox() { 225 return this._textbox; 226 } 227 228 /** 229 * Textbox alias for API compatibility with UrlbarInput. 230 */ 231 get inputField() { 232 return this.textbox; 233 } 234 235 set value(val) { 236 this._textbox.value = val; 237 } 238 239 get value() { 240 return this._textbox.value; 241 } 242 243 destroy() { 244 if (this._initialized) { 245 this._initialized = false; 246 247 Services.obs.removeObserver( 248 this.observer, 249 "browser-search-engine-modified" 250 ); 251 } 252 253 // Make sure to break the cycle from _textbox to us. Otherwise we leak 254 // the world. But make sure it's actually pointing to us. 255 // Also make sure the textbox has ever been constructed, otherwise the 256 // _textbox getter will cause the textbox constructor to run, add an 257 // observer, and leak the world too. 258 if ( 259 this._textbox && 260 this._textbox.mController && 261 this._textbox.mController.input && 262 this._textbox.mController.input.wrappedJSObject == 263 this.nsIAutocompleteInput 264 ) { 265 this._textbox.mController.input = null; 266 } 267 } 268 269 focus() { 270 this._textbox.focus(); 271 } 272 273 select() { 274 this._textbox.select(); 275 } 276 277 setIcon(element, uri) { 278 element.setAttribute("src", uri); 279 } 280 281 updateDisplay() { 282 this._textbox.title = this._stringBundle.getFormattedString("searchtip", [ 283 this.currentEngine.name, 284 ]); 285 } 286 287 updateGoButtonVisibility() { 288 this.querySelector(".search-go-button").hidden = !this._textbox.value; 289 } 290 291 openSuggestionsPanel(aShowOnlySettingsIfEmpty) { 292 if (this._textbox.open) { 293 return; 294 } 295 296 this._textbox.showHistoryPopup(); 297 let searchIcon = document.querySelector(".searchbar-search-button"); 298 searchIcon.setAttribute("aria-expanded", "true"); 299 300 if (this._textbox.value) { 301 // showHistoryPopup does a startSearch("") call, ensure the 302 // controller handles the text from the input box instead: 303 this._textbox.mController.handleText(); 304 } else if (aShowOnlySettingsIfEmpty) { 305 this.setAttribute("showonlysettings", "true"); 306 } 307 } 308 309 async selectEngine(aEvent, isNextEngine) { 310 // Stop event bubbling now, because the rest of this method is async. 311 aEvent.preventDefault(); 312 aEvent.stopPropagation(); 313 314 // Find the new index. 315 let engines = await this.getEngines(); 316 let currentName = this.currentEngine.name; 317 let newIndex = -1; 318 let lastIndex = engines.length - 1; 319 for (let i = lastIndex; i >= 0; --i) { 320 if (engines[i].name == currentName) { 321 // Check bounds to cycle through the list of engines continuously. 322 if (!isNextEngine && i == 0) { 323 newIndex = lastIndex; 324 } else if (isNextEngine && i == lastIndex) { 325 newIndex = 0; 326 } else { 327 newIndex = i + (isNextEngine ? 1 : -1); 328 } 329 break; 330 } 331 } 332 333 this.currentEngine = engines[newIndex]; 334 335 this.openSuggestionsPanel(); 336 } 337 338 handleSearchCommand(aEvent, aEngine, aForceNewTab) { 339 if ( 340 aEvent && 341 aEvent.originalTarget.classList.contains("search-go-button") && 342 aEvent.button == 2 343 ) { 344 return; 345 } 346 let { where, params } = this._whereToOpen(aEvent, aForceNewTab); 347 this.handleSearchCommandWhere(aEvent, aEngine, where, params); 348 } 349 350 handleSearchCommandWhere(aEvent, aEngine, aWhere, aParams = {}) { 351 let textBox = this._textbox; 352 let textValue = textBox.value; 353 354 let selectedIndex = this.telemetrySelectedIndex; 355 let isOneOff = false; 356 357 lazy.BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod( 358 aEvent, 359 selectedIndex 360 ); 361 362 if (selectedIndex == -1) { 363 isOneOff = 364 this.textbox.popup.oneOffButtons.eventTargetIsAOneOff(aEvent); 365 } 366 367 if (aWhere === "tab" && !!aParams.inBackground) { 368 // Keep the focus in the search bar. 369 aParams.avoidBrowserFocus = true; 370 } else if ( 371 aWhere !== "window" && 372 aEvent.keyCode === KeyEvent.DOM_VK_RETURN 373 ) { 374 // Move the focus to the selected browser when keyup the Enter. 375 aParams.avoidBrowserFocus = true; 376 this._needBrowserFocusAtEnterKeyUp = true; 377 } 378 379 // This is a one-off search only if oneOffRecorded is true. 380 this.doSearch(textValue, aWhere, aEngine, aParams, isOneOff); 381 } 382 383 doSearch(aData, aWhere, aEngine, aParams, isOneOff = false) { 384 let textBox = this._textbox; 385 let engine = aEngine || this.currentEngine; 386 387 // Save the current value in the form history 388 if ( 389 aData && 390 !PrivateBrowsingUtils.isWindowPrivate(window) && 391 lazy.FormHistory.enabled && 392 aData.length <= 393 lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH 394 ) { 395 lazy.FormHistory.update({ 396 op: "bump", 397 fieldname: textBox.getAttribute("autocompletesearchparam"), 398 value: aData, 399 source: engine.name, 400 }).catch(error => 401 console.error("Saving search to form history failed:", error) 402 ); 403 } 404 405 let submission = engine.getSubmission(aData, null); 406 407 // If we hit here, we come either from a one-off, a plain search or a suggestion. 408 const details = { 409 isOneOff, 410 isSuggestion: !isOneOff && this.telemetrySelectedIndex != -1, 411 }; 412 413 this.telemetrySelectedIndex = -1; 414 415 // Record when the user uses the search bar 416 Services.prefs.setStringPref( 417 "browser.search.widget.lastUsed", 418 new Date().toISOString() 419 ); 420 421 // null parameter below specifies HTML response for search 422 let params = { 423 postData: submission.postData, 424 globalHistoryOptions: { 425 triggeringSearchEngine: engine.name, 426 }, 427 }; 428 if (aParams) { 429 for (let key in aParams) { 430 params[key] = aParams[key]; 431 } 432 } 433 434 if (aWhere == "tab") { 435 gBrowser.tabContainer.addEventListener( 436 "TabOpen", 437 event => 438 lazy.BrowserSearchTelemetry.recordSearch( 439 event.target.linkedBrowser, 440 engine, 441 "searchbar", 442 details 443 ), 444 { once: true } 445 ); 446 } else { 447 lazy.BrowserSearchTelemetry.recordSearch( 448 gBrowser.selectedBrowser, 449 engine, 450 "searchbar", 451 details 452 ); 453 } 454 455 openTrustedLinkIn(submission.uri.spec, aWhere, params); 456 } 457 458 /** 459 * Returns information on where a search results page should be loaded: in the 460 * current tab or a new tab. 461 * 462 * @param {event} aEvent 463 * The event that triggered the page load. 464 * @param {boolean} [aForceNewTab] 465 * True to force the load in a new tab. 466 * @returns {object} An object { where, params }. `where` is a string: 467 * "current" or "tab". `params` is an object further describing how 468 * the page should be loaded. 469 */ 470 _whereToOpen(aEvent, aForceNewTab = false) { 471 let where = "current"; 472 let params = {}; 473 const newTabPref = Services.prefs.getBoolPref("browser.search.openintab"); 474 475 // Open ctrl/cmd clicks on one-off buttons in a new background tab. 476 if (aEvent?.originalTarget.classList.contains("search-go-button")) { 477 where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true); 478 if ( 479 newTabPref && 480 !aEvent.altKey && 481 !aEvent.getModifierState("AltGraph") && 482 where == "current" && 483 !gBrowser.selectedTab.isEmpty 484 ) { 485 where = "tab"; 486 } 487 } else if (aForceNewTab) { 488 where = "tab"; 489 if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) { 490 params = { 491 inBackground: true, 492 }; 493 } 494 } else { 495 if ( 496 (KeyboardEvent.isInstance(aEvent) && 497 (aEvent.altKey || aEvent.getModifierState("AltGraph"))) ^ 498 newTabPref && 499 !gBrowser.selectedTab.isEmpty 500 ) { 501 where = "tab"; 502 } 503 if ( 504 MouseEvent.isInstance(aEvent) && 505 (aEvent.button == 1 || aEvent.getModifierState("Accel")) 506 ) { 507 where = "tab"; 508 params = { 509 inBackground: true, 510 }; 511 } 512 } 513 514 return { where, params }; 515 } 516 517 /** 518 * Opens the search form of the provided engine or the current engine 519 * if no engine was provided. 520 * 521 * @param {event} aEvent 522 * The event causing the searchForm to be opened. 523 * @param {nsISearchEngine} [aEngine] 524 * The search engine or undefined to use the current engine. 525 * @param {string} where 526 * Where the search form should be opened. 527 * @param {object} [params] 528 * Parameters for URILoadingHelper.openLinkIn. 529 */ 530 openSearchFormWhere(aEvent, aEngine, where, params = {}) { 531 let engine = aEngine || this.currentEngine; 532 let searchForm = engine.searchForm; 533 534 if (where === "tab" && !!params.inBackground) { 535 // Keep the focus in the search bar. 536 params.avoidBrowserFocus = true; 537 } else if ( 538 where !== "window" && 539 aEvent.keyCode === KeyEvent.DOM_VK_RETURN 540 ) { 541 // Move the focus to the selected browser when keyup the Enter. 542 params.avoidBrowserFocus = true; 543 this._needBrowserFocusAtEnterKeyUp = true; 544 } 545 546 lazy.BrowserSearchTelemetry.recordSearchForm(engine, "searchbar"); 547 openTrustedLinkIn(searchForm, where, params); 548 } 549 550 disconnectedCallback() { 551 this.destroy(); 552 while (this.firstChild) { 553 this.firstChild.remove(); 554 } 555 } 556 557 /** 558 * Determines if we should select all the text in the searchbar based on the 559 * searchbar state, and whether the selection is empty. 560 */ 561 _maybeSelectAll() { 562 if ( 563 !this._preventClickSelectsAll && 564 document.activeElement == this._textbox && 565 this._textbox.selectionStart == this._textbox.selectionEnd 566 ) { 567 this.select(); 568 } 569 } 570 571 _setupEventListeners() { 572 this.addEventListener("click", () => { 573 this._maybeSelectAll(); 574 }); 575 576 this.addEventListener( 577 "DOMMouseScroll", 578 event => { 579 if (event.getModifierState("Accel")) { 580 this.selectEngine(event, event.detail > 0); 581 } 582 }, 583 true 584 ); 585 586 this.addEventListener("input", () => { 587 this.updateGoButtonVisibility(); 588 }); 589 590 this.addEventListener("drop", () => { 591 this.updateGoButtonVisibility(); 592 }); 593 594 this.addEventListener( 595 "blur", 596 () => { 597 // Reset the flag since we can't capture enter keyup event if the event happens 598 // after moving the focus. 599 this._needBrowserFocusAtEnterKeyUp = false; 600 601 // If the input field is still focused then a different window has 602 // received focus, ignore the next focus event. 603 this._ignoreFocus = document.activeElement == this._textbox; 604 }, 605 true 606 ); 607 608 this.addEventListener( 609 "focus", 610 () => { 611 // Speculatively connect to the current engine's search URI (and 612 // suggest URI, if different) to reduce request latency 613 this.currentEngine.speculativeConnect({ 614 window, 615 originAttributes: gBrowser.contentPrincipal.originAttributes, 616 }); 617 618 if (this._ignoreFocus) { 619 // This window has been re-focused, don't show the suggestions 620 this._ignoreFocus = false; 621 return; 622 } 623 624 // Don't open the suggestions if there is no text in the textbox. 625 if (!this._textbox.value) { 626 return; 627 } 628 629 // Don't open the suggestions if the mouse was used to focus the 630 // textbox, that will be taken care of in the click handler. 631 if ( 632 Services.focus.getLastFocusMethod(window) & 633 Services.focus.FLAG_BYMOUSE 634 ) { 635 return; 636 } 637 638 this.openSuggestionsPanel(); 639 }, 640 true 641 ); 642 643 this.addEventListener("mousedown", event => { 644 this._preventClickSelectsAll = this._textbox.focused; 645 // Ignore right clicks 646 if (event.button != 0) { 647 return; 648 } 649 650 // Ignore clicks on the search go button. 651 if (event.originalTarget.classList.contains("search-go-button")) { 652 return; 653 } 654 655 // Ignore clicks on menu items in the input's context menu. 656 if (event.originalTarget.localName == "menuitem") { 657 return; 658 } 659 660 let isIconClick = event.originalTarget.classList.contains( 661 "searchbar-search-button" 662 ); 663 664 // Hide popup when icon is clicked while popup is open 665 if (isIconClick && this.textbox.popup.popupOpen) { 666 this.textbox.popup.closePopup(); 667 let searchIcon = document.querySelector(".searchbar-search-button"); 668 searchIcon.setAttribute("aria-expanded", "false"); 669 } else if (isIconClick || this._textbox.value) { 670 // Open the suggestions whenever clicking on the search icon or if there 671 // is text in the textbox. 672 this.openSuggestionsPanel(true); 673 } 674 }); 675 } 676 677 _setupTextboxEventListeners() { 678 this.textbox.addEventListener("input", () => { 679 this.textbox.popup.removeAttribute("showonlysettings"); 680 }); 681 682 this.textbox.addEventListener("dragover", event => { 683 let types = event.dataTransfer.types; 684 if ( 685 types.includes("text/plain") || 686 types.includes("text/x-moz-text-internal") 687 ) { 688 event.preventDefault(); 689 } 690 }); 691 692 this.textbox.addEventListener("drop", event => { 693 let dataTransfer = event.dataTransfer; 694 let data = dataTransfer.getData("text/plain"); 695 if (!data) { 696 data = dataTransfer.getData("text/x-moz-text-internal"); 697 } 698 if (data) { 699 event.preventDefault(); 700 this.textbox.value = data; 701 this.openSuggestionsPanel(); 702 } 703 }); 704 705 this.textbox.addEventListener("contextmenu", event => { 706 if (!this._menupopup) { 707 this._buildContextMenu(); 708 } 709 710 this._textbox.closePopup(); 711 712 // Make sure the context menu isn't opened via keyboard shortcut. Check for text selection 713 // before updating the state of any menu items. 714 if (event.button) { 715 this._maybeSelectAll(); 716 } 717 718 // Update disabled state of menu items 719 for (let item of this._menupopup.querySelectorAll("menuitem[cmd]")) { 720 let command = item.getAttribute("cmd"); 721 let controller = 722 document.commandDispatcher.getControllerForCommand(command); 723 item.disabled = !controller.isCommandEnabled(command); 724 } 725 726 let pasteEnabled = document.commandDispatcher 727 .getControllerForCommand("cmd_paste") 728 .isCommandEnabled("cmd_paste"); 729 this._pasteAndSearchMenuItem.disabled = !pasteEnabled; 730 731 this._menupopup.openPopupAtScreen(event.screenX, event.screenY, true); 732 733 event.preventDefault(); 734 }); 735 } 736 737 _initTextbox() { 738 if (this.parentNode.parentNode.localName == "toolbarpaletteitem") { 739 return; 740 } 741 742 this.setAttribute("role", "combobox"); 743 this.setAttribute("aria-owns", this.textbox.popup.id); 744 745 // This overrides the searchParam property in autocomplete.xml. We're 746 // hijacking this property as a vehicle for delivering the privacy 747 // information about the window into the guts of nsSearchSuggestions. 748 // Note that the setter is the same as the parent. We were not sure whether 749 // we can override just the getter. If that proves to be the case, the setter 750 // can be removed. 751 Object.defineProperty(this.textbox, "searchParam", { 752 get() { 753 return ( 754 this.getAttribute("autocompletesearchparam") + 755 (PrivateBrowsingUtils.isWindowPrivate(window) ? "|private" : "") 756 ); 757 }, 758 set(val) { 759 this.setAttribute("autocompletesearchparam", val); 760 }, 761 }); 762 763 Object.defineProperty(this.textbox, "selectedButton", { 764 get() { 765 return this.popup.oneOffButtons.selectedButton; 766 }, 767 set(val) { 768 this.popup.oneOffButtons.selectedButton = val; 769 }, 770 }); 771 772 // This is implemented so that when textbox.value is set directly (e.g., 773 // by tests), the one-off query is updated. 774 this.textbox.onBeforeValueSet = aValue => { 775 if (this.textbox.popup._oneOffButtons) { 776 this.textbox.popup.oneOffButtons.query = aValue; 777 } 778 return aValue; 779 }; 780 781 // Returns true if the event is handled by us, false otherwise. 782 this.textbox.onBeforeHandleKeyDown = aEvent => { 783 if (aEvent.getModifierState("Accel")) { 784 if ( 785 aEvent.keyCode == KeyEvent.DOM_VK_DOWN || 786 aEvent.keyCode == KeyEvent.DOM_VK_UP 787 ) { 788 this.selectEngine(aEvent, aEvent.keyCode == KeyEvent.DOM_VK_DOWN); 789 return true; 790 } 791 return false; 792 } 793 794 if ( 795 (AppConstants.platform == "macosx" && 796 aEvent.keyCode == KeyEvent.DOM_VK_F4) || 797 (aEvent.getModifierState("Alt") && 798 (aEvent.keyCode == KeyEvent.DOM_VK_DOWN || 799 aEvent.keyCode == KeyEvent.DOM_VK_UP)) 800 ) { 801 if (!this.textbox.openSearch()) { 802 aEvent.preventDefault(); 803 aEvent.stopPropagation(); 804 return true; 805 } 806 } 807 808 let popup = this.textbox.popup; 809 let searchIcon = document.querySelector(".searchbar-search-button"); 810 searchIcon.setAttribute("aria-expanded", popup.popupOpen); 811 if (popup.popupOpen) { 812 let suggestionsHidden = popup.richlistbox.hasAttribute("collapsed"); 813 let numItems = suggestionsHidden ? 0 : popup.matchCount; 814 return popup.oneOffButtons.handleKeyDown(aEvent, numItems, true); 815 } else if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) { 816 if (this.textbox.editor.canUndo) { 817 this.textbox.editor.undoAll(); 818 } else { 819 this.textbox.select(); 820 } 821 aEvent.preventDefault(); 822 return true; 823 } 824 return false; 825 }; 826 827 // This method overrides the autocomplete binding's openPopup (essentially 828 // duplicating the logic from the autocomplete popup binding's 829 // openAutocompletePopup method), modifying it so that the popup is aligned with 830 // the inner textbox, but sized to not extend beyond the search bar border. 831 this.textbox.openPopup = () => { 832 // Entering customization mode after the search bar had focus causes 833 // the popup to appear again, due to focus returning after the 834 // hamburger panel closes. Don't open in that spurious event. 835 if (document.documentElement.hasAttribute("customizing")) { 836 return; 837 } 838 839 let popup = this.textbox.popup; 840 let searchIcon = document.querySelector(".searchbar-search-button"); 841 if (!popup.mPopupOpen) { 842 // Initially the panel used for the searchbar (PopupSearchAutoComplete 843 // in browser.xhtml) is hidden to avoid impacting startup / new 844 // window performance. The base binding's openPopup would normally 845 // call the overriden openAutocompletePopup in 846 // browser-search-autocomplete-result-popup binding to unhide the popup, 847 // but since we're overriding openPopup we need to unhide the panel 848 // ourselves. 849 popup.hidden = false; 850 851 // Don't roll up on mouse click in the anchor for the search UI. 852 if (popup.id == "PopupSearchAutoComplete") { 853 popup.setAttribute("norolluponanchor", "true"); 854 } 855 856 popup.mInput = this.textbox; 857 // clear any previous selection, see bugs 400671 and 488357 858 popup.selectedIndex = -1; 859 860 // Ensure the panel has a meaningful initial size and doesn't grow 861 // unconditionally. 862 let { width } = window.windowUtils.getBoundsWithoutFlushing(this); 863 if (popup.oneOffButtons) { 864 // We have a min-width rule on search-panel-one-offs to show at 865 // least 4 buttons, so take that into account here. 866 width = Math.max(width, popup.oneOffButtons.buttonWidth * 4); 867 } 868 869 popup.style.setProperty("--panel-width", width + "px"); 870 popup._invalidate(); 871 popup.openPopup(this, "after_start"); 872 searchIcon.setAttribute("aria-expanded", "true"); 873 } 874 }; 875 876 this.textbox.openSearch = () => { 877 if (!this.textbox.popupOpen) { 878 this.openSuggestionsPanel(); 879 return false; 880 } 881 return true; 882 }; 883 884 this.textbox.handleEnter = event => { 885 // Toggle the open state of the add-engine menu button if it's 886 // selected. We're using handleEnter for this instead of listening 887 // for the command event because a command event isn't fired. 888 if ( 889 this.textbox.selectedButton && 890 this.textbox.selectedButton.getAttribute("anonid") == 891 "addengine-menu-button" 892 ) { 893 this.textbox.selectedButton.open = !this.textbox.selectedButton.open; 894 return true; 895 } 896 // Ignore blank search unless add search engine or 897 // settings button is selected, see bugs 1894910 and 1903608. 898 if ( 899 !this.textbox.value && 900 !( 901 this.textbox.selectedButton?.getAttribute("id") == 902 "searchbar-anon-search-settings" || 903 this.textbox.selectedButton?.classList.contains( 904 "searchbar-engine-one-off-add-engine" 905 ) 906 ) 907 ) { 908 if (event.shiftKey) { 909 let engine = this.textbox.selectedButton?.engine; 910 let { where, params } = this._whereToOpen(event); 911 this.openSearchFormWhere(event, engine, where, params); 912 } 913 return true; 914 } 915 // Otherwise, "call super": do what the autocomplete binding's 916 // handleEnter implementation does. 917 return this.textbox.mController.handleEnter(false, event || null); 918 }; 919 920 // override |onTextEntered| in autocomplete.xml 921 this.textbox.onTextEntered = event => { 922 this.textbox.editor.clearUndoRedo(); 923 924 let engine; 925 let oneOff = this.textbox.selectedButton; 926 if (oneOff) { 927 if (!oneOff.engine) { 928 oneOff.doCommand(); 929 return; 930 } 931 engine = oneOff.engine; 932 } 933 if (this.textbox.popupSelectedIndex != -1) { 934 this.telemetrySelectedIndex = this.textbox.popupSelectedIndex; 935 this.textbox.popupSelectedIndex = -1; 936 } 937 this.handleSearchCommand(event, engine); 938 }; 939 940 this.textbox.onbeforeinput = event => { 941 if (event.data && this._needBrowserFocusAtEnterKeyUp) { 942 // Ignore char key input while processing enter key. 943 event.preventDefault(); 944 } 945 }; 946 947 this.textbox.onkeyup = () => { 948 // Pressing Enter key while pressing Meta key, and next, even when 949 // releasing Enter key before releasing Meta key, the keyup event is not 950 // fired. Therefore, if Enter keydown is detecting, continue the post 951 // processing for Enter key when any keyup event is detected. 952 if (this._needBrowserFocusAtEnterKeyUp) { 953 this._needBrowserFocusAtEnterKeyUp = false; 954 gBrowser.selectedBrowser.focus(); 955 } 956 }; 957 } 958 959 _buildContextMenu() { 960 const raw = ` 961 <menuitem data-l10n-id="text-action-undo" cmd="cmd_undo"/> 962 <menuitem data-l10n-id="text-action-redo" cmd="cmd_redo"/> 963 <menuseparator/> 964 <menuitem data-l10n-id="text-action-cut" cmd="cmd_cut"/> 965 <menuitem data-l10n-id="text-action-copy" cmd="cmd_copy"/> 966 <menuitem data-l10n-id="text-action-paste" cmd="cmd_paste"/> 967 <menuitem class="searchbar-paste-and-search"/> 968 <menuitem data-l10n-id="text-action-delete" cmd="cmd_delete"/> 969 <menuitem data-l10n-id="text-action-select-all" cmd="cmd_selectAll"/> 970 <menuseparator/> 971 <menuitem class="searchbar-clear-history"/> 972 `; 973 974 this._menupopup = this.querySelector(".textbox-contextmenu"); 975 976 let frag = MozXULElement.parseXULToFragment(raw); 977 978 // Insert attributes that come from localized properties 979 this._pasteAndSearchMenuItem = frag.querySelector( 980 ".searchbar-paste-and-search" 981 ); 982 this._pasteAndSearchMenuItem.setAttribute( 983 "label", 984 this._stringBundle.getString("cmd_pasteAndSearch") 985 ); 986 987 let clearHistoryItem = frag.querySelector(".searchbar-clear-history"); 988 clearHistoryItem.setAttribute( 989 "label", 990 this._stringBundle.getString("cmd_clearHistory") 991 ); 992 clearHistoryItem.setAttribute( 993 "accesskey", 994 this._stringBundle.getString("cmd_clearHistory_accesskey") 995 ); 996 997 this._menupopup.appendChild(frag); 998 999 this._menupopup.addEventListener("command", event => { 1000 switch (event.originalTarget) { 1001 case this._pasteAndSearchMenuItem: 1002 this.select(); 1003 goDoCommand("cmd_paste"); 1004 this.handleSearchCommand(event); 1005 break; 1006 case clearHistoryItem: { 1007 let param = this.textbox.getAttribute("autocompletesearchparam"); 1008 lazy.FormHistory.update({ op: "remove", fieldname: param }); 1009 this.textbox.value = ""; 1010 break; 1011 } 1012 default: { 1013 let cmd = event.originalTarget.getAttribute("cmd"); 1014 if (cmd) { 1015 let controller = 1016 document.commandDispatcher.getControllerForCommand(cmd); 1017 controller.doCommand(cmd); 1018 } 1019 break; 1020 } 1021 } 1022 }); 1023 } 1024 } 1025 1026 customElements.define("searchbar", MozSearchbar); 1027 }