ContentSearchParent.sys.mjs (24368B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", 9 BrowserSearchTelemetry: 10 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 11 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 12 DEFAULT_FORM_HISTORY_PARAM: 13 "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs", 14 FormHistory: "resource://gre/modules/FormHistory.sys.mjs", 15 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 16 SearchSuggestionController: 17 "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs", 18 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 19 }); 20 21 const MAX_LOCAL_SUGGESTIONS = 3; 22 const MAX_SUGGESTIONS = 6; 23 const SEARCH_ENGINE_PLACEHOLDER_ICON = 24 "chrome://browser/skin/search-engine-placeholder.png"; 25 26 // Set of all ContentSearch actors, used to broadcast messages to all of them. 27 let gContentSearchActors = new Set(); 28 29 /** 30 * Inbound messages have the following types: 31 * 32 * AddFormHistoryEntry 33 * Adds an entry to the search form history. 34 * data: the entry, a string 35 * GetSuggestions 36 * Retrieves an array of search suggestions given a search string. 37 * data: { engineName, searchString } 38 * GetState 39 * Retrieves the current search engine state. 40 * data: null 41 * GetStrings 42 * Retrieves localized search UI strings. 43 * data: null 44 * ManageEngines 45 * Opens the search engine management window. 46 * data: null 47 * RemoveFormHistoryEntry 48 * Removes an entry from the search form history. 49 * data: the entry, a string 50 * Search 51 * Performs a search. 52 * Any GetSuggestions messages in the queue from the same target will be 53 * cancelled. 54 * data: { engineName, searchString, healthReportKey, searchPurpose } 55 * SetCurrentEngine 56 * Sets the current engine. 57 * data: the name of the engine 58 * SpeculativeConnect 59 * Speculatively connects to an engine. 60 * data: the name of the engine 61 * 62 * Outbound messages have the following types: 63 * 64 * CurrentEngine 65 * Broadcast when the current engine changes. 66 * data: see _currentEngineObj 67 * CurrentState 68 * Broadcast when the current search state changes. 69 * data: see currentStateObj 70 * State 71 * Sent in reply to GetState. 72 * data: see currentStateObj 73 * Strings 74 * Sent in reply to GetStrings 75 * data: Object containing string names and values for the current locale. 76 * Suggestions 77 * Sent in reply to GetSuggestions. 78 * data: see _onMessageGetSuggestions 79 * SuggestionsCancelled 80 * Sent in reply to GetSuggestions when pending GetSuggestions events are 81 * cancelled. 82 * data: null 83 */ 84 85 export let ContentSearch = { 86 initialized: false, 87 88 // Inbound events are queued and processed in FIFO order instead of handling 89 // them immediately, which would result in non-FIFO responses due to the 90 // asynchrononicity added by converting image data URIs to ArrayBuffers. 91 _eventQueue: [], 92 _currentEventPromise: null, 93 94 // This is used to handle search suggestions. It maps xul:browsers to objects 95 // { controller, previousFormHistoryResults }. See _onMessageGetSuggestions. 96 _suggestionMap: new WeakMap(), 97 98 // Resolved when we finish shutting down. 99 _destroyedPromise: null, 100 101 // The current controller and browser in _onMessageGetSuggestions. Allows 102 // fetch cancellation from _cancelSuggestions. 103 _currentSuggestion: null, 104 105 init() { 106 if (!this.initialized) { 107 Services.obs.addObserver(this, "browser-search-engine-modified"); 108 Services.obs.addObserver(this, "shutdown-leaks-before-check"); 109 lazy.UrlbarPrefs.addObserver(this); 110 111 this.initialized = true; 112 } 113 }, 114 115 get searchSuggestionUIStrings() { 116 if (this._searchSuggestionUIStrings) { 117 return this._searchSuggestionUIStrings; 118 } 119 this._searchSuggestionUIStrings = {}; 120 let searchBundle = Services.strings.createBundle( 121 "chrome://browser/locale/search.properties" 122 ); 123 let stringNames = [ 124 "searchHeader", 125 "searchForSomethingWith2", 126 "searchWithHeader", 127 "searchSettings", 128 ]; 129 130 for (let name of stringNames) { 131 this._searchSuggestionUIStrings[name] = 132 searchBundle.GetStringFromName(name); 133 } 134 return this._searchSuggestionUIStrings; 135 }, 136 137 destroy() { 138 if (!this.initialized) { 139 return new Promise(); 140 } 141 142 if (this._destroyedPromise) { 143 return this._destroyedPromise; 144 } 145 146 Services.obs.removeObserver(this, "browser-search-engine-modified"); 147 Services.obs.removeObserver(this, "shutdown-leaks-before-check"); 148 149 this._eventQueue.length = 0; 150 this._destroyedPromise = Promise.resolve(this._currentEventPromise); 151 return this._destroyedPromise; 152 }, 153 154 observe(subj, topic, data) { 155 switch (topic) { 156 case "browser-search-engine-modified": 157 this._eventQueue.push({ 158 type: "Observe", 159 data, 160 }); 161 this._processEventQueue(); 162 break; 163 case "shutdown-leaks-before-check": 164 subj.wrappedJSObject.client.addBlocker( 165 "ContentSearch: Wait until the service is destroyed", 166 () => this.destroy() 167 ); 168 break; 169 } 170 }, 171 172 /** 173 * Observes changes in prefs tracked by UrlbarPrefs. 174 * 175 * @param {string} pref 176 * The name of the pref, relative to `browser.urlbar.` if the pref is 177 * in that branch. 178 */ 179 onPrefChanged(pref) { 180 if (lazy.UrlbarPrefs.shouldHandOffToSearchModePrefs.includes(pref)) { 181 this._eventQueue.push({ 182 type: "Observe", 183 data: "shouldHandOffToSearchMode", 184 }); 185 this._processEventQueue(); 186 } 187 }, 188 189 removeFormHistoryEntry(browser, entry) { 190 let browserData = this._suggestionDataForBrowser(browser); 191 if (browserData?.previousFormHistoryResults) { 192 let result = browserData.previousFormHistoryResults.find( 193 e => e.text == entry 194 ); 195 lazy.FormHistory.update({ 196 op: "remove", 197 fieldname: lazy.DEFAULT_FORM_HISTORY_PARAM, 198 value: entry, 199 guid: result.guid, 200 }).catch(err => 201 console.error("Error removing form history entry: ", err) 202 ); 203 } 204 }, 205 206 performSearch(actor, browser, data) { 207 this._ensureDataHasProperties(data, [ 208 "engineName", 209 "searchString", 210 "healthReportKey", 211 ]); 212 let engine = Services.search.getEngineByName(data.engineName); 213 let submission = engine.getSubmission(data.searchString, ""); 214 let win = browser.ownerGlobal; 215 if (!win) { 216 // The browser may have been closed between the time its content sent the 217 // message and the time we handle it. 218 return; 219 } 220 let where = lazy.BrowserUtils.whereToOpenLink(data.originalEvent); 221 222 // There is a chance that by the time we receive the search message, the user 223 // has switched away from the tab that triggered the search. If, based on the 224 // event, we need to load the search in the same tab that triggered it (i.e. 225 // where === "current"), openUILinkIn will not work because that tab is no 226 // longer the current one. For this case we manually load the URI. 227 if (where === "current") { 228 // Since we're going to load the search in the same browser, blur the search 229 // UI to prevent further interaction before we start loading. 230 this._reply(actor, "Blur"); 231 browser.loadURI(submission.uri, { 232 postData: submission.postData, 233 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 234 { 235 userContextId: 236 win.gBrowser.selectedBrowser.getAttribute("userContextId"), 237 } 238 ), 239 }); 240 } else { 241 let params = { 242 postData: submission.postData, 243 inBackground: Services.prefs.getBoolPref( 244 "browser.tabs.loadInBackground" 245 ), 246 }; 247 win.openTrustedLinkIn(submission.uri.spec, where, params); 248 } 249 lazy.BrowserSearchTelemetry.recordSearch( 250 browser, 251 engine, 252 data.healthReportKey, 253 { 254 selection: data.selection, 255 } 256 ); 257 }, 258 259 async getSuggestions(engineName, searchString, browser) { 260 let engine = Services.search.getEngineByName(engineName); 261 if (!engine) { 262 throw new Error("Unknown engine name: " + engineName); 263 } 264 265 let browserData = this._suggestionDataForBrowser(browser, true); 266 let { controller } = browserData; 267 let ok = lazy.SearchSuggestionController.engineOffersSuggestions(engine); 268 let maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS; 269 let maxRemoteResults = ok ? MAX_SUGGESTIONS : 0; 270 // fetch() rejects its promise if there's a pending request, but since we 271 // process our event queue serially, there's never a pending request. 272 this._currentSuggestion = { controller, browser }; 273 let suggestions = await controller.fetch({ 274 searchString, 275 inPrivateBrowsing: lazy.PrivateBrowsingUtils.isBrowserPrivate(browser), 276 engine, 277 maxLocalResults, 278 maxRemoteResults, 279 }); 280 281 // Simplify results since we do not support rich results in this component. 282 suggestions.local = suggestions.local.map(e => e.value); 283 // We shouldn't show tail suggestions in their full-text form. 284 let nonTailEntries = suggestions.remote.filter( 285 e => !e.matchPrefix && !e.tail 286 ); 287 suggestions.remote = nonTailEntries.map(e => e.value); 288 289 this._currentSuggestion = null; 290 291 // suggestions will be null if the request was cancelled 292 let result = {}; 293 if (!suggestions) { 294 return result; 295 } 296 297 // Keep the form history results so RemoveFormHistoryEntry can remove entries 298 // from it. Keeping only one result isn't foolproof because the client may 299 // try to remove an entry from one set of suggestions after it has requested 300 // more but before it's received them. In that case, the entry may not 301 // appear in the new suggestions. But that should happen rarely. 302 browserData.previousFormHistoryResults = suggestions.formHistoryResults; 303 result = { 304 engineName, 305 term: suggestions.term, 306 local: suggestions.local, 307 remote: suggestions.remote, 308 }; 309 return result; 310 }, 311 312 async addFormHistoryEntry(browser, entry = null) { 313 let isPrivate = false; 314 try { 315 // isBrowserPrivate assumes that the passed-in browser has all the normal 316 // properties, which won't be true if the browser has been destroyed. 317 // That may be the case here due to the asynchronous nature of messaging. 318 isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); 319 } catch (err) { 320 return false; 321 } 322 if ( 323 isPrivate || 324 !entry || 325 entry.value.length > 326 lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH 327 ) { 328 return false; 329 } 330 lazy.FormHistory.update({ 331 op: "bump", 332 fieldname: lazy.DEFAULT_FORM_HISTORY_PARAM, 333 value: entry.value, 334 source: entry.engineName, 335 }).catch(err => console.error("Error adding form history entry: ", err)); 336 return true; 337 }, 338 339 /** 340 * Construct a state object representing the search engine state. 341 * 342 * @returns {object} state 343 */ 344 async currentStateObj() { 345 let state = { 346 engines: [], 347 currentEngine: await this._currentEngineObj(false), 348 currentPrivateEngine: await this._currentEngineObj(true), 349 }; 350 351 for (let engine of await Services.search.getVisibleEngines()) { 352 state.engines.push({ 353 name: engine.name, 354 iconData: await this._getEngineIconURL(engine), 355 hidden: engine.hideOneOffButton, 356 isConfigEngine: engine.isConfigEngine, 357 }); 358 } 359 360 return state; 361 }, 362 363 _processEventQueue() { 364 if (this._currentEventPromise || !this._eventQueue.length) { 365 return; 366 } 367 368 let event = this._eventQueue.shift(); 369 370 this._currentEventPromise = (async () => { 371 try { 372 await this["_on" + event.type](event); 373 } catch (err) { 374 console.error(err); 375 } finally { 376 this._currentEventPromise = null; 377 378 this._processEventQueue(); 379 } 380 })(); 381 }, 382 383 _cancelSuggestions({ actor, browser }) { 384 let cancelled = false; 385 // cancel active suggestion request 386 if ( 387 this._currentSuggestion && 388 this._currentSuggestion.browser === browser 389 ) { 390 this._currentSuggestion.controller.stop(); 391 cancelled = true; 392 } 393 // cancel queued suggestion requests 394 for (let i = 0; i < this._eventQueue.length; i++) { 395 let m = this._eventQueue[i]; 396 if (actor === m.actor && m.name === "GetSuggestions") { 397 this._eventQueue.splice(i, 1); 398 cancelled = true; 399 i--; 400 } 401 } 402 if (cancelled) { 403 this._reply(actor, "SuggestionsCancelled"); 404 } 405 }, 406 407 async _onMessage(eventItem) { 408 let methodName = "_onMessage" + eventItem.name; 409 if (methodName in this) { 410 await this._initService(); 411 await this[methodName](eventItem); 412 eventItem.browser.removeEventListener("SwapDocShells", eventItem, true); 413 } 414 }, 415 416 async _onMessageGetState({ actor }) { 417 let state = await this.currentStateObj(); 418 return this._reply(actor, "State", state); 419 }, 420 421 async _onMessageGetEngine({ actor }) { 422 let state = await this.currentStateObj(); 423 let { usePrivateBrowsing } = actor.browsingContext; 424 return this._reply(actor, "Engine", { 425 isPrivateEngine: usePrivateBrowsing, 426 engine: usePrivateBrowsing 427 ? state.currentPrivateEngine 428 : state.currentEngine, 429 }); 430 }, 431 432 _onMessageGetHandoffSearchModePrefs({ actor }) { 433 this._reply( 434 actor, 435 "HandoffSearchModePrefs", 436 lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") 437 ); 438 }, 439 440 _onMessageGetStrings({ actor }) { 441 this._reply(actor, "Strings", this.searchSuggestionUIStrings); 442 }, 443 444 _onMessageSearch({ actor, browser, data }) { 445 this.performSearch(actor, browser, data); 446 }, 447 448 _onMessageSetCurrentEngine({ data }) { 449 Services.search.setDefault( 450 Services.search.getEngineByName(data), 451 Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR 452 ); 453 }, 454 455 _onMessageManageEngines({ browser }) { 456 browser.ownerGlobal.openPreferences("paneSearch"); 457 }, 458 459 async _onMessageGetSuggestions({ actor, browser, data }) { 460 this._ensureDataHasProperties(data, ["engineName", "searchString"]); 461 let { engineName, searchString } = data; 462 let suggestions = await this.getSuggestions( 463 engineName, 464 searchString, 465 browser 466 ); 467 468 this._reply(actor, "Suggestions", { 469 engineName: data.engineName, 470 searchString: suggestions.term, 471 formHistory: suggestions.local, 472 remote: suggestions.remote, 473 }); 474 }, 475 476 async _onMessageAddFormHistoryEntry({ browser, data: entry }) { 477 await this.addFormHistoryEntry(browser, entry); 478 }, 479 480 _onMessageRemoveFormHistoryEntry({ browser, data: entry }) { 481 this.removeFormHistoryEntry(browser, entry); 482 }, 483 484 _onMessageSpeculativeConnect({ browser, data: engineName }) { 485 let engine = Services.search.getEngineByName(engineName); 486 if (!engine) { 487 throw new Error("Unknown engine name: " + engineName); 488 } 489 if (browser.contentWindow) { 490 engine.speculativeConnect({ 491 window: browser.contentWindow, 492 originAttributes: browser.contentPrincipal.originAttributes, 493 }); 494 } 495 }, 496 497 _onMessageSearchHandoff({ browser, data, actor }) { 498 let win = browser.ownerGlobal; 499 let text = data.text; 500 let urlBar = win.gURLBar; 501 let inPrivateBrowsing = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); 502 let searchEngine = inPrivateBrowsing 503 ? Services.search.defaultPrivateEngine 504 : Services.search.defaultEngine; 505 let isFirstChange = true; 506 507 // It's possible that this is a handoff from about:home / about:newtab, 508 // in which case we want to include the newtab_session_id in our call to 509 // urlBar.handoff. We have to jump through some unfortunate hoops to get 510 // that. 511 let newtabSessionId = null; 512 let newtabActor = 513 browser.browsingContext?.currentWindowGlobal?.getExistingActor( 514 "AboutNewTab" 515 ); 516 if (newtabActor) { 517 const portID = newtabActor.getTabDetails()?.portID; 518 if (portID) { 519 newtabSessionId = lazy.AboutNewTab.activityStream.store.feeds 520 .get("feeds.telemetry") 521 ?.sessions.get(portID)?.session_id; 522 } 523 } 524 525 if (!text) { 526 urlBar.setHiddenFocus(); 527 } else { 528 // Pass the provided text to the awesomebar 529 urlBar.handoff(text, searchEngine, newtabSessionId); 530 isFirstChange = false; 531 } 532 533 let checkFirstChange = () => { 534 // Check if this is the first change since we hidden focused. If it is, 535 // remove hidden focus styles, prepend the search alias and hide the 536 // in-content search. 537 if (isFirstChange) { 538 isFirstChange = false; 539 urlBar.removeHiddenFocus(true); 540 urlBar.handoff("", searchEngine, newtabSessionId); 541 actor.sendAsyncMessage("DisableSearch"); 542 urlBar.removeEventListener("compositionstart", checkFirstChange); 543 urlBar.removeEventListener("paste", checkFirstChange); 544 } 545 }; 546 547 let onKeydown = ev => { 548 // Check if the keydown will cause a value change. 549 if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { 550 checkFirstChange(); 551 } 552 // If the Esc button is pressed, we are done. Show in-content search and cleanup. 553 if (ev.key === "Escape") { 554 onDone(); 555 } 556 }; 557 558 let onDone = ev => { 559 // We are done. Show in-content search again and cleanup. 560 const forceSuppressFocusBorder = ev?.type === "mousedown"; 561 urlBar.removeHiddenFocus(forceSuppressFocusBorder); 562 563 urlBar.removeEventListener("keydown", onKeydown); 564 urlBar.removeEventListener("mousedown", onDone); 565 urlBar.removeEventListener("blur", onDone); 566 urlBar.removeEventListener("compositionstart", checkFirstChange); 567 urlBar.removeEventListener("paste", checkFirstChange); 568 569 actor.sendAsyncMessage("ShowSearch"); 570 }; 571 572 urlBar.addEventListener("keydown", onKeydown); 573 urlBar.addEventListener("mousedown", onDone); 574 urlBar.addEventListener("blur", onDone); 575 urlBar.addEventListener("compositionstart", checkFirstChange); 576 urlBar.addEventListener("paste", checkFirstChange); 577 }, 578 579 async _onObserve(eventItem) { 580 let engine; 581 switch (eventItem.data) { 582 case "engine-default": 583 engine = await this._currentEngineObj(false); 584 this._broadcast("CurrentEngine", engine); 585 break; 586 case "engine-default-private": 587 engine = await this._currentEngineObj(true); 588 this._broadcast("CurrentPrivateEngine", engine); 589 break; 590 case "shouldHandOffToSearchMode": 591 this._broadcast( 592 "HandoffSearchModePrefs", 593 lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") 594 ); 595 break; 596 default: { 597 let state = await this.currentStateObj(); 598 this._broadcast("CurrentState", state); 599 break; 600 } 601 } 602 }, 603 604 _suggestionDataForBrowser(browser, create = false) { 605 let data = this._suggestionMap.get(browser); 606 if (!data && create) { 607 // Since one SearchSuggestionController instance is meant to be used per 608 // autocomplete widget, this means that we assume each xul:browser has at 609 // most one such widget. 610 data = { 611 controller: new lazy.SearchSuggestionController(), 612 }; 613 this._suggestionMap.set(browser, data); 614 } 615 return data; 616 }, 617 618 _reply(actor, type, data) { 619 actor.sendAsyncMessage(type, data); 620 }, 621 622 _broadcast(type, data) { 623 for (let actor of gContentSearchActors) { 624 actor.sendAsyncMessage(type, data); 625 } 626 }, 627 628 async _currentEngineObj(usePrivate) { 629 let engine = 630 Services.search[usePrivate ? "defaultPrivateEngine" : "defaultEngine"]; 631 let obj = { 632 name: engine.name, 633 iconData: await this._getEngineIconURL(engine), 634 isConfigEngine: engine.isConfigEngine, 635 }; 636 return obj; 637 }, 638 639 /** 640 * Used in _getEngineIconURL 641 * 642 * @typedef {object} iconData 643 * @property {ArrayBuffer|string} icon 644 * The icon data in an ArrayBuffer or a placeholder icon string. 645 * @property {string|null} mimeType 646 * The MIME type of the icon. 647 */ 648 649 /** 650 * Converts the engine's icon into a URL or an ArrayBuffer for passing to the 651 * content process. 652 * 653 * @param {nsISearchEngine} engine 654 * The engine to get the icon for. 655 * @returns {string|iconData} 656 * The icon's URL or an iconData object containing the icon data. 657 */ 658 async _getEngineIconURL(engine) { 659 let url = await engine.getIconURL(); 660 if (!url) { 661 return SEARCH_ENGINE_PLACEHOLDER_ICON; 662 } 663 664 // The uri received here can be one of several types: 665 // 1 - moz-extension://[uuid]/path/to/icon.ico 666 // 2 - data:image/x-icon;base64,VERY-LONG-STRING 667 // 3 - blob: 668 // 669 // For moz-extension URIs we can pass the URI to the content process and 670 // use it directly as they can be accessed from there and it is cheaper. 671 // 672 // For blob URIs the content process is a different scope and we can't share 673 // the blob with that scope. Hence we have to create a copy of the data. 674 // 675 // For data: URIs we convert to an ArrayBuffer as that is more optimal for 676 // passing the data across to the content process. This is passed to the 677 // 'icon' field of the return object. The object also receives the 678 // content-type of the URI, which is passed to its 'mimeType' field. 679 if (!url.startsWith("data:") && !url.startsWith("blob:")) { 680 return url; 681 } 682 683 try { 684 const response = await fetch(url); 685 const mimeType = response.headers.get("Content-Type") || ""; 686 const data = await response.arrayBuffer(); 687 return { icon: data, mimeType }; 688 } catch (err) { 689 console.error("Fetch error: ", err); 690 return SEARCH_ENGINE_PLACEHOLDER_ICON; 691 } 692 }, 693 694 _ensureDataHasProperties(data, requiredProperties) { 695 for (let prop of requiredProperties) { 696 if (!(prop in data)) { 697 throw new Error("Message data missing required property: " + prop); 698 } 699 } 700 }, 701 702 _initService() { 703 if (!this._initServicePromise) { 704 this._initServicePromise = Services.search.init(); 705 } 706 return this._initServicePromise; 707 }, 708 }; 709 710 export class ContentSearchParent extends JSWindowActorParent { 711 constructor() { 712 super(); 713 ContentSearch.init(); 714 gContentSearchActors.add(this); 715 } 716 717 didDestroy() { 718 gContentSearchActors.delete(this); 719 } 720 721 receiveMessage(msg) { 722 // Add a temporary event handler that exists only while the message is in 723 // the event queue. If the message's source docshell changes browsers in 724 // the meantime, then we need to update the browser. event.detail will be 725 // the docshell's new parent <xul:browser> element. 726 let browser = this.browsingContext.top.embedderElement; 727 if (!browser) { 728 // The associated browser has gone away, so there's nothing more we can 729 // do here. 730 return; 731 } 732 let eventItem = { 733 type: "Message", 734 name: msg.name, 735 data: msg.data, 736 browser, 737 actor: this, 738 handleEvent: event => { 739 let browserData = ContentSearch._suggestionMap.get(eventItem.browser); 740 if (browserData) { 741 ContentSearch._suggestionMap.delete(eventItem.browser); 742 ContentSearch._suggestionMap.set(event.detail, browserData); 743 } 744 browser.removeEventListener("SwapDocShells", eventItem, true); 745 eventItem.browser = event.detail; 746 eventItem.browser.addEventListener("SwapDocShells", eventItem, true); 747 }, 748 }; 749 browser.addEventListener("SwapDocShells", eventItem, true); 750 751 // Search requests cause cancellation of all Suggestion requests from the 752 // same browser. 753 if (msg.name === "Search") { 754 ContentSearch._cancelSuggestions(eventItem); 755 } 756 757 ContentSearch._eventQueue.push(eventItem); 758 ContentSearch._processEventQueue(); 759 } 760 }