diff options
Diffstat (limited to 'browser/components/search/content/autocomplete-popup.js')
-rw-r--r-- | browser/components/search/content/autocomplete-popup.js | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/browser/components/search/content/autocomplete-popup.js b/browser/components/search/content/autocomplete-popup.js new file mode 100644 index 0000000000..7732f2bbb2 --- /dev/null +++ b/browser/components/search/content/autocomplete-popup.js @@ -0,0 +1,289 @@ +/* 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 ` + <hbox class="search-panel-header search-panel-current-engine"> + <image class="searchbar-engine-image"/> + <label class="searchbar-engine-name" flex="1" crop="end" role="presentation"/> + </hbox> + <menuseparator class="searchbar-separator"/> + <richlistbox class="autocomplete-richlistbox search-panel-tree"/> + <menuseparator class="searchbar-separator"/> + <hbox class="search-one-offs" is_searchbar="true"/> + `; + } + + 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_<event type> methods. + * + * @param {Event} event + * DOM event from the <popup>. + */ + 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", + } + ); +} |