Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(469)

Side by Side Diff: chrome/browser/resources/settings/search_settings.js

Issue 2103133007: MD Settings: Second iteration of search within settings. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@cr_search_migration1
Patch Set: Addressing comments. Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /**
6 * @typedef {{id: number, rawQuery: ?string, regExp: ?RegExp}}
7 */
Dan Beam 2016/07/13 01:26:12 /** ... */ is actually 1 line ;)
dpapad 2016/07/13 02:27:01 Done.
8 var SearchContext;
9
5 cr.define('settings', function() { 10 cr.define('settings', function() {
6 /** @const {string} */ 11 /** @const {string} */
7 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; 12 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper';
8 13
9 /** @const {string} */ 14 /** @const {string} */
10 var HIT_CSS_CLASS = 'search-highlight-hit'; 15 var HIT_CSS_CLASS = 'search-highlight-hit';
11 16
12 /** @const {!RegExp} */
13 var SANITIZE_REGEX = /[-[\]{}()*+?.,\\^$|#\s]/g;
14
15 /** 17 /**
16 * List of elements types that should not be searched at all. 18 * List of elements types that should not be searched at all.
17 * The only DOM-MODULE node is in <body> which is not searched, therefore 19 * The only DOM-MODULE node is in <body> which is not searched, therefore
18 * DOM-MODULE is not needed in this set. 20 * DOM-MODULE is not needed in this set.
19 * @const {!Set<string>} 21 * @const {!Set<string>}
20 */ 22 */
21 var IGNORED_ELEMENTS = new Set([ 23 var IGNORED_ELEMENTS = new Set([
22 'CONTENT', 24 'CONTENT',
23 'CR-EVENTS', 25 'CR-EVENTS',
24 'IMG', 26 'IMG',
25 'IRON-ICON', 27 'IRON-ICON',
26 'IRON-LIST', 28 'IRON-LIST',
27 'PAPER-ICON-BUTTON', 29 'PAPER-ICON-BUTTON',
28 /* TODO(dpapad): paper-item is used for dynamically populated dropdown 30 /* TODO(dpapad): paper-item is used for dynamically populated dropdown
29 * menus. Perhaps a better approach is to mark the entire dropdown menu such 31 * menus. Perhaps a better approach is to mark the entire dropdown menu such
30 * that search algorithm can skip it as a whole instead. 32 * that search algorithm can skip it as a whole instead.
31 */ 33 */
32 'PAPER-ITEM', 34 'PAPER-ITEM',
33 'PAPER-RIPPLE', 35 'PAPER-RIPPLE',
34 'PAPER-SLIDER', 36 'PAPER-SLIDER',
35 'PAPER-SPINNER', 37 'PAPER-SPINNER',
36 'STYLE', 38 'STYLE',
37 'TEMPLATE', 39 'TEMPLATE',
38 ]); 40 ]);
39 41
40 /** 42 /**
41 * Finds all previous highlighted nodes under |node| (both within self and 43 * Finds all previous highlighted nodes under |node| (both within self and
42 * children's Shadow DOM) and removes the highlight (yellow rectangle). 44 * children's Shadow DOM) and removes the highlight (yellow rectangle).
45 * TODO(dpapad): Consider making this a private method of TopLevelSearchTask.
43 * @param {!Node} node 46 * @param {!Node} node
44 * @private 47 * @private
45 */ 48 */
46 function findAndRemoveHighlights_(node) { 49 function findAndRemoveHighlights_(node) {
47 var wrappers = node.querySelectorAll('* /deep/ .' + WRAPPER_CSS_CLASS); 50 var wrappers = node.querySelectorAll('* /deep/ .' + WRAPPER_CSS_CLASS);
48 51
49 for (var wrapper of wrappers) { 52 for (var wrapper of wrappers) {
50 var hitElements = wrapper.querySelectorAll('.' + HIT_CSS_CLASS); 53 var hitElements = wrapper.querySelectorAll('.' + HIT_CSS_CLASS);
51 // For each hit element, remove the highlighting. 54 // For each hit element, remove the highlighting.
52 for (var hitElement of hitElements) { 55 for (var hitElement of hitElements) {
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
90 var span = document.createElement('span'); 93 var span = document.createElement('span');
91 span.classList.add(HIT_CSS_CLASS); 94 span.classList.add(HIT_CSS_CLASS);
92 span.style.backgroundColor = 'yellow'; 95 span.style.backgroundColor = 'yellow';
93 span.textContent = tokens[i]; 96 span.textContent = tokens[i];
94 wrapper.appendChild(span); 97 wrapper.appendChild(span);
95 } 98 }
96 } 99 }
97 } 100 }
98 101
99 /** 102 /**
103 * Checks whether the given |node| requires force rendering.
104 *
105 * @param {!SearchContext} context
106 * @param {!Node} node
107 * @return {boolean} Whether a forced rendering task was scheduled.
108 * @private
109 */
110 function forceRenderNeeded_(context, node) {
111 if (node.nodeName != 'TEMPLATE' || !node.hasAttribute('name') || node.if)
112 return false;
113
114 // TODO(dpapad): Temporarily ignore site-settings because it throws an
115 // assertion error during force-rendering.
116 return node.getAttribute('name').indexOf('site-') != 0;
117 }
118
119 /**
100 * Traverses the entire DOM (including Shadow DOM), finds text nodes that 120 * Traverses the entire DOM (including Shadow DOM), finds text nodes that
101 * match the given regular expression and applies the highlight UI. It also 121 * match the given regular expression and applies the highlight UI. It also
102 * ensures that <settings-section> instances become visible if any matches 122 * ensures that <settings-section> instances become visible if any matches
103 * occurred under their subtree. 123 * occurred under their subtree.
104 * 124 *
105 * @param {!Element} page The page to be searched, should be either 125 * @param {!SearchContext} context
106 * <settings-basic-page> or <settings-advanced-page>. 126 * @param {!Node} root The root of the sub-tree to be searched
107 * @param {!RegExp} regExp The regular expression to detect matches.
108 * @private 127 * @private
109 */ 128 */
110 function findAndHighlightMatches_(page, regExp) { 129 function findAndHighlightMatches_(context, root) {
111 function doSearch(node) { 130 function doSearch(node) {
112 if (IGNORED_ELEMENTS.has(node.tagName)) 131 if (forceRenderNeeded_(context, node)) {
132 SearchManager.getInstance().queue_.addRenderTask(
133 new RenderTask(context, node));
134 return;
135 }
136
137 if (IGNORED_ELEMENTS.has(node.nodeName))
113 return; 138 return;
114 139
115 if (node.nodeType == Node.TEXT_NODE) { 140 if (node.nodeType == Node.TEXT_NODE) {
116 var textContent = node.nodeValue.trim(); 141 var textContent = node.nodeValue.trim();
117 if (textContent.length == 0) 142 if (textContent.length == 0)
118 return; 143 return;
119 144
120 if (regExp.test(textContent)) { 145 if (context.regExp.test(textContent)) {
121 revealParentSection_(node); 146 revealParentSection_(node);
122 highlight_(node, textContent.split(regExp)); 147 highlight_(node, textContent.split(context.regExp));
123 } 148 }
124 // Returning early since TEXT_NODE nodes never have children. 149 // Returning early since TEXT_NODE nodes never have children.
125 return; 150 return;
126 } 151 }
127 152
128 var child = node.firstChild; 153 var child = node.firstChild;
129 while (child !== null) { 154 while (child !== null) {
130 // Getting a reference to the |nextSibling| before calling doSearch() 155 // Getting a reference to the |nextSibling| before calling doSearch()
131 // because |child| could be removed from the DOM within doSearch(). 156 // because |child| could be removed from the DOM within doSearch().
132 var nextSibling = child.nextSibling; 157 var nextSibling = child.nextSibling;
133 doSearch(child); 158 doSearch(child);
134 child = nextSibling; 159 child = nextSibling;
135 } 160 }
136 161
137 var shadowRoot = node.shadowRoot; 162 var shadowRoot = node.shadowRoot;
138 if (shadowRoot) 163 if (shadowRoot)
139 doSearch(shadowRoot); 164 doSearch(shadowRoot);
140 } 165 }
141 166
142 doSearch(page); 167 doSearch(root);
143 } 168 }
144 169
145 /** 170 /**
146 * Finds and makes visible the <settings-section> parent of |node|. 171 * Finds and makes visible the <settings-section> parent of |node|.
147 * @param {!Node} node 172 * @param {!Node} node
148 */ 173 */
149 function revealParentSection_(node) { 174 function revealParentSection_(node) {
150 // Find corresponding SETTINGS-SECTION parent and make it visible. 175 // Find corresponding SETTINGS-SECTION parent and make it visible.
151 var parent = node; 176 var parent = node;
152 while (parent && parent.tagName !== 'SETTINGS-SECTION') { 177 while (parent && parent.nodeName !== 'SETTINGS-SECTION') {
153 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? 178 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
154 parent.host : parent.parentNode; 179 parent.host : parent.parentNode;
155 } 180 }
156 if (parent) 181 if (parent)
157 parent.hidden = false; 182 parent.hidden = false;
158 } 183 }
159 184
160 /** 185 /**
161 * @param {!Element} page 186 * @constructor
162 * @param {boolean} visible 187 *
163 * @private 188 * @param {!SearchContext} context
164 */ 189 * @param {!Node} node
165 function setSectionsVisibility_(page, visible) { 190 */
166 var sections = Polymer.dom(page.root).querySelectorAll('settings-section'); 191 function Task(context, node) {
167 for (var i = 0; i < sections.length; i++) 192 /** @protected {!SearchContext} */
168 sections[i].hidden = !visible; 193 this.context = context;
169 } 194
170 195 /** @protected {!Node} */
171 /** 196 this.node = node;
172 * Performs hierarchical search, starting at the given page element. 197 }
198
199 Task.prototype = {
200 /**
201 * @abstract
202 * @return {!Promise}
203 */
204 exec: function() {},
205 };
206
207 /**
208 * A task that takes a <template is="dom-if">...</template> node corresponding
209 * to a setting subpage and renders it. A SearchTask is posted for the newly
210 * rendered subtree, once rendering is done.
211 * @constructor
212 * @extends {Task}
213 *
214 * @param {!SearchContext} context
215 * @param {!Node} node
216 */
217 function RenderTask(context, node) {
218 Task.call(this, context, node);
219 }
220
221 RenderTask.prototype = {
222 /** @override */
223 exec: function() {
224 var subpageTemplate =
225 this.node['_content'].querySelector('settings-subpage');
226 subpageTemplate.id = subpageTemplate.id || this.node.getAttribute('name');
227 assert(!this.node.if);
228 this.node.if = true;
229
230 return new Promise(function(resolve, reject) {
231 var parent = this.node.parentNode;
232 parent.async(function() {
233 var renderedNode = parent.querySelector('#' + subpageTemplate.id);
234 // Register a SearchTask for the part of the DOM that was just
235 // rendered.
236 SearchManager.getInstance().queue_.addSearchTask(
237 new SearchTask(this.context, assert(renderedNode)));
238 resolve();
239 }.bind(this));
240 }.bind(this));
241 },
242 };
243
244 /**
245 * @constructor
246 * @extends {Task}
247 *
248 * @param {!SearchContext} context
249 * @param {!Node} node
250 */
251 function SearchTask(context, node) {
252 Task.call(this, context, node);
253 }
254
255 SearchTask.prototype = {
256 /** @override */
257 exec: function() {
258 findAndHighlightMatches_(this.context, this.node);
259 return Promise.resolve();
260 },
261 };
262
263 /**
264 * @constructor
265 * @extends {Task}
266 *
267 * @param {!SearchContext} context
268 * @param {!Node} page
269 */
270 function TopLevelSearchTask(context, page) {
271 Task.call(this, context, page);
272 }
273
274 TopLevelSearchTask.prototype = {
275 /** @override */
276 exec: function() {
277 findAndRemoveHighlights_(this.node);
278
279 var shouldSearch = this.context.regExp !== null;
280 this.setSectionsVisibility_(!shouldSearch);
281 if (shouldSearch)
282 findAndHighlightMatches_(this.context, this.node);
283
284 return Promise.resolve();
285 },
286
287 /**
288 * @param {boolean} visible
289 * @private
290 */
291 setSectionsVisibility_: function(visible) {
292 var sections = Polymer.dom(
293 this.node.root).querySelectorAll('settings-section');
294 for (var i = 0; i < sections.length; i++)
295 sections[i].hidden = !visible;
296 },
297 };
298
299 /**
300 * @constructor
301 */
302 function TaskQueue() {
303 /**
304 * @private {{
305 * high: !Array<!Task>,
306 * middle: !Array<!Task>,
307 * low: !Array<!Task>
308 * }}
309 */
310 this.queues_ = {high: [], middle: [], low: []};
311
312 /**
313 * Whether a task is currently running.
314 * @private {boolean}
315 */
316 this.running_ = false;
317 }
318
319 TaskQueue.prototype = {
320 /** Drops all tasks. */
321 reset: function() {
322 this.queues_.high.length = 0;
323 this.queues_.middle.length = 0;
324 this.queues_.low.length = 0;
Dan Beam 2016/07/13 01:26:12 or just make this impl: this.queues_ = {high: [],
dpapad 2016/07/13 02:27:01 Done.
325 },
326
327 /** @param {!TopLevelSearchTask} task */
328 addTopLevelSearchTask: function(task) {
329 this.queues_.high.push(task);
330 this.consumePending_();
331 },
332
333 /** @param {!SearchTask} task */
334 addSearchTask: function(task) {
335 this.queues_.middle.push(task);
336 this.consumePending_();
337 },
338
339 /** @param {!RenderTask} task */
340 addRenderTask: function(task) {
341 this.queues_.low.push(task);
342 this.consumePending_();
343 },
344
345 /**
346 * @return {?Task}
Dan Beam 2016/07/13 01:26:12 isn't this technically !Task|undefined now?
dpapad 2016/07/13 02:27:01 Done. As said, either way compiler did not complai
347 * @private
348 */
349 popNextTask_: function() {
350 return this.queues_.high.shift() ||
351 this.queues_.middle.shift() ||
352 this.queues_.low.shift();
353 },
354
355 /**
356 * @param {!Task} task
357 * @return {boolean} Whether the given task is now obsolete (refers to a
358 * previous search query).
359 * @private
360 */
361 isTaskObsolete_: function(task) {
362 return task.context.id != SearchManager.getInstance().activeContext_.id;
363 },
364
365 /** @private */
366 consumePending_: function() {
367 if (this.running_)
368 return;
369
370 while (1) {
371 var task = this.popNextTask_();
372 if (!task) {
373 this.running_ = false;
374 return;
375 }
376
377 if (this.isTaskObsolete_(task)) {
Dan Beam 2016/07/13 01:26:12 when will this happen?
dpapad 2016/07/13 02:27:01 Now that we are clearing the queues immediately, t
378 // Dropping this task without ever executing it, since a new search
379 // has been issued since this task was queued.
380 continue;
381 }
382
383 window.requestIdleCallback(function() {
384 // Need to check if this task is obsolete again, since a new search
385 // might have been issued after requestIdleCallback() was called.
386 if (this.isTaskObsolete_(assert(task))) {
387 this.running_ = false;
388 this.consumePending_();
389 } else {
390 task.exec().then(function(result) {
391 this.running_ = false;
392 this.consumePending_();
393 }.bind(this));
394 }
Dan Beam 2016/07/13 01:26:12 function startNextTask() { this.running_ = false
dpapad 2016/07/13 02:27:01 Done (kept isTaskObsolete() though).
395 }.bind(this));
396 return;
397 }
398 },
399 };
400
401 /**
402 * @constructor
403 */
404 var SearchManager = function() {
405 /** @private {!TaskQueue} */
406 this.queue_ = new TaskQueue();
407
408 /** @private {!SearchContext} */
409 this.activeContext_ = {id: 0, rawQuery: null, regExp: null};
410 };
411 cr.addSingletonGetter(SearchManager);
412
413 /** @private @const {!RegExp} */
414 SearchManager.SANITIZE_REGEX_ = /[-[\]{}()*+?.,\\^$|#\s]/g;
415
416 SearchManager.prototype = {
417 /**
418 * @param {string} text The text to search for.
419 * @param {!Node} page
420 */
421 search: function(text, page) {
422 if (this.activeContext_.rawQuery != text) {
Dan Beam 2016/07/13 01:28:18 i'm still confused: you said that we'll never get
dpapad 2016/07/13 02:27:01 search() is called twice for every user initiated
423 var newId = this.activeContext_.id + 1;
424
425 var regExp = null;
426 // Generate search text by escaping any characters that would be
427 // problematic for regular expressions.
428 var searchText = text.trim().replace(
429 SearchManager.SANITIZE_REGEX_, '\\$&');
430 if (searchText.length > 0)
431 regExp = new RegExp('(' + searchText + ')', 'i');
432
433 this.activeContext_ = {id: newId, rawQuery: text, regExp: regExp};
434
435 // Drop all previously scheduled tasks, since a new search was just
436 // issued.
437 this.queue_.reset();
438 }
439
440 this.queue_.addTopLevelSearchTask(
441 new TopLevelSearchTask(this.activeContext_, page));
442 },
443 };
444
445 /**
173 * @param {string} text 446 * @param {string} text
174 * @param {!Element} page Must be either <settings-basic-page> or 447 * @param {!Node} page
175 * <settings-advanced-page>.
176 */ 448 */
177 function search(text, page) { 449 function search(text, page) {
178 findAndRemoveHighlights_(page); 450 SearchManager.getInstance().search(text, page);
179
180 // Generate search text by escaping any characters that would be problematic
181 // for regular expressions.
182 var searchText = text.trim().replace(SANITIZE_REGEX, '\\$&');
183 if (searchText.length == 0) {
184 setSectionsVisibility_(page, true);
185 return;
186 }
187
188 setSectionsVisibility_(page, false);
189 findAndHighlightMatches_(page, new RegExp('(' + searchText + ')', 'i'));
190 } 451 }
191 452
192 return { 453 return {
193 search: search, 454 search: search,
194 }; 455 };
195 }); 456 });
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698