/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; // Wrap in a block to prevent leaking to window scope. { ChromeUtils.defineESModuleGetters(this, { SearchOneOffs: "resource:///modules/SearchOneOffs.sys.mjs", }); /** * A richlistbox popup custom element for for a browser search autocomplete * widget. */ class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup { constructor() { super(); this.addEventListener("popupshowing", event => { // First handle deciding if we are showing the reduced version of the // popup containing only the preferences button. We do this if the // glass icon has been clicked if the text field is empty. if (this.searchbar.hasAttribute("showonlysettings")) { this.searchbar.removeAttribute("showonlysettings"); this.setAttribute("showonlysettings", "true"); // Setting this with an xbl-inherited attribute gets overridden the // second time the user clicks the glass icon for some reason... this.richlistbox.collapsed = true; } else { this.removeAttribute("showonlysettings"); // Uncollapse as long as we have a view which has >= 1 row. // The autocomplete binding itself will take care of uncollapsing later, // if we currently have no rows but end up having some in the future // when the search string changes this.richlistbox.collapsed = this.matchCount == 0; } // Show the current default engine in the top header of the panel. this.updateHeader().catch(console.error); this._oneOffButtons.addEventListener( "SelectedOneOffButtonChanged", this ); }); this.addEventListener("popuphiding", event => { this._oneOffButtons.removeEventListener( "SelectedOneOffButtonChanged", this ); }); /** * This handles clicks on the topmost "Foo Search" header in the * popup (hbox.search-panel-header]). */ this.addEventListener("click", event => { if (event.button == 2) { // Ignore right clicks. return; } let button = event.originalTarget; let engine = button.parentNode.engine; if (!engine) { return; } this.oneOffButtons.handleSearchCommand(event, engine); }); this._bundle = null; } static get inheritedAttributes() { return { ".search-panel-current-engine": "showonlysettings", ".searchbar-engine-image": "src", }; } // We override this because even though we have a shadow root, we want our // inheritance to be done on the light tree. getElementForAttrInheritance(selector) { return this.querySelector(selector); } initialize() { super.initialize(); this.initializeAttributeInheritance(); this._searchOneOffsContainer = this.querySelector(".search-one-offs"); this._searchbarEngine = this.querySelector(".search-panel-header"); this._searchbarEngineName = this.querySelector(".searchbar-engine-name"); this._oneOffButtons = new SearchOneOffs(this._searchOneOffsContainer); this._searchbar = document.getElementById("searchbar"); } get oneOffButtons() { if (!this._oneOffButtons) { this.initialize(); } return this._oneOffButtons; } static get markup() { return ` `; } get searchOneOffsContainer() { if (!this._searchOneOffsContainer) { this.initialize(); } return this._searchOneOffsContainer; } get searchbarEngine() { if (!this._searchbarEngine) { this.initialize(); } return this._searchbarEngine; } get searchbarEngineName() { if (!this._searchbarEngineName) { this.initialize(); } return this._searchbarEngineName; } get searchbar() { if (!this._searchbar) { this.initialize(); } return this._searchbar; } get bundle() { if (!this._bundle) { const kBundleURI = "chrome://browser/locale/search.properties"; this._bundle = Services.strings.createBundle(kBundleURI); } return this._bundle; } openAutocompletePopup(aInput, aElement) { // initially the panel is hidden // to avoid impacting startup / new window performance aInput.popup.hidden = false; // this method is defined on the base binding this._openAutocompletePopup(aInput, aElement); } onPopupClick(aEvent) { // Ignore all right-clicks if (aEvent.button == 2) { return; } this.searchbar.telemetrySelectedIndex = this.selectedIndex; // Check for unmodified left-click, and use default behavior if ( aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && !aEvent.altKey && !aEvent.metaKey ) { this.input.controller.handleEnter(true, aEvent); return; } // Check for middle-click or modified clicks on the search bar BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod( aEvent, "searchbar", this.selectedIndex ); // Handle search bar popup clicks let search = this.input.controller.getValueAt(this.selectedIndex); // open the search results according to the clicking subtlety let where = whereToOpenLink(aEvent, false, true); let params = {}; // But open ctrl/cmd clicks on autocomplete items in a new background tab. let modifier = AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; if ( where == "tab" && MouseEvent.isInstance(aEvent) && (aEvent.button == 1 || modifier) ) { params.inBackground = true; } // leave the popup open for background tab loads if (!(where == "tab" && params.inBackground)) { // close the autocomplete popup and revert the entered search term this.closePopup(); this.input.controller.handleEscape(); } this.searchbar.doSearch(search, where, null, params); if (where == "tab" && params.inBackground) { this.searchbar.focus(); } else { this.searchbar.value = search; } } async updateHeader(engine) { if (!engine) { if (PrivateBrowsingUtils.isWindowPrivate(window)) { engine = await Services.search.getDefaultPrivate(); } else { engine = await Services.search.getDefault(); } } let uri = engine.iconURI; if (uri) { this.setAttribute("src", uri.spec); } else { // If the default has just been changed to a provider without icon, // avoid showing the icon of the previous default provider. this.removeAttribute("src"); } let headerText = this.bundle.formatStringFromName("searchHeader", [ engine.name, ]); this.searchbarEngineName.setAttribute("value", headerText); this.searchbarEngine.engine = engine; } /** * This is called when a one-off is clicked and when "search in new tab" * is selected from a one-off context menu. */ /* eslint-disable-next-line valid-jsdoc */ handleOneOffSearch(event, engine, where, params) { this.searchbar.handleSearchCommandWhere(event, engine, where, params); } /** * Passes DOM events for the popup to the _on_ methods. * * @param {Event} event * DOM event from the . */ handleEvent(event) { let methodName = "_on_" + event.type; if (methodName in this) { this[methodName](event); } else { throw new Error("Unrecognized UrlbarView event: " + event.type); } } _on_SelectedOneOffButtonChanged() { let engine = this.oneOffButtons.selectedButton && this.oneOffButtons.selectedButton.engine; this.updateHeader(engine).catch(console.error); } } customElements.define( "search-autocomplete-richlistbox-popup", MozSearchAutocompleteRichlistboxPopup, { extends: "panel", } ); }