Commit afe3dffe authored by Dan Beam's avatar Dan Beam Committed by Commit Bot

Settings/Print Preview: make search support diacritics

Fixed: 719764

Change-Id: If0ce2a66ca805e69902a2c79f1c674b3ee88c010
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1244550
Commit-Queue: Dan Beam <dbeam@chromium.org>
Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Cr-Commit-Position: refs/heads/master@{#728131}
parent c55653d4
......@@ -4,7 +4,7 @@
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {createEmptySearchBubble, highlight} from 'chrome://resources/js/search_highlight_utils.m.js';
import {createEmptySearchBubble, highlight, Range, stripDiacritics} from 'chrome://resources/js/search_highlight_utils.m.js';
/**
* @param {!HTMLElement} element The element to update. Element should have a
......@@ -28,13 +28,19 @@ export function updateHighlights(element, query, bubbles) {
return;
}
const textContent = node.nodeValue.trim();
if (textContent.length === 0) {
const textContent = node.nodeValue;
if (textContent.trim().length === 0) {
return;
}
const matches = textContent.match(query);
if (matches) {
const strippedText = stripDiacritics(textContent);
/** @type {!Array<!Range>} */
const ranges = [];
for (let match; match = query.exec(strippedText);) {
ranges.push({start: match.index, length: match[0].length});
}
if (ranges.length > 0) {
// Don't highlight <select> nodes, yellow rectangles can't be
// displayed within an <option>.
if (node.parentNode.nodeName === 'OPTION') {
......@@ -48,14 +54,14 @@ export function updateHighlights(element, query, bubbles) {
const bubble = createEmptySearchBubble(
/** @type {!Node} */ (assert(node.parentNode.parentNode)),
/* horizontallyCenter= */ false);
const numHits = matches.length + (bubbles.get(bubble) || 0);
const numHits = ranges.length + (bubbles.get(bubble) || 0);
bubbles.set(bubble, numHits);
const msgName = numHits === 1 ? 'searchResultBubbleText' :
'searchResultsBubbleText';
bubble.firstChild.textContent =
loadTimeData.getStringF(msgName, numHits);
} else {
highlights.push(highlight(node, textContent.split(query)));
highlights.push(highlight(node, ranges));
}
}
});
......
......@@ -9,6 +9,7 @@ import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import './print_preview_shared_css.js';
import {CrSearchFieldBehavior} from 'chrome://resources/cr_elements/cr_search_field/cr_search_field_behavior.m.js';
import {stripDiacritics} from 'chrome://resources/js/search_highlight_utils.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
/** @type {!RegExp} */
......@@ -39,7 +40,7 @@ Polymer({
* The last search query.
* @private {string}
*/
lastString_: '',
lastQuery_: '',
/** @return {!CrInputElement} */
getSearchInput: function() {
......@@ -55,15 +56,15 @@ Polymer({
* @private
*/
onSearchChanged_: function(e) {
const safeQueryString = e.detail.trim().replace(SANITIZE_REGEX, '\\$&');
if (safeQueryString === this.lastString_) {
const strippedQuery = stripDiacritics(e.detail.trim());
const safeQuery = strippedQuery.replace(SANITIZE_REGEX, '\\$&');
if (safeQuery === this.lastQuery_) {
return;
}
this.lastString_ = safeQueryString;
this.searchQuery = safeQueryString.length > 0 ?
new RegExp(`(${safeQueryString})`, 'ig') :
null;
this.lastQuery_ = safeQuery;
this.searchQuery =
safeQuery.length > 0 ? new RegExp(`(${safeQuery})`, 'ig') : null;
},
/** @private */
......
......@@ -91,16 +91,22 @@ cr.define('settings', function() {
}
if (node.nodeType == Node.TEXT_NODE) {
const textContent = node.nodeValue.trim();
if (textContent.length == 0) {
const textContent = node.nodeValue;
if (textContent.trim().length === 0) {
return;
}
const matches = textContent.match(request.regExp);
if (matches) {
const strippedText =
cr.search_highlight_utils.stripDiacritics(textContent);
const ranges = [];
for (let match; match = request.regExp.exec(strippedText);) {
ranges.push({start: match.index, length: match[0].length});
}
if (ranges.length > 0) {
foundMatches = true;
revealParentSection_(
node, /*numResults=*/ matches.length, request.bubbles);
node, /*numResults=*/ ranges.length, request.bubbles);
if (node.parentNode.nodeName === 'OPTION') {
const select = node.parentNode.parentNode;
......@@ -116,14 +122,14 @@ cr.define('settings', function() {
}
showBubble_(
select, /*numResults=*/ matches.length, request.bubbles,
select, /*numResults=*/ ranges.length, request.bubbles,
/*horizontallyCenter=*/ true);
} else {
request.addTextObserver(node);
highlights.push(cr.search_highlight_utils.highlight(
node, textContent.split(request.regExp)));
highlights.push(cr.search_highlight_utils.highlight(node, ranges));
}
}
// Returning early since TEXT_NODE nodes never have children.
return;
}
......@@ -503,14 +509,14 @@ cr.define('settings', function() {
*/
generateRegExp_() {
let regExp = null;
// Generate search text by escaping any characters that would be
// problematic for regular expressions.
const searchText = this.rawQuery_.trim().replace(SANITIZE_REGEX, '\\$&');
if (searchText.length > 0) {
regExp = new RegExp(`(${searchText})`, 'ig');
const strippedQuery =
cr.search_highlight_utils.stripDiacritics(this.rawQuery_.trim());
const sanitizedQuery = strippedQuery.replace(SANITIZE_REGEX, '\\$&');
if (sanitizedQuery.length > 0) {
regExp = new RegExp(`(${sanitizedQuery})`, 'ig');
}
return regExp;
}
......
......@@ -114,7 +114,7 @@ suite(destination_list_test.suiteName, function() {
assertTrue(noMatchHint.hidden);
// Clearing the query restores the original state.
list.searchQuery = /()/ig;
list.searchQuery = null;
flush();
items.forEach(item => assertFalse(item.hidden));
assertTrue(noMatchHint.hidden);
......
......@@ -313,5 +313,28 @@ cr.define('settings_test', function() {
assertEquals('4 results', bubbles[1].textContent);
assertEquals('1 result', bubbles[0].textContent);
});
test('diacritics', async () => {
document.body.innerHTML = `
<settings-section>
<select>
<option>año de oro</option>
</select>
<button></button>
<settings-subpage>
malibu cañon
</settings-subpage>
danger zone
</setting-section>`;
const subpage = document.querySelector('settings-subpage');
subpage.associatedControl = document.querySelector('button');
await searchManager.search('an', document.body);
const highlights = document.querySelectorAll('.search-highlight-wrapper');
assertEquals(2, highlights.length);
assertEquals(2, document.querySelectorAll('.search-bubble').length);
});
});
});
......@@ -153,6 +153,7 @@ js_modulizer("modulize") {
"web_ui_listener_behavior.js",
]
namespace_rewrites = [
"cr.search_highlight_utils.Range|Range",
"cr.ui.KeyboardShortcutList|KeyboardShortcutList",
"Polymer.ArraySplice.calculateSplices|calculateSplices",
]
......
......@@ -17,6 +17,9 @@ cr.define('cr.search_highlight_utils', function() {
/** @type {string} */
const SEARCH_BUBBLE_CSS_CLASS = 'search-bubble';
/** @typedef {{start: number, length: number}} */
/* #export */ let Range;
/**
* Replaces the the highlight wrappers given in |wrappers| with the original
* search nodes.
......@@ -52,14 +55,12 @@ cr.define('cr.search_highlight_utils', function() {
* Applies the highlight UI (yellow rectangle) around all matches in |node|.
* @param {!Node} node The text node to be highlighted. |node| ends up
* being hidden.
* @param {!Array<string>} tokens The string tokens after splitting on the
* relevant regExp. Even indices hold text that doesn't need highlighting,
* odd indices hold the text to be highlighted. For example:
* const r = new RegExp('(foo)', 'i');
* 'barfoobar foo bar'.split(r) => ['bar', 'foo', 'bar ', 'foo', ' bar']
* @param {!Array<!cr.search_highlight_utils.Range>} ranges
* @return {!Node} The new highlight wrapper.
*/
/* #export */ function highlight(node, tokens) {
/* #export */ function highlight(node, ranges) {
assert(ranges.length > 0);
const wrapper = document.createElement('span');
wrapper.classList.add(WRAPPER_CSS_CLASS);
// Use existing node as placeholder to determine where to insert the
......@@ -75,6 +76,19 @@ cr.define('cr.search_highlight_utils', function() {
span.appendChild(node);
wrapper.appendChild(span);
const text = node.textContent;
/** @type {!Array<string>} */ const tokens = [];
for (let i = 0; i < ranges.length; ++i) {
const range = ranges[i];
const prev = ranges[i - 1] || {start: 0, length: 0};
const start = prev.start + prev.length;
const length = range.start - start;
tokens.push(text.substr(start, length));
tokens.push(text.substr(range.start, range.length));
}
const last = ranges.slice(-1)[0];
tokens.push(text.substr(last.start + last.length));
for (let i = 0; i < tokens.length; ++i) {
if (i % 2 == 0) {
wrapper.appendChild(document.createTextNode(tokens[i]));
......@@ -146,11 +160,21 @@ cr.define('cr.search_highlight_utils', function() {
return searchBubble;
}
/**
* @param {string} text
* @return {string}
*/
/* #export */ function stripDiacritics(text) {
return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
// #cr_define_end
return {
Range,
createEmptySearchBubble,
findAndRemoveHighlights,
highlight,
removeHighlights,
stripDiacritics,
};
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment