Bug 1420761 - Handle new localization API in Preferences Search. r=jaws
MozReview-Commit-ID: 8J1siQtFn1t
--- a/browser/components/preferences/in-content/findInPage.js
+++ b/browser/components/preferences/in-content/findInPage.js
@@ -4,16 +4,19 @@
/* import-globals-from extensionControlled.js */
/* import-globals-from preferences.js */
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,
init() {
if (this.inited) {
return;
}
this.inited = true;
this.searchInput = document.getElementById("searchInput");
@@ -259,17 +262,17 @@ var gSearchResultsPane = {
ts = await new Promise(resolve => window.requestAnimationFrame(resolve));
if (query !== this.query) {
return;
}
}
if (!child.classList.contains("header") &&
!child.classList.contains("subcategory") &&
- this.searchWithinNode(child, this.query)) {
+ await this.searchWithinNode(child, this.query)) {
child.hidden = false;
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;
@@ -322,17 +325,17 @@ var gSearchResultsPane = {
* 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) {
+ async 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);
@@ -361,18 +364,30 @@ var gSearchResultsPane = {
// 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);
+ // 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");
@@ -393,22 +408,22 @@ var gSearchResultsPane = {
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);
+ let result = await 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);
+ 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.
@@ -416,29 +431,79 @@ var gSearchResultsPane = {
* @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) {
+ async 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);
+ result = await this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
// Creating tooltips for menulist element
if (result && nodeObject.tagName === "menulist") {
this.listSearchTooltips.add(nodeObject);
}
}
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 > 0);
+
+ const messages = await document.l10n.formatMessages(refs.map(ref => [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) => {
+ if (msg === null) {
+ console.warn(`Missing search l10n id "${refs[i][0]}"`);
+ return null;
+ }
+ if (refs[i][1]) {
+ let attr = msg.attrs.find(a => a.name === refs[i][1]);
+ if (attr) {
+ return attr.value;
+ }
+ return null;
+ }
+ 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
*/
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
@@ -138,43 +138,46 @@ add_task(async function search_for_passw
/**
* Test for if nothing is found
*/
add_task(async function search_with_nothing_found() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
+ let sorryMsgQueryEl = gBrowser.contentDocument.getElementById("sorry-message-query");
is_element_hidden(noResultsEl, "Should not be in search results yet");
// Performs search
let searchInput = gBrowser.contentDocument.getElementById("searchInput");
is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
"Search input should be focused when visiting preferences");
let query = "coach";
let searchCompletedPromise = BrowserTestUtils.waitForEvent(
gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
EventUtils.sendString(query);
await searchCompletedPromise;
is_element_visible(noResultsEl, "Should be in search results");
+ is(sorryMsgQueryEl.textContent, query, "sorry-message-query should contain the query");
// Takes search off
searchCompletedPromise = BrowserTestUtils.waitForEvent(
gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == "");
let count = query.length;
while (count--) {
EventUtils.sendKey("BACK_SPACE");
}
await searchCompletedPromise;
is_element_hidden(noResultsEl, "Should not be in search results");
+ is(sorryMsgQueryEl.textContent.length, 0, "sorry-message-query should be empty");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
/**
* Test for if we go back to general tab after search case
*/
add_task(async function exiting_search_reverts_to_general_pane() {
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
@@ -60,8 +60,69 @@ add_task(async function() {
EventUtils.sendString(query);
await searchCompletedPromise;
let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
is_element_visible(noResultsEl, "Should be reporting no results");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
+
+
+/**
+ * Test that we search using `search-l10n-ids`.
+ *
+ * The test uses element `browserContainersSettings` and
+ * l10n id `language-and-appearance-header` and expects the element
+ * to be matched on the first word from the l10n id value ("Language" in en-US).
+ */
+add_task(async function() {
+ let l10nId = "language-and-appearance-header";
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+ // First, lets make sure that the element is not matched without
+ // `search-l10n-ids`.
+ {
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+
+ is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences");
+
+ ok(!bcsElem.getAttribute("search-l10n-ids").includes(l10nId),
+ "browserContainersSettings element should not contain the l10n id here.");
+
+ let query = "Language";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ is_element_hidden(bcsElem, "browserContainersSettings should not be in search results");
+ }
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Now, let's add the l10n id to the element and perform the same search again.
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+ {
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences");
+
+ let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+ bcsElem.setAttribute("search-l10n-ids", l10nId);
+
+ let query = "Language";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ is_element_visible(bcsElem, "browserContainersSettings should be in search results");
+ }
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});