зеркало из https://github.com/mozilla/gecko-dev.git
771 строка
25 KiB
JavaScript
771 строка
25 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 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,
|
|
|
|
searchableNodes: new Set([
|
|
"button",
|
|
"label",
|
|
"description",
|
|
"menulist",
|
|
"menuitem",
|
|
"checkbox",
|
|
]),
|
|
|
|
init() {
|
|
if (this.inited) {
|
|
return;
|
|
}
|
|
this.inited = true;
|
|
this.searchInput = document.getElementById("searchInput");
|
|
|
|
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 category of gCategoryInits.values()) {
|
|
category.init();
|
|
}
|
|
if (document.hasPendingL10nMutations) {
|
|
await new Promise(r =>
|
|
document.addEventListener("L10nMutationsFinished", r, { once: true })
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Finds and returns text nodes within node and all descendants.
|
|
* Iterates through all the siblings of the node object and adds each sibling to an
|
|
* array if it's a TEXT_NODE, and otherwise recurses to check text nodes within it.
|
|
* 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);
|
|
|
|
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") || child.closest("[data-category]");
|
|
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);
|
|
}
|
|
}
|
|
} 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 ||
|
|
this.searchableNodes.has(nodeObject.localName) ||
|
|
(nodeObject.localName?.startsWith("moz-") &&
|
|
nodeObject.localName !== "moz-input-box")
|
|
) {
|
|
let simpleTextNodes = this.textNodeDescendants(nodeObject);
|
|
if (nodeObject.shadowRoot) {
|
|
simpleTextNodes.push(
|
|
...this.textNodeDescendants(nodeObject.shadowRoot)
|
|
);
|
|
}
|
|
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.localName == "label" ||
|
|
nodeObject.localName == "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.localName !== "menuitem" && nodeObject.localName !== "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.localName === "button" ||
|
|
nodeObject.localName == "menulist")
|
|
) {
|
|
this.listSearchTooltips.add(nodeObject);
|
|
}
|
|
|
|
if (keywordsResult && nodeObject.localName === "menuitem") {
|
|
nodeObject.setAttribute("indicator", "true");
|
|
this.listSearchMenuitemIndicators.add(nodeObject);
|
|
let menulist = nodeObject.closest("menulist");
|
|
|
|
menulist.setAttribute("indicator", "true");
|
|
this.listSearchMenuitemIndicators.add(menulist);
|
|
}
|
|
|
|
if (
|
|
(nodeObject.localName == "menulist" ||
|
|
nodeObject.localName == "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.localName == "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.localName === "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();
|
|
},
|
|
};
|