summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/findInPage.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/preferences/findInPage.js772
1 files changed, 772 insertions, 0 deletions
diff --git a/browser/components/preferences/findInPage.js b/browser/components/preferences/findInPage.js
new file mode 100644
index 0000000000..26b6f23846
--- /dev/null
+++ b/browser/components/preferences/findInPage.js
@@ -0,0 +1,772 @@
+/* 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/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+
+// A tweak to the standard <button> CE to use textContent on the <label>
+// inside the button, which allows the text to be highlighted when the user
+// is searching.
+
+const MozButton = customElements.get("button");
+class HighlightableButton extends MozButton {
+ static get inheritedAttributes() {
+ return Object.assign({}, super.inheritedAttributes, {
+ ".button-text": "text=label,accesskey,crop",
+ });
+ }
+}
+customElements.define("highlightable-button", HighlightableButton, {
+ extends: "button",
+});
+
+var gSearchResultsPane = {
+ listSearchTooltips: new Set(),
+ listSearchMenuitemIndicators: new Set(),
+ searchInput: null,
+ // A map of DOM Elements to a string of keywords used in search
+ // XXX: We should invalidate this cache on `intl:app-locales-changed`
+ searchKeywords: new WeakMap(),
+ inited: false,
+
+ // A (node -> boolean) map of subitems to be made visible or hidden.
+ subItems: new Map(),
+
+ searchResultsHighlighted: false,
+
+ init() {
+ if (this.inited) {
+ return;
+ }
+ this.inited = true;
+ this.searchInput = document.getElementById("searchInput");
+ this.searchInput.hidden = !Services.prefs.getBoolPref(
+ "browser.preferences.search"
+ );
+
+ window.addEventListener("resize", () => {
+ this._recomputeTooltipPositions();
+ });
+
+ if (!this.searchInput.hidden) {
+ this.searchInput.addEventListener("input", this);
+ this.searchInput.addEventListener("command", this);
+ window.addEventListener("DOMContentLoaded", () => {
+ this.searchInput.focus();
+ // Initialize other panes in an idle callback.
+ window.requestIdleCallback(() => this.initializeCategories());
+ });
+ }
+ ensureScrollPadding();
+ },
+
+ async handleEvent(event) {
+ // Ensure categories are initialized if idle callback didn't run sooo enough.
+ await this.initializeCategories();
+ this.searchFunction(event);
+ },
+
+ /**
+ * This stops the search input from moving, when typing in it
+ * changes which items in the prefs are visible.
+ */
+ fixInputPosition() {
+ let innerContainer = document.querySelector(".sticky-inner-container");
+ let width =
+ window.windowUtils.getBoundsWithoutFlushing(innerContainer).width;
+ innerContainer.style.maxWidth = width + "px";
+ },
+
+ /**
+ * Check that the text content contains the query string.
+ *
+ * @param String content
+ * the text content to be searched
+ * @param String query
+ * the query string
+ * @returns boolean
+ * true when the text content contains the query string else false
+ */
+ queryMatchesContent(content, query) {
+ if (!content || !query) {
+ return false;
+ }
+ return content.toLowerCase().includes(query.toLowerCase());
+ },
+
+ categoriesInitialized: false,
+
+ /**
+ * Will attempt to initialize all uninitialized categories
+ */
+ async initializeCategories() {
+ // Initializing all the JS for all the tabs
+ if (!this.categoriesInitialized) {
+ this.categoriesInitialized = true;
+ // Each element of gCategoryInits is a name
+ for (let [, /* name */ category] of gCategoryInits) {
+ if (!category.inited) {
+ await category.init();
+ }
+ }
+ }
+ },
+
+ /**
+ * Finds and returns text nodes within node and all descendants
+ * Iterates through all the sibilings of the node object and adds the sibilings
+ * to an array if sibiling is a TEXT_NODE else checks the text nodes with in current node
+ * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page
+ *
+ * @param Node nodeObject
+ * DOM element
+ * @returns array of text nodes
+ */
+ textNodeDescendants(node) {
+ if (!node) {
+ return [];
+ }
+ let all = [];
+ for (node = node.firstChild; node; node = node.nextSibling) {
+ if (node.nodeType === node.TEXT_NODE) {
+ all.push(node);
+ } else {
+ all = all.concat(this.textNodeDescendants(node));
+ }
+ }
+ return all;
+ },
+
+ /**
+ * This function is used to find words contained within the text nodes.
+ * We pass in the textNodes because they contain the text to be highlighted.
+ * We pass in the nodeSizes to tell exactly where highlighting need be done.
+ * When creating the range for highlighting, if the nodes are section is split
+ * by an access key, it is important to have the size of each of the nodes summed.
+ * @param Array textNodes
+ * List of DOM elements
+ * @param Array nodeSizes
+ * Running size of text nodes. This will contain the same number of elements as textNodes.
+ * The first element is the size of first textNode element.
+ * For any nodes after, they will contain the summation of the nodes thus far in the array.
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = [[8], [9], [18]]
+ * This is used to determine the offset when highlighting
+ * @param String textSearch
+ * Concatination of textNodes's text content
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = "This is an example"
+ * This is used when executing the regular expression
+ * @param String searchPhrase
+ * word or words to search for
+ * @returns boolean
+ * Returns true when atleast one instance of search phrase is found, otherwise false
+ */
+ highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) {
+ if (!searchPhrase) {
+ return false;
+ }
+
+ let indices = [];
+ let i = -1;
+ while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) {
+ indices.push(i);
+ }
+
+ // Looping through each spot the searchPhrase is found in the concatenated string
+ for (let startValue of indices) {
+ let endValue = startValue + searchPhrase.length;
+ let startNode = null;
+ let endNode = null;
+ let nodeStartIndex = null;
+
+ // Determining the start and end node to highlight from
+ for (let index = 0; index < nodeSizes.length; index++) {
+ let lengthNodes = nodeSizes[index];
+ // Determining the start node
+ if (!startNode && lengthNodes >= startValue) {
+ startNode = textNodes[index];
+ nodeStartIndex = index;
+ // Calculating the offset when found query is not in the first node
+ if (index > 0) {
+ startValue -= nodeSizes[index - 1];
+ }
+ }
+ // Determining the end node
+ if (!endNode && lengthNodes >= endValue) {
+ endNode = textNodes[index];
+ // Calculating the offset when endNode is different from startNode
+ // or when endNode is not the first node
+ if (index != nodeStartIndex || index > 0) {
+ endValue -= nodeSizes[index - 1];
+ }
+ }
+ }
+ let range = document.createRange();
+ range.setStart(startNode, startValue);
+ range.setEnd(endNode, endValue);
+ this.getFindSelection(startNode.ownerGlobal).addRange(range);
+
+ this.searchResultsHighlighted = true;
+ }
+
+ return !!indices.length;
+ },
+
+ /**
+ * Get the selection instance from given window
+ *
+ * @param Object win
+ * The window object points to frame's window
+ */
+ getFindSelection(win) {
+ // Yuck. See bug 138068.
+ let docShell = win.docShell;
+
+ let controller = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+
+ let selection = controller.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa");
+
+ return selection;
+ },
+
+ /**
+ * Shows or hides content according to search input
+ *
+ * @param String event
+ * to search for filted query in
+ */
+ async searchFunction(event) {
+ let query = event.target.value.trim().toLowerCase();
+ if (this.query == query) {
+ return;
+ }
+
+ let firstQuery = !this.query && query;
+ let endQuery = !query && this.query;
+ let subQuery = this.query && query.includes(this.query);
+ this.query = query;
+
+ // If there is a query, don't reshow the existing hidden subitems yet
+ // to avoid them flickering into view only to be hidden again by
+ // this next search.
+ this.removeAllSearchIndicators(window, !query.length);
+
+ // Clear telemetry request if user types very frequently.
+ if (this.telemetryTimer) {
+ clearTimeout(this.telemetryTimer);
+ }
+
+ let srHeader = document.getElementById("header-searchResults");
+ let noResultsEl = document.getElementById("no-results-message");
+ if (this.query) {
+ // If this is the first query, fix the search input in place.
+ if (firstQuery) {
+ this.fixInputPosition();
+ }
+ // Showing the Search Results Tag
+ await gotoPref("paneSearchResults");
+ srHeader.hidden = false;
+
+ let resultsFound = false;
+
+ // Building the range for highlighted areas
+ let rootPreferencesChildren = [
+ ...document.querySelectorAll(
+ "#mainPrefPane > *:not([data-hidden-from-search], script, stringbundle)"
+ ),
+ ];
+
+ if (subQuery) {
+ // Since the previous query is a subset of the current query,
+ // there is no need to check elements that is hidden already.
+ rootPreferencesChildren = rootPreferencesChildren.filter(
+ el => !el.hidden
+ );
+ }
+
+ // Attach the bindings for all children if they were not already visible.
+ for (let child of rootPreferencesChildren) {
+ if (child.hidden) {
+ child.classList.add("visually-hidden");
+ child.hidden = false;
+ }
+ }
+
+ let ts = performance.now();
+ let FRAME_THRESHOLD = 1000 / 60;
+
+ // Showing or Hiding specific section depending on if words in query are found
+ for (let child of rootPreferencesChildren) {
+ if (performance.now() - ts > FRAME_THRESHOLD) {
+ // Creating tooltips for all the instances found
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+ ts = await new Promise(resolve =>
+ window.requestAnimationFrame(resolve)
+ );
+ if (query !== this.query) {
+ return;
+ }
+ }
+
+ if (
+ !child.classList.contains("header") &&
+ !child.classList.contains("subcategory") &&
+ (await this.searchWithinNode(child, this.query))
+ ) {
+ child.classList.remove("visually-hidden");
+
+ // Show the preceding search-header if one exists.
+ let groupbox = child.closest("groupbox");
+ let groupHeader =
+ groupbox && groupbox.querySelector(".search-header");
+ if (groupHeader) {
+ groupHeader.hidden = false;
+ }
+
+ resultsFound = true;
+ } else {
+ child.classList.add("visually-hidden");
+ }
+ }
+
+ // Hide any subitems that don't match the search term and show
+ // only those that do.
+ if (this.subItems.size) {
+ for (let [subItem, matches] of this.subItems) {
+ subItem.classList.toggle("visually-hidden", !matches);
+ }
+ }
+
+ noResultsEl.hidden = !!resultsFound;
+ noResultsEl.setAttribute("query", this.query);
+ // XXX: This is potentially racy in case where Fluent retranslates the
+ // message and ereases the query within.
+ // The feature is not yet supported, but we should fix for it before
+ // we enable it. See bug 1446389 for details.
+ let msgQueryElem = document.getElementById("sorry-message-query");
+ msgQueryElem.textContent = this.query;
+ if (resultsFound) {
+ // Creating tooltips for all the instances found
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+
+ // Implant search telemetry probe after user stops typing for a while
+ if (this.query.length >= 2) {
+ this.telemetryTimer = setTimeout(() => {
+ Services.telemetry.keyedScalarAdd(
+ "preferences.search_query",
+ this.query,
+ 1
+ );
+ }, 1000);
+ }
+ }
+ } else {
+ if (endQuery) {
+ document
+ .querySelector(".sticky-inner-container")
+ .style.removeProperty("max-width");
+ }
+ noResultsEl.hidden = true;
+ document.getElementById("sorry-message-query").textContent = "";
+ // Going back to General when cleared
+ await gotoPref("paneGeneral");
+ srHeader.hidden = true;
+
+ // Hide some special second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("PreferencesSearchCompleted", { detail: query })
+ );
+ },
+
+ /**
+ * Finding leaf nodes and checking their content for words to search,
+ * It is a recursive function
+ *
+ * @param Node nodeObject
+ * DOM Element
+ * @param String searchPhrase
+ * @returns boolean
+ * Returns true when found in at least one childNode, false otherwise
+ */
+ async searchWithinNode(nodeObject, searchPhrase) {
+ let matchesFound = false;
+ if (
+ nodeObject.childElementCount == 0 ||
+ nodeObject.tagName == "button" ||
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description" ||
+ nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem"
+ ) {
+ let simpleTextNodes = this.textNodeDescendants(nodeObject);
+ for (let node of simpleTextNodes) {
+ let result = this.highlightMatches(
+ [node],
+ [node.length],
+ node.textContent.toLowerCase(),
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+
+ // Collecting data from anonymous content / label / description
+ let nodeSizes = [];
+ let allNodeText = "";
+ let runningSize = 0;
+
+ let accessKeyTextNodes = [];
+
+ if (
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description"
+ ) {
+ accessKeyTextNodes.push(...simpleTextNodes);
+ }
+
+ for (let node of accessKeyTextNodes) {
+ runningSize += node.textContent.length;
+ allNodeText += node.textContent;
+ nodeSizes.push(runningSize);
+ }
+
+ // Access key are presented
+ let complexTextNodesResult = this.highlightMatches(
+ accessKeyTextNodes,
+ nodeSizes,
+ allNodeText.toLowerCase(),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
+ let labelResult = this.queryMatchesContent(
+ nodeObject.getAttribute("label"),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
+ // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item.
+ let valueResult =
+ nodeObject.tagName !== "menuitem" && nodeObject.tagName !== "radio"
+ ? this.queryMatchesContent(
+ nodeObject.getAttribute("value"),
+ searchPhrase
+ )
+ : false;
+
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using l10n ids.
+ let keywordsResult =
+ nodeObject.hasAttribute("search-l10n-ids") &&
+ (await this.matchesSearchL10nIDs(nodeObject, searchPhrase));
+
+ if (!keywordsResult) {
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using searchkeywords attribute.
+ keywordsResult =
+ !keywordsResult &&
+ nodeObject.hasAttribute("searchkeywords") &&
+ this.queryMatchesContent(
+ nodeObject.getAttribute("searchkeywords"),
+ searchPhrase
+ );
+ }
+
+ // Creating tooltips for buttons
+ if (
+ keywordsResult &&
+ (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")
+ ) {
+ this.listSearchTooltips.add(nodeObject);
+ }
+
+ if (keywordsResult && nodeObject.tagName === "menuitem") {
+ nodeObject.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(nodeObject);
+ let menulist = nodeObject.closest("menulist");
+
+ menulist.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(menulist);
+ }
+
+ if (
+ (nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem") &&
+ (labelResult || valueResult || keywordsResult)
+ ) {
+ nodeObject.setAttribute("highlightable", "true");
+ }
+
+ matchesFound =
+ matchesFound ||
+ complexTextNodesResult ||
+ labelResult ||
+ valueResult ||
+ keywordsResult;
+ }
+
+ // Should not search unselected child nodes of a <xul:deck> element
+ // except the "historyPane" <xul:deck> element.
+ if (nodeObject.tagName == "deck" && nodeObject.id != "historyPane") {
+ let index = nodeObject.selectedIndex;
+ if (index != -1) {
+ let result = await this.searchChildNodeIfVisible(
+ nodeObject,
+ index,
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+ } else {
+ for (let i = 0; i < nodeObject.childNodes.length; i++) {
+ let result = await this.searchChildNodeIfVisible(
+ nodeObject,
+ i,
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+ }
+ return matchesFound;
+ },
+
+ /**
+ * Search for a phrase within a child node if it is visible.
+ *
+ * @param Node nodeObject
+ * The parent DOM Element
+ * @param Number index
+ * The index for the childNode
+ * @param String searchPhrase
+ * @returns boolean
+ * Returns true when found the specific childNode, false otherwise
+ */
+ async searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
+ let result = false;
+ let child = nodeObject.childNodes[index];
+ if (
+ !child.hidden &&
+ nodeObject.getAttribute("data-hidden-from-search") !== "true"
+ ) {
+ result = await this.searchWithinNode(child, searchPhrase);
+ // Creating tooltips for menulist element
+ if (result && nodeObject.tagName === "menulist") {
+ this.listSearchTooltips.add(nodeObject);
+ }
+
+ // If this is a node for an experimental feature option or a Mozilla product item,
+ // add it to the list of subitems. The items that don't match the search term
+ // will be hidden.
+ if (
+ Element.isInstance(child) &&
+ (child.classList.contains("featureGate") ||
+ child.classList.contains("mozilla-product-item"))
+ ) {
+ this.subItems.set(child, result);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Search for a phrase in l10n messages associated with the element.
+ *
+ * @param Node nodeObject
+ * The parent DOM Element
+ * @param String searchPhrase
+ * @returns boolean
+ * true when the text content contains the query string else false
+ */
+ async matchesSearchL10nIDs(nodeObject, searchPhrase) {
+ if (!this.searchKeywords.has(nodeObject)) {
+ // The `search-l10n-ids` attribute is a comma-separated list of
+ // l10n ids. It may also uses a dot notation to specify an attribute
+ // of the message to be used.
+ //
+ // Example: "containers-add-button.label, user-context-personal"
+ //
+ // The result is an array of arrays of l10n ids and optionally attribute names.
+ //
+ // Example: [["containers-add-button", "label"], ["user-context-personal"]]
+ const refs = nodeObject
+ .getAttribute("search-l10n-ids")
+ .split(",")
+ .map(s => s.trim().split("."))
+ .filter(s => !!s[0].length);
+
+ const messages = await document.l10n.formatMessages(
+ refs.map(ref => ({ id: ref[0] }))
+ );
+
+ // Map the localized messages taking value or a selected attribute and
+ // building a string of concatenated translated strings out of it.
+ let keywords = messages
+ .map((msg, i) => {
+ let [refId, refAttr] = refs[i];
+ if (!msg) {
+ console.error(`Missing search l10n id "${refId}"`);
+ return null;
+ }
+ if (refAttr) {
+ let attr =
+ msg.attributes && msg.attributes.find(a => a.name === refAttr);
+ if (!attr) {
+ console.error(`Missing search l10n id "${refId}.${refAttr}"`);
+ return null;
+ }
+ if (attr.value === "") {
+ console.error(
+ `Empty value added to search-l10n-ids "${refId}.${refAttr}"`
+ );
+ }
+ return attr.value;
+ }
+ if (msg.value === "") {
+ console.error(`Empty value added to search-l10n-ids "${refId}"`);
+ }
+ return msg.value;
+ })
+ .filter(keyword => keyword !== null)
+ .join(" ");
+
+ this.searchKeywords.set(nodeObject, keywords);
+ return this.queryMatchesContent(keywords, searchPhrase);
+ }
+
+ return this.queryMatchesContent(
+ this.searchKeywords.get(nodeObject),
+ searchPhrase
+ );
+ },
+
+ /**
+ * Inserting a div structure infront of the DOM element matched textContent.
+ * Then calculation the offsets to position the tooltip in the correct place.
+ *
+ * @param Node anchorNode
+ * DOM Element
+ * @param String query
+ * Word or words that are being searched for
+ */
+ createSearchTooltip(anchorNode, query) {
+ if (anchorNode.tooltipNode) {
+ return;
+ }
+ let searchTooltip = anchorNode.ownerDocument.createElement("span");
+ let searchTooltipText = anchorNode.ownerDocument.createElement("span");
+ searchTooltip.className = "search-tooltip";
+ searchTooltipText.textContent = query;
+ searchTooltip.appendChild(searchTooltipText);
+
+ // Set tooltipNode property to track corresponded tooltip node.
+ anchorNode.tooltipNode = searchTooltip;
+ anchorNode.parentElement.classList.add("search-tooltip-parent");
+ anchorNode.parentElement.appendChild(searchTooltip);
+
+ this._applyTooltipPosition(
+ searchTooltip,
+ this._computeTooltipPosition(anchorNode, searchTooltip)
+ );
+ },
+
+ _recomputeTooltipPositions() {
+ let positions = [];
+ for (let anchorNode of this.listSearchTooltips) {
+ let searchTooltip = anchorNode.tooltipNode;
+ if (!searchTooltip) {
+ continue;
+ }
+ let position = this._computeTooltipPosition(anchorNode, searchTooltip);
+ positions.push({ searchTooltip, position });
+ }
+ for (let { searchTooltip, position } of positions) {
+ this._applyTooltipPosition(searchTooltip, position);
+ }
+ },
+
+ _applyTooltipPosition(searchTooltip, position) {
+ searchTooltip.style.left = position.left + "px";
+ searchTooltip.style.top = position.top + "px";
+ },
+
+ _computeTooltipPosition(anchorNode, searchTooltip) {
+ // In order to get the up-to-date position of each of the nodes that we're
+ // putting tooltips on, we have to flush layout intentionally. Once
+ // menulists don't use XUL layout we can remove this and use plain CSS to
+ // position them, see bug 1363730.
+ let anchorRect = anchorNode.getBoundingClientRect();
+ let containerRect = anchorNode.parentElement.getBoundingClientRect();
+ let tooltipRect = searchTooltip.getBoundingClientRect();
+
+ let left =
+ anchorRect.left -
+ containerRect.left +
+ anchorRect.width / 2 -
+ tooltipRect.width / 2;
+ let top = anchorRect.top - containerRect.top;
+ return { left, top };
+ },
+
+ /**
+ * Remove all search indicators. This would be called when switching away from
+ * a search to another preference category.
+ */
+ removeAllSearchIndicators(window, showSubItems) {
+ if (this.searchResultsHighlighted) {
+ this.getFindSelection(window).removeAllRanges();
+ this.searchResultsHighlighted = false;
+ }
+ this.removeAllSearchTooltips();
+ this.removeAllSearchMenuitemIndicators();
+
+ // Make any previously hidden subitems visible again for the next search.
+ if (showSubItems && this.subItems.size) {
+ for (let subItem of this.subItems.keys()) {
+ subItem.classList.remove("visually-hidden");
+ }
+ this.subItems.clear();
+ }
+ },
+
+ /**
+ * Remove all search tooltips.
+ */
+ removeAllSearchTooltips() {
+ for (let anchorNode of this.listSearchTooltips) {
+ anchorNode.parentElement.classList.remove("search-tooltip-parent");
+ if (anchorNode.tooltipNode) {
+ anchorNode.tooltipNode.remove();
+ }
+ anchorNode.tooltipNode = null;
+ }
+ this.listSearchTooltips.clear();
+ },
+
+ /**
+ * Remove all indicators on menuitem.
+ */
+ removeAllSearchMenuitemIndicators() {
+ for (let node of this.listSearchMenuitemIndicators) {
+ node.removeAttribute("indicator");
+ }
+ this.listSearchMenuitemIndicators.clear();
+ },
+};