autocomplete-popup.js (10593B)
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 // Wrap in a block to prevent leaking to window scope. 8 { 9 const lazy = {}; 10 ChromeUtils.defineESModuleGetters(lazy, { 11 BrowserSearchTelemetry: 12 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 13 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 14 SearchOneOffs: "moz-src:///browser/components/search/SearchOneOffs.sys.mjs", 15 }); 16 17 /** 18 * A richlistbox popup custom element for for a browser search autocomplete 19 * widget. 20 */ 21 class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup { 22 constructor() { 23 super(); 24 25 this.addEventListener("popupshowing", () => { 26 // First handle deciding if we are showing the reduced version of the 27 // popup containing only the preferences button. We do this if the 28 // glass icon has been clicked if the text field is empty. 29 if (this.searchbar.hasAttribute("showonlysettings")) { 30 this.searchbar.removeAttribute("showonlysettings"); 31 this.setAttribute("showonlysettings", "true"); 32 33 // Setting this with an xbl-inherited attribute gets overridden the 34 // second time the user clicks the glass icon for some reason... 35 this.richlistbox.collapsed = true; 36 } else { 37 this.removeAttribute("showonlysettings"); 38 // Uncollapse as long as we have a view which has >= 1 row. 39 // The autocomplete binding itself will take care of uncollapsing later, 40 // if we currently have no rows but end up having some in the future 41 // when the search string changes 42 this.richlistbox.collapsed = this.matchCount == 0; 43 } 44 45 // Show the current default engine in the top header of the panel. 46 this.updateHeader().catch(console.error); 47 48 this._oneOffButtons.addEventListener( 49 "SelectedOneOffButtonChanged", 50 this 51 ); 52 }); 53 54 this.addEventListener("popuphiding", () => { 55 this._oneOffButtons.removeEventListener( 56 "SelectedOneOffButtonChanged", 57 this 58 ); 59 }); 60 61 /** 62 * This handles clicks on the topmost "Foo Search" header in the 63 * popup (hbox.search-panel-header]). 64 */ 65 this.addEventListener("click", event => { 66 if (event.button == 2) { 67 // Ignore right clicks. 68 return; 69 } 70 let button = event.originalTarget; 71 let engine = button.parentNode.engine; 72 if (!engine) { 73 return; 74 } 75 if (this.searchbar.value) { 76 this.oneOffButtons.handleSearchCommand(event, engine); 77 } else if (event.shiftKey) { 78 this.openSearchForm(event, engine); 79 } 80 }); 81 82 this._bundle = null; 83 } 84 85 static get inheritedAttributes() { 86 return { 87 ".search-panel-current-engine": "showonlysettings", 88 ".searchbar-engine-image": "src", 89 }; 90 } 91 92 // We override this because even though we have a shadow root, we want our 93 // inheritance to be done on the light tree. 94 getElementForAttrInheritance(selector) { 95 return this.querySelector(selector); 96 } 97 98 initialize() { 99 super.initialize(); 100 this.initializeAttributeInheritance(); 101 102 this._searchOneOffsContainer = this.querySelector(".search-one-offs"); 103 this._searchbarEngine = this.querySelector(".search-panel-header"); 104 this._searchbarEngineName = this.querySelector(".searchbar-engine-name"); 105 this._oneOffButtons = new lazy.SearchOneOffs( 106 this._searchOneOffsContainer 107 ); 108 this._searchbar = document.getElementById("searchbar"); 109 } 110 111 get oneOffButtons() { 112 if (!this._oneOffButtons) { 113 this.initialize(); 114 } 115 return this._oneOffButtons; 116 } 117 118 static get markup() { 119 return ` 120 <hbox class="search-panel-header search-panel-current-engine"> 121 <image class="searchbar-engine-image"/> 122 <label class="searchbar-engine-name" flex="1" crop="end" role="presentation"/> 123 </hbox> 124 <menuseparator class="searchbar-separator"/> 125 <richlistbox class="autocomplete-richlistbox search-panel-tree"/> 126 <menuseparator class="searchbar-separator"/> 127 <hbox class="search-one-offs" is_searchbar="true"/> 128 `; 129 } 130 131 get searchOneOffsContainer() { 132 if (!this._searchOneOffsContainer) { 133 this.initialize(); 134 } 135 return this._searchOneOffsContainer; 136 } 137 138 get searchbarEngine() { 139 if (!this._searchbarEngine) { 140 this.initialize(); 141 } 142 return this._searchbarEngine; 143 } 144 145 get searchbarEngineName() { 146 if (!this._searchbarEngineName) { 147 this.initialize(); 148 } 149 return this._searchbarEngineName; 150 } 151 152 get searchbar() { 153 if (!this._searchbar) { 154 this.initialize(); 155 } 156 return this._searchbar; 157 } 158 159 get bundle() { 160 if (!this._bundle) { 161 const kBundleURI = "chrome://browser/locale/search.properties"; 162 this._bundle = Services.strings.createBundle(kBundleURI); 163 } 164 return this._bundle; 165 } 166 167 openAutocompletePopup(aInput, aElement) { 168 // initially the panel is hidden 169 // to avoid impacting startup / new window performance 170 aInput.popup.hidden = false; 171 172 // this method is defined on the base binding 173 this._openAutocompletePopup(aInput, aElement); 174 } 175 176 onPopupClick(aEvent) { 177 // Ignore all right-clicks 178 if (aEvent.button == 2) { 179 return; 180 } 181 182 this.searchbar.telemetrySelectedIndex = this.selectedIndex; 183 184 // Check for unmodified left-click, and use default behavior 185 if ( 186 aEvent.button == 0 && 187 !aEvent.shiftKey && 188 !aEvent.ctrlKey && 189 !aEvent.altKey && 190 !aEvent.metaKey 191 ) { 192 this.input.controller.handleEnter(true, aEvent); 193 return; 194 } 195 196 // Check for middle-click or modified clicks on the search bar 197 lazy.BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod( 198 aEvent, 199 this.selectedIndex 200 ); 201 202 // Handle search bar popup clicks 203 let search = this.input.controller.getValueAt(this.selectedIndex); 204 205 // open the search results according to the clicking subtlety 206 let where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true); 207 let params = {}; 208 209 // But open ctrl/cmd clicks on autocomplete items in a new background tab. 210 let modifier = 211 AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; 212 if ( 213 where == "tab" && 214 MouseEvent.isInstance(aEvent) && 215 (aEvent.button == 1 || modifier) 216 ) { 217 params.inBackground = true; 218 } 219 220 // leave the popup open for background tab loads 221 if (!(where == "tab" && params.inBackground)) { 222 // close the autocomplete popup and revert the entered search term 223 this.closePopup(); 224 this.input.controller.handleEscape(); 225 } 226 227 this.searchbar.doSearch(search, where, null, params); 228 if (where == "tab" && params.inBackground) { 229 this.searchbar.focus(); 230 } else { 231 this.searchbar.value = search; 232 } 233 } 234 235 /** 236 * @type {string} 237 * The current engine name being displayed in updateHeader. 238 */ 239 #currentEngineName; 240 241 /** 242 * Updates the header of the pop-up with the search engine name and icon. 243 * 244 * @param {nsISearchEngine} [engine] 245 * The engine to use, if not specified falls back to the default engine. 246 */ 247 async updateHeader(engine) { 248 if (!engine) { 249 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 250 engine = await Services.search.getDefaultPrivate(); 251 } else { 252 engine = await Services.search.getDefault(); 253 } 254 } 255 this.#currentEngineName = engine.name; 256 257 let uri = await engine.getIconURL(); 258 259 // If the engine name has changed since we started loading, this means 260 // that getIconURL probably took a long time and we had an update in 261 // the meantime. Hence we skip updating to avoid displaying the wrong 262 // thing. 263 if (engine.name != this.#currentEngineName) { 264 return; 265 } 266 267 if (uri) { 268 this.setAttribute("src", uri); 269 } else { 270 // If the default has just been changed to a provider without icon, 271 // avoid showing the icon of the previous default provider. 272 this.removeAttribute("src"); 273 } 274 275 let headerText = this.bundle.formatStringFromName("searchHeader", [ 276 engine.name, 277 ]); 278 this.searchbarEngineName.setAttribute("value", headerText); 279 this.searchbarEngine.engine = engine; 280 } 281 282 /** 283 * This is called when a one-off is clicked and when "search in new tab" 284 * is selected from a one-off context menu. 285 * 286 * @param {Event} event 287 * The event that triggered the search. 288 * @param {nsISearchEngine} engine 289 * The search engine being used for the search. 290 * @param {string} where 291 * Where the search should be opened (current tab, new tab, window etc). 292 * @param {object} params 293 * The parameters associated with opening the search. 294 */ 295 handleOneOffSearch(event, engine, where, params) { 296 this.searchbar.handleSearchCommandWhere(event, engine, where, params); 297 } 298 299 openSearchForm(event, engine, forceNewTab = false) { 300 let { where, params } = this.oneOffButtons._whereToOpen( 301 event, 302 forceNewTab 303 ); 304 this.searchbar.openSearchFormWhere(event, engine, where, params); 305 } 306 307 /** 308 * Passes DOM events for the popup to the _on_<event type> methods. 309 * 310 * @param {Event} event 311 * DOM event from the <popup>. 312 */ 313 handleEvent(event) { 314 let methodName = "_on_" + event.type; 315 if (methodName in this) { 316 this[methodName](event); 317 } else { 318 throw new Error("Unrecognized UrlbarView event: " + event.type); 319 } 320 } 321 _on_SelectedOneOffButtonChanged() { 322 let engine = 323 this.oneOffButtons.selectedButton && 324 this.oneOffButtons.selectedButton.engine; 325 this.updateHeader(engine).catch(console.error); 326 } 327 } 328 329 customElements.define( 330 "search-autocomplete-richlistbox-popup", 331 MozSearchAutocompleteRichlistboxPopup, 332 { 333 extends: "panel", 334 } 335 ); 336 }