summaryrefslogtreecommitdiffstats
path: root/browser/components/search/content/autocomplete-popup.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/content/autocomplete-popup.js')
-rw-r--r--browser/components/search/content/autocomplete-popup.js289
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",
+ }
+ );
+}