findInPage.js (26603B)
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 /* import-globals-from extensionControlled.js */ 6 /* import-globals-from preferences.js */ 7 8 // A tweak to the standard <button> CE to use textContent on the <label> 9 // inside the button, which allows the text to be highlighted when the user 10 // is searching. 11 12 /** @import MozInputSearch from "chrome://global/content/elements/moz-input-search.mjs" */ 13 14 const MozButtonClass = customElements.get("button"); 15 class HighlightableButton extends MozButtonClass { 16 static get inheritedAttributes() { 17 // @ts-expect-error super is MozButton from toolkit/content/widgets/button.js 18 return Object.assign({}, super.inheritedAttributes, { 19 ".button-text": "text=label,accesskey,crop", 20 }); 21 } 22 } 23 customElements.define("highlightable-button", HighlightableButton, { 24 extends: "button", 25 }); 26 27 var gSearchResultsPane = { 28 /** @type {string} */ 29 query: undefined, 30 listSearchTooltips: new Set(), 31 listSearchMenuitemIndicators: new Set(), 32 /** @type {MozInputSearch} */ 33 searchInput: null, 34 /** @type {HTMLDivElement} */ 35 searchTooltipContainer: null, 36 // A map of DOM Elements to a string of keywords used in search 37 // XXX: We should invalidate this cache on `intl:app-locales-changed` 38 searchKeywords: new WeakMap(), 39 inited: false, 40 41 // A (node -> boolean) map of subitems to be made visible or hidden. 42 subItems: new Map(), 43 44 searchResultsHighlighted: false, 45 46 searchableNodes: new Set([ 47 "button", 48 "label", 49 "description", 50 "menulist", 51 "menuitem", 52 "checkbox", 53 ]), 54 55 init() { 56 if (this.inited) { 57 return; 58 } 59 this.inited = true; 60 this.searchInput = /** @type {MozInputSearch} */ ( 61 document.getElementById("searchInput") 62 ); 63 this.searchTooltipContainer = /** @type {HTMLDivElement} */ ( 64 document.getElementById("search-tooltip-container") 65 ); 66 67 window.addEventListener("resize", () => { 68 this._recomputeTooltipPositions(); 69 }); 70 71 if (!this.searchInput.hidden) { 72 this.searchInput.addEventListener("input", this); 73 window.addEventListener("DOMContentLoaded", () => { 74 this.searchInput.updateComplete.then(() => { 75 this.searchInput.focus(); 76 }); 77 // Initialize other panes in an idle callback. 78 window.requestIdleCallback(() => this.initializeCategories()); 79 }); 80 } 81 ensureScrollPadding(); 82 }, 83 84 /** @param {InputEvent} event */ 85 async handleEvent(event) { 86 // Ensure categories are initialized if idle callback didn't run sooo enough. 87 await this.initializeCategories(); 88 this.searchFunction(event); 89 }, 90 91 /** 92 * This stops the search input from moving, when typing in it 93 * changes which items in the prefs are visible. 94 */ 95 fixInputPosition() { 96 let innerContainer = document.querySelector(".sticky-inner-container"); 97 let width = 98 window.windowUtils.getBoundsWithoutFlushing(innerContainer).width; 99 innerContainer.style.maxWidth = width + "px"; 100 }, 101 102 /** 103 * Check that the text content contains the query string. 104 * 105 * @param String content 106 * the text content to be searched 107 * @param String query 108 * the query string 109 * @returns boolean 110 * true when the text content contains the query string else false 111 */ 112 queryMatchesContent(content, query) { 113 if (!content || !query) { 114 return false; 115 } 116 return content.toLowerCase().includes(query.toLowerCase()); 117 }, 118 119 categoriesInitialized: false, 120 121 /** 122 * Will attempt to initialize all uninitialized categories 123 */ 124 async initializeCategories() { 125 // Initializing all the JS for all the tabs 126 if (!this.categoriesInitialized) { 127 this.categoriesInitialized = true; 128 // Each element of gCategoryInits is a name 129 for (let category of gCategoryInits.values()) { 130 category.init(); 131 } 132 if (document.hasPendingL10nMutations) { 133 await new Promise(r => 134 document.addEventListener("L10nMutationsFinished", r, { once: true }) 135 ); 136 } 137 } 138 }, 139 140 /** 141 * Finds and returns text nodes within node and all descendants. 142 * Iterates through all the siblings of the node object and adds each sibling to an 143 * array if it's a TEXT_NODE, and otherwise recurses to check text nodes within it. 144 * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page 145 * 146 * @param Node nodeObject 147 * DOM element 148 * @returns array of text nodes 149 */ 150 textNodeDescendants(node) { 151 if (!node) { 152 return []; 153 } 154 let all = []; 155 for (node = node.firstChild; node; node = node.nextSibling) { 156 if (node.nodeType === node.TEXT_NODE) { 157 all.push(node); 158 } else { 159 all = all.concat(this.textNodeDescendants(node)); 160 } 161 } 162 return all; 163 }, 164 165 /** 166 * This function is used to find words contained within the text nodes. 167 * We pass in the textNodes because they contain the text to be highlighted. 168 * We pass in the nodeSizes to tell exactly where highlighting need be done. 169 * When creating the range for highlighting, if the nodes are section is split 170 * by an access key, it is important to have the size of each of the nodes summed. 171 * 172 * @param Array textNodes 173 * List of DOM elements 174 * @param Array nodeSizes 175 * Running size of text nodes. This will contain the same number of elements as textNodes. 176 * The first element is the size of first textNode element. 177 * For any nodes after, they will contain the summation of the nodes thus far in the array. 178 * Example: 179 * textNodes = [[This is ], [a], [n example]] 180 * nodeSizes = [[8], [9], [18]] 181 * This is used to determine the offset when highlighting 182 * @param String textSearch 183 * Concatination of textNodes's text content 184 * Example: 185 * textNodes = [[This is ], [a], [n example]] 186 * nodeSizes = "This is an example" 187 * This is used when executing the regular expression 188 * @param String searchPhrase 189 * word or words to search for 190 * @returns boolean 191 * Returns true when atleast one instance of search phrase is found, otherwise false 192 */ 193 highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) { 194 if (!searchPhrase) { 195 return false; 196 } 197 198 let indices = []; 199 let i = -1; 200 while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) { 201 indices.push(i); 202 } 203 204 // Looping through each spot the searchPhrase is found in the concatenated string 205 for (let startValue of indices) { 206 let endValue = startValue + searchPhrase.length; 207 let startNode = null; 208 let endNode = null; 209 let nodeStartIndex = null; 210 211 // Determining the start and end node to highlight from 212 for (let index = 0; index < nodeSizes.length; index++) { 213 let lengthNodes = nodeSizes[index]; 214 // Determining the start node 215 if (!startNode && lengthNodes >= startValue) { 216 startNode = textNodes[index]; 217 nodeStartIndex = index; 218 // Calculating the offset when found query is not in the first node 219 if (index > 0) { 220 startValue -= nodeSizes[index - 1]; 221 } 222 } 223 // Determining the end node 224 if (!endNode && lengthNodes >= endValue) { 225 endNode = textNodes[index]; 226 // Calculating the offset when endNode is different from startNode 227 // or when endNode is not the first node 228 if (index != nodeStartIndex || index > 0) { 229 endValue -= nodeSizes[index - 1]; 230 } 231 } 232 } 233 let range = document.createRange(); 234 range.setStart(startNode, startValue); 235 range.setEnd(endNode, endValue); 236 this.getFindSelection(startNode.ownerGlobal).addRange(range); 237 238 this.searchResultsHighlighted = true; 239 } 240 241 return !!indices.length; 242 }, 243 244 /** 245 * Get the selection instance from given window 246 * 247 * @param Object win 248 * The window object points to frame's window 249 */ 250 getFindSelection(win) { 251 // Yuck. See bug 138068. 252 let docShell = win.docShell; 253 254 let controller = docShell 255 .QueryInterface(Ci.nsIInterfaceRequestor) 256 .getInterface(Ci.nsISelectionDisplay) 257 .QueryInterface(Ci.nsISelectionController); 258 259 let selection = controller.getSelection( 260 Ci.nsISelectionController.SELECTION_FIND 261 ); 262 selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa"); 263 264 return selection; 265 }, 266 267 /** 268 * Shows or hides content according to search input 269 * 270 * @param String event 271 * to search for filted query in 272 */ 273 async searchFunction(event) { 274 let query = event.target.value.trim().toLowerCase(); 275 if (this.query == query) { 276 return; 277 } 278 279 let firstQuery = !this.query && query; 280 let endQuery = !query && this.query; 281 let subQuery = this.query && query.includes(this.query); 282 this.query = query; 283 284 // If there is a query, don't reshow the existing hidden subitems yet 285 // to avoid them flickering into view only to be hidden again by 286 // this next search. 287 this.removeAllSearchIndicators(window, !query.length); 288 289 let srHeader = document.getElementById("header-searchResults"); 290 let noResultsEl = document.getElementById("no-results-message"); 291 if (this.query) { 292 // If this is the first query, fix the search input in place. 293 if (firstQuery) { 294 this.fixInputPosition(); 295 } 296 // Showing the Search Results Tag 297 await gotoPref("paneSearchResults"); 298 srHeader.hidden = false; 299 300 let resultsFound = false; 301 302 // Building the range for highlighted areas 303 let rootPreferencesChildren = [ 304 ...document.querySelectorAll( 305 "#mainPrefPane > *:not([data-hidden-from-search], script, stringbundle)" 306 ), 307 ]; 308 309 if (subQuery) { 310 // Since the previous query is a subset of the current query, 311 // there is no need to check elements that is hidden already. 312 rootPreferencesChildren = rootPreferencesChildren.filter( 313 el => !el.hidden 314 ); 315 } 316 317 // Attach the bindings for all children if they were not already visible. 318 for (let child of rootPreferencesChildren) { 319 if (child.hidden) { 320 child.classList.add("visually-hidden"); 321 child.hidden = false; 322 } 323 } 324 325 let ts = performance.now(); 326 let FRAME_THRESHOLD = 1000 / 60; 327 328 // Showing or Hiding specific section depending on if words in query are found 329 for (let child of rootPreferencesChildren) { 330 if (performance.now() - ts > FRAME_THRESHOLD) { 331 // Creating tooltips for all the instances found 332 for (let anchorNode of this.listSearchTooltips) { 333 this.createSearchTooltip(anchorNode, this.query); 334 } 335 ts = await new Promise(resolve => 336 window.requestAnimationFrame(resolve) 337 ); 338 if (query !== this.query) { 339 return; 340 } 341 } 342 343 if ( 344 !child.classList.contains("header") && 345 !child.classList.contains("subcategory") && 346 (await this.searchWithinNode(child, this.query)) 347 ) { 348 child.classList.remove("visually-hidden"); 349 350 // Show the preceding search-header if one exists. 351 let groupbox = 352 child.closest("groupbox") || child.closest("[data-category]"); 353 let groupHeader = 354 groupbox && groupbox.querySelector(".search-header"); 355 if (groupHeader) { 356 groupHeader.hidden = false; 357 } 358 359 resultsFound = true; 360 } else { 361 child.classList.add("visually-hidden"); 362 } 363 } 364 365 // Hide any subitems that don't match the search term and show 366 // only those that do. 367 if (this.subItems.size) { 368 for (let [subItem, matches] of this.subItems) { 369 subItem.classList.toggle("visually-hidden", !matches); 370 } 371 } 372 373 noResultsEl.hidden = !!resultsFound; 374 noResultsEl.setAttribute("query", this.query); 375 // XXX: This is potentially racy in case where Fluent retranslates the 376 // message and ereases the query within. 377 // The feature is not yet supported, but we should fix for it before 378 // we enable it. See bug 1446389 for details. 379 let msgQueryElem = document.getElementById("sorry-message-query"); 380 msgQueryElem.textContent = this.query; 381 if (resultsFound) { 382 // Creating tooltips for all the instances found 383 for (let anchorNode of this.listSearchTooltips) { 384 this.createSearchTooltip(anchorNode, this.query); 385 } 386 } 387 } else { 388 if (endQuery) { 389 document 390 .querySelector(".sticky-inner-container") 391 .style.removeProperty("max-width"); 392 } 393 noResultsEl.hidden = true; 394 document.getElementById("sorry-message-query").textContent = ""; 395 // Going back to General when cleared 396 await gotoPref("paneGeneral"); 397 srHeader.hidden = true; 398 399 // Hide some special second level headers in normal view 400 for (let element of document.querySelectorAll(".search-header")) { 401 element.hidden = true; 402 } 403 } 404 405 window.dispatchEvent( 406 new CustomEvent("PreferencesSearchCompleted", { detail: query }) 407 ); 408 }, 409 410 /** 411 * Determine if the given element is an anchor tag. 412 * 413 * @param {HTMLElement} el The element. 414 * @returns {boolean} Whether or not the element is an anchor tag. 415 */ 416 _isAnchor(el) { 417 return (el.prefix === null || el.prefix === "html") && el.localName === "a"; 418 }, 419 420 /** 421 * Finding leaf nodes and checking their content for words to search, 422 * It is a recursive function 423 * 424 * @param Node nodeObject 425 * DOM Element 426 * @param String searchPhrase 427 * @returns boolean 428 * Returns true when found in at least one childNode, false otherwise 429 */ 430 async searchWithinNode(nodeObject, searchPhrase) { 431 let matchesFound = false; 432 if ( 433 nodeObject.childElementCount == 0 || 434 (typeof nodeObject.children !== "undefined" && 435 Array.prototype.every.call(nodeObject.children, this._isAnchor)) || 436 this.searchableNodes.has(nodeObject.localName) || 437 (nodeObject.localName?.startsWith("moz-") && 438 nodeObject.localName !== "moz-input-box") 439 ) { 440 let simpleTextNodes = this.textNodeDescendants(nodeObject); 441 if (nodeObject.shadowRoot) { 442 simpleTextNodes.push( 443 ...this.textNodeDescendants(nodeObject.shadowRoot) 444 ); 445 } 446 for (let node of simpleTextNodes) { 447 let result = this.highlightMatches( 448 [node], 449 [node.length], 450 node.textContent.toLowerCase(), 451 searchPhrase 452 ); 453 matchesFound = matchesFound || result; 454 } 455 456 // Collecting data from anonymous content / label / description 457 let nodeSizes = []; 458 let allNodeText = ""; 459 let runningSize = 0; 460 461 let accessKeyTextNodes = []; 462 463 if ( 464 nodeObject.localName == "label" || 465 nodeObject.localName == "description" || 466 nodeObject.localName.startsWith("moz-") 467 ) { 468 accessKeyTextNodes.push(...simpleTextNodes); 469 } 470 471 for (let node of accessKeyTextNodes) { 472 runningSize += node.textContent.length; 473 allNodeText += node.textContent; 474 nodeSizes.push(runningSize); 475 } 476 477 // Access key are presented 478 let complexTextNodesResult = this.highlightMatches( 479 accessKeyTextNodes, 480 nodeSizes, 481 allNodeText.toLowerCase(), 482 searchPhrase 483 ); 484 485 // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text. 486 let labelResult = this.queryMatchesContent( 487 nodeObject.getAttribute("label"), 488 searchPhrase 489 ); 490 491 // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute. 492 // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item. 493 let valueResult = 494 nodeObject.localName !== "menuitem" && nodeObject.localName !== "radio" 495 ? this.queryMatchesContent( 496 nodeObject.getAttribute("value"), 497 searchPhrase 498 ) 499 : false; 500 501 // Searching some elements, such as xul:button, buttons to open subdialogs 502 // using l10n ids. 503 let keywordsResult = 504 nodeObject.hasAttribute("search-l10n-ids") && 505 (await this.matchesSearchL10nIDs(nodeObject, searchPhrase)); 506 507 if (!keywordsResult) { 508 // Searching some elements, such as xul:button, buttons to open subdialogs 509 // using searchkeywords attribute. 510 keywordsResult = 511 !keywordsResult && 512 nodeObject.hasAttribute("searchkeywords") && 513 this.queryMatchesContent( 514 nodeObject.getAttribute("searchkeywords"), 515 searchPhrase 516 ); 517 } 518 519 // Creating tooltips for buttons 520 if ( 521 keywordsResult && 522 (nodeObject instanceof HTMLElement || 523 nodeObject.localName === "button" || 524 nodeObject.localName == "menulist") 525 ) { 526 this.listSearchTooltips.add(nodeObject); 527 } 528 529 if (keywordsResult && nodeObject.localName === "menuitem") { 530 nodeObject.setAttribute("indicator", "true"); 531 this.listSearchMenuitemIndicators.add(nodeObject); 532 let menulist = nodeObject.closest("menulist"); 533 534 menulist.setAttribute("indicator", "true"); 535 this.listSearchMenuitemIndicators.add(menulist); 536 } 537 538 if ( 539 (nodeObject.localName == "menulist" || 540 nodeObject.localName == "menuitem") && 541 (labelResult || valueResult || keywordsResult) 542 ) { 543 nodeObject.setAttribute("highlightable", "true"); 544 } 545 546 matchesFound = 547 matchesFound || 548 complexTextNodesResult || 549 labelResult || 550 valueResult || 551 keywordsResult; 552 } 553 554 // Should not search unselected child nodes of a <xul:deck> element 555 // except the "historyPane" <xul:deck> element. 556 if (nodeObject.localName == "deck" && nodeObject.id != "historyPane") { 557 let index = nodeObject.selectedIndex; 558 if (index != -1) { 559 let result = await this.searchChildNodeIfVisible( 560 nodeObject, 561 index, 562 searchPhrase 563 ); 564 matchesFound = matchesFound || result; 565 } 566 } else { 567 for (let i = 0; i < nodeObject.childNodes.length; i++) { 568 let result = await this.searchChildNodeIfVisible( 569 nodeObject, 570 i, 571 searchPhrase 572 ); 573 matchesFound = matchesFound || result; 574 } 575 } 576 return matchesFound; 577 }, 578 579 /** 580 * Search for a phrase within a child node if it is visible. 581 * 582 * @param Node nodeObject 583 * The parent DOM Element 584 * @param Number index 585 * The index for the childNode 586 * @param String searchPhrase 587 * @returns boolean 588 * Returns true when found the specific childNode, false otherwise 589 */ 590 async searchChildNodeIfVisible(nodeObject, index, searchPhrase) { 591 let result = false; 592 let child = nodeObject.childNodes[index]; 593 if ( 594 !child.hidden && 595 nodeObject.getAttribute("data-hidden-from-search") !== "true" 596 ) { 597 result = await this.searchWithinNode(child, searchPhrase); 598 // Creating tooltips for menulist element 599 if (result && nodeObject.localName === "menulist") { 600 this.listSearchTooltips.add(nodeObject); 601 } 602 603 // If this is a node for an experimental feature option or a Mozilla product item, 604 // add it to the list of subitems. The items that don't match the search term 605 // will be hidden. 606 if ( 607 Element.isInstance(child) && 608 (child.classList.contains("featureGate") || 609 child.classList.contains("mozilla-product-item")) 610 ) { 611 this.subItems.set(child, result); 612 } 613 } 614 return result; 615 }, 616 617 /** 618 * Search for a phrase in l10n messages associated with the element. 619 * 620 * @param Node nodeObject 621 * The parent DOM Element 622 * @param String searchPhrase 623 * @returns boolean 624 * true when the text content contains the query string else false 625 */ 626 async matchesSearchL10nIDs(nodeObject, searchPhrase) { 627 if (!this.searchKeywords.has(nodeObject)) { 628 // The `search-l10n-ids` attribute is a comma-separated list of 629 // l10n ids. It may also uses a dot notation to specify an attribute 630 // of the message to be used. 631 // 632 // Example: "containers-add-button.label, user-context-personal" 633 // 634 // The result is an array of arrays of l10n ids and optionally attribute names. 635 // 636 // Example: [["containers-add-button", "label"], ["user-context-personal"]] 637 const refs = nodeObject 638 .getAttribute("search-l10n-ids") 639 .split(",") 640 .map(s => s.trim().split(".")) 641 .filter(s => !!s[0].length); 642 643 const messages = await document.l10n.formatMessages( 644 refs.map(ref => ({ id: ref[0] })) 645 ); 646 647 // Map the localized messages taking value or a selected attribute and 648 // building a string of concatenated translated strings out of it. 649 let keywords = messages 650 .map((msg, i) => { 651 let [refId, refAttr] = refs[i]; 652 if (!msg) { 653 console.error(`Missing search l10n id "${refId}"`); 654 return null; 655 } 656 if (refAttr) { 657 let attr = 658 msg.attributes && msg.attributes.find(a => a.name === refAttr); 659 if (!attr) { 660 console.error(`Missing search l10n id "${refId}.${refAttr}"`); 661 return null; 662 } 663 if (attr.value === "") { 664 console.error( 665 `Empty value added to search-l10n-ids "${refId}.${refAttr}"` 666 ); 667 } 668 return attr.value; 669 } 670 if (msg.value === "") { 671 console.error(`Empty value added to search-l10n-ids "${refId}"`); 672 } 673 return msg.value; 674 }) 675 .filter(keyword => keyword !== null) 676 .join(" "); 677 678 this.searchKeywords.set(nodeObject, keywords); 679 return this.queryMatchesContent(keywords, searchPhrase); 680 } 681 682 return this.queryMatchesContent( 683 this.searchKeywords.get(nodeObject), 684 searchPhrase 685 ); 686 }, 687 688 /** 689 * Inserting a div structure infront of the DOM element matched textContent. 690 * Then calculation the offsets to position the tooltip in the correct place. 691 * 692 * @param Node anchorNode 693 * DOM Element 694 * @param String query 695 * Word or words that are being searched for 696 */ 697 createSearchTooltip(anchorNode, query) { 698 if (anchorNode.tooltipNode) { 699 return; 700 } 701 let searchTooltip = anchorNode.ownerDocument.createElement("span"); 702 let searchTooltipText = anchorNode.ownerDocument.createElement("span"); 703 searchTooltip.className = "search-tooltip"; 704 searchTooltipText.textContent = query; 705 searchTooltip.appendChild(searchTooltipText); 706 707 // Set tooltipNode property to track corresponded tooltip node. 708 anchorNode.tooltipNode = searchTooltip; 709 anchorNode.parentElement.classList.add("search-tooltip-parent"); 710 this.searchTooltipContainer.append(searchTooltip); 711 712 this._applyTooltipPosition( 713 searchTooltip, 714 this._computeTooltipPosition(anchorNode, searchTooltip) 715 ); 716 }, 717 718 _recomputeTooltipPositions() { 719 let positions = []; 720 for (let anchorNode of this.listSearchTooltips) { 721 let searchTooltip = anchorNode.tooltipNode; 722 if (!searchTooltip) { 723 continue; 724 } 725 let position = this._computeTooltipPosition(anchorNode, searchTooltip); 726 positions.push({ searchTooltip, position }); 727 } 728 for (let { searchTooltip, position } of positions) { 729 this._applyTooltipPosition(searchTooltip, position); 730 } 731 }, 732 733 _applyTooltipPosition(searchTooltip, position) { 734 searchTooltip.style.left = position.left + "px"; 735 searchTooltip.style.top = position.top + "px"; 736 }, 737 738 _computeTooltipPosition(anchorNode, searchTooltip) { 739 // In order to get the up-to-date position of each of the nodes that we're 740 // putting tooltips on, we have to flush layout intentionally. Once 741 // menulists don't use XUL layout we can remove this and use plain CSS to 742 // position them, see bug 1363730. 743 let anchorRect = anchorNode.getBoundingClientRect(); 744 let tooltipContainerRect = 745 this.searchTooltipContainer.getBoundingClientRect(); 746 let tooltipRect = searchTooltip.getBoundingClientRect(); 747 748 let top = anchorRect.top - tooltipContainerRect.top; 749 750 let left; 751 if (anchorRect.left <= tooltipContainerRect.left + 20) { 752 // Left align on anchors that are close to the side of the main content 753 left = 8; 754 } else { 755 // Center tooltips if their anchor is floating off somewhere else 756 left = 757 anchorRect.left - 758 tooltipContainerRect.left + 759 anchorRect.width / 2 - 760 tooltipRect.width / 2; 761 } 762 return { left, top }; 763 }, 764 765 /** 766 * Remove all search indicators. This would be called when switching away from 767 * a search to another preference category. 768 */ 769 removeAllSearchIndicators(window, showSubItems) { 770 if (this.searchResultsHighlighted) { 771 this.getFindSelection(window).removeAllRanges(); 772 this.searchResultsHighlighted = false; 773 } 774 this.removeAllSearchTooltips(); 775 this.removeAllSearchMenuitemIndicators(); 776 777 // Make any previously hidden subitems visible again for the next search. 778 if (showSubItems && this.subItems.size) { 779 for (let subItem of this.subItems.keys()) { 780 subItem.classList.remove("visually-hidden"); 781 } 782 this.subItems.clear(); 783 } 784 }, 785 786 /** 787 * Remove all search tooltips. 788 */ 789 removeAllSearchTooltips() { 790 for (let anchorNode of this.listSearchTooltips) { 791 anchorNode.parentElement.classList.remove("search-tooltip-parent"); 792 if (anchorNode.tooltipNode) { 793 anchorNode.tooltipNode.remove(); 794 } 795 anchorNode.tooltipNode = null; 796 } 797 this.listSearchTooltips.clear(); 798 }, 799 800 /** 801 * Remove all indicators on menuitem. 802 */ 803 removeAllSearchMenuitemIndicators() { 804 for (let node of this.listSearchMenuitemIndicators) { 805 node.removeAttribute("indicator"); 806 } 807 this.listSearchMenuitemIndicators.clear(); 808 }, 809 };