forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			512 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			512 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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 preferences.js */
 | 
						|
 | 
						|
var gSearchResultsPane = {
 | 
						|
  listSearchTooltips: new Set(),
 | 
						|
  listSearchMenuitemIndicators: new Set(),
 | 
						|
  searchInput: null,
 | 
						|
  inited: false,
 | 
						|
 | 
						|
  init() {
 | 
						|
    if (this.inited) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.inited = true;
 | 
						|
    this.searchInput = document.getElementById("searchInput");
 | 
						|
    this.searchInput.hidden = !Services.prefs.getBoolPref("browser.preferences.search");
 | 
						|
    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());
 | 
						|
    }
 | 
						|
    let strings = this.strings;
 | 
						|
    this.searchInput.placeholder = AppConstants.platform == "win" ?
 | 
						|
      strings.getString("searchInput.labelWin") :
 | 
						|
      strings.getString("searchInput.labelUnix");
 | 
						|
  },
 | 
						|
 | 
						|
  handleEvent(event) {
 | 
						|
    // Ensure categories are initialized if idle callback didn't run sooo enough.
 | 
						|
    this.initializeCategories();
 | 
						|
    this.searchFunction(event);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
   */
 | 
						|
  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) {
 | 
						|
          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);
 | 
						|
    }
 | 
						|
 | 
						|
    return indices.length > 0;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.QueryInterface(Ci.nsIInterfaceRequestor)
 | 
						|
                      .getInterface(Ci.nsIWebNavigation)
 | 
						|
                      .QueryInterface(Ci.nsIDocShell);
 | 
						|
 | 
						|
    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;
 | 
						|
  },
 | 
						|
 | 
						|
  get strings() {
 | 
						|
    delete this.strings;
 | 
						|
    return this.strings = document.getElementById("searchResultBundle");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 subQuery = this.query && query.indexOf(this.query) !== -1;
 | 
						|
    this.query = query;
 | 
						|
 | 
						|
    this.getFindSelection(window).removeAllRanges();
 | 
						|
    this.removeAllSearchTooltips();
 | 
						|
    this.removeAllSearchMenuitemIndicators();
 | 
						|
 | 
						|
    // Clear telemetry request if user types very frequently.
 | 
						|
    if (this.telemetryTimer) {
 | 
						|
      clearTimeout(this.telemetryTimer);
 | 
						|
    }
 | 
						|
 | 
						|
    let srHeader = document.getElementById("header-searchResults");
 | 
						|
 | 
						|
    if (this.query) {
 | 
						|
      // Showing the Search Results Tag
 | 
						|
      gotoPref("paneSearchResults");
 | 
						|
 | 
						|
      let resultsFound = false;
 | 
						|
 | 
						|
      // Building the range for highlighted areas
 | 
						|
      let rootPreferencesChildren = document
 | 
						|
        .querySelectorAll("#mainPrefPane > *:not([data-hidden-from-search])");
 | 
						|
 | 
						|
      // Show all second level headers in search result
 | 
						|
      for (let element of document.querySelectorAll("caption.search-header")) {
 | 
						|
        element.hidden = false;
 | 
						|
      }
 | 
						|
 | 
						|
      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 =
 | 
						|
          Array.prototype.filter.call(rootPreferencesChildren, el => !el.hidden);
 | 
						|
      }
 | 
						|
 | 
						|
      // Mark all the children to check be visible to bind JS, Access Keys, etc,
 | 
						|
      // but don't really show them by setting their visibility to hidden in CSS.
 | 
						|
      for (let i = 0; i < rootPreferencesChildren.length; i++) {
 | 
						|
        rootPreferencesChildren[i].hidden = false;
 | 
						|
        rootPreferencesChildren[i].classList.add("visually-hidden");
 | 
						|
      }
 | 
						|
 | 
						|
      let ts = performance.now();
 | 
						|
      let FRAME_THRESHOLD = 1000 / 60;
 | 
						|
 | 
						|
      // Showing or Hiding specific section depending on if words in query are found
 | 
						|
      for (let i = 0; i < rootPreferencesChildren.length; i++) {
 | 
						|
        if (performance.now() - ts > FRAME_THRESHOLD) {
 | 
						|
          // Creating tooltips for all the instances found
 | 
						|
          for (let anchorNode of this.listSearchTooltips) {
 | 
						|
            this.createSearchTooltip(anchorNode, this.query);
 | 
						|
          }
 | 
						|
          // It hides Search Results header so turning it on
 | 
						|
          srHeader.hidden = false;
 | 
						|
          srHeader.classList.remove("visually-hidden");
 | 
						|
          ts = await new Promise(resolve => window.requestAnimationFrame(resolve));
 | 
						|
          if (query !== this.query) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        rootPreferencesChildren[i].classList.remove("visually-hidden");
 | 
						|
        if (!rootPreferencesChildren[i].classList.contains("header") &&
 | 
						|
            !rootPreferencesChildren[i].classList.contains("subcategory") &&
 | 
						|
            !rootPreferencesChildren[i].classList.contains("no-results-message") &&
 | 
						|
            this.searchWithinNode(rootPreferencesChildren[i], this.query)) {
 | 
						|
          rootPreferencesChildren[i].hidden = false;
 | 
						|
          resultsFound = true;
 | 
						|
        } else {
 | 
						|
          rootPreferencesChildren[i].hidden = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      // It hides Search Results header so turning it on
 | 
						|
      srHeader.hidden = false;
 | 
						|
      srHeader.classList.remove("visually-hidden");
 | 
						|
 | 
						|
      if (!resultsFound) {
 | 
						|
        let noResultsEl = document.querySelector(".no-results-message");
 | 
						|
        noResultsEl.setAttribute("query", this.query);
 | 
						|
        noResultsEl.hidden = false;
 | 
						|
 | 
						|
        let strings = this.strings;
 | 
						|
 | 
						|
        document.getElementById("sorry-message").textContent = AppConstants.platform == "win" ?
 | 
						|
          strings.getFormattedString("searchResults.sorryMessageWin", [this.query]) :
 | 
						|
          strings.getFormattedString("searchResults.sorryMessageUnix", [this.query]);
 | 
						|
        let helpUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "preferences";
 | 
						|
        let brandName = document.getElementById("bundleBrand").getString("brandShortName");
 | 
						|
        // eslint-disable-next-line no-unsanitized/property
 | 
						|
        document.getElementById("need-help").innerHTML =
 | 
						|
          strings.getFormattedString("searchResults.needHelp2", [helpUrl, brandName]);
 | 
						|
      } else {
 | 
						|
        // 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 {
 | 
						|
      document.getElementById("sorry-message").textContent = "";
 | 
						|
      // Going back to General when cleared
 | 
						|
      gotoPref("paneGeneral");
 | 
						|
 | 
						|
      // Hide some special second level headers in normal view
 | 
						|
      for (let element of document.querySelectorAll("caption.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
 | 
						|
   */
 | 
						|
  searchWithinNode(nodeObject, searchPhrase) {
 | 
						|
    let matchesFound = false;
 | 
						|
    if (nodeObject.childElementCount == 0 ||
 | 
						|
        nodeObject.tagName == "label" ||
 | 
						|
        nodeObject.tagName == "description" ||
 | 
						|
        nodeObject.tagName == "menulist") {
 | 
						|
      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 boxObject / label / description
 | 
						|
      let nodeSizes = [];
 | 
						|
      let allNodeText = "";
 | 
						|
      let runningSize = 0;
 | 
						|
      let accessKeyTextNodes = this.textNodeDescendants(nodeObject.boxObject);
 | 
						|
 | 
						|
      if (nodeObject.tagName == "label" || nodeObject.tagName == "description") {
 | 
						|
        accessKeyTextNodes.push(...this.textNodeDescendants(nodeObject));
 | 
						|
      }
 | 
						|
 | 
						|
      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" ?
 | 
						|
        this.queryMatchesContent(nodeObject.getAttribute("value"), searchPhrase) : false;
 | 
						|
 | 
						|
      // Searching some elements, such as xul:button, buttons to open subdialogs.
 | 
						|
      let keywordsResult = 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 == "button" ||
 | 
						|
           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 = this.searchChildNodeIfVisible(nodeObject, index, searchPhrase);
 | 
						|
        matchesFound = matchesFound || result;
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      for (let i = 0; i < nodeObject.childNodes.length; i++) {
 | 
						|
        let result = 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
 | 
						|
   */
 | 
						|
  searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
 | 
						|
    let result = false;
 | 
						|
    if (!nodeObject.childNodes[index].hidden && nodeObject.getAttribute("data-hidden-from-search") !== "true") {
 | 
						|
      result = this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
 | 
						|
      // Creating tooltips for menulist element
 | 
						|
      if (result && nodeObject.tagName === "menulist") {
 | 
						|
        this.listSearchTooltips.add(nodeObject);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.setAttribute("class", "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.calculateTooltipPosition(anchorNode);
 | 
						|
  },
 | 
						|
 | 
						|
  calculateTooltipPosition(anchorNode) {
 | 
						|
    let searchTooltip = anchorNode.tooltipNode;
 | 
						|
    // 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, and that
 | 
						|
    // this is the result of a XUL limitation (bug 1363730).
 | 
						|
    let tooltipRect = searchTooltip.getBoundingClientRect();
 | 
						|
    searchTooltip.style.setProperty("left", `calc(50% - ${(tooltipRect.width / 2)}px)`);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove all search tooltips that were created.
 | 
						|
   */
 | 
						|
  removeAllSearchTooltips() {
 | 
						|
    let searchTooltips = Array.from(document.querySelectorAll(".search-tooltip"));
 | 
						|
    for (let searchTooltip of searchTooltips) {
 | 
						|
      searchTooltip.parentElement.classList.remove("search-tooltip-parent");
 | 
						|
      searchTooltip.remove();
 | 
						|
    }
 | 
						|
    for (let anchorNode of this.listSearchTooltips) {
 | 
						|
      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();
 | 
						|
  }
 | 
						|
};
 |