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

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

Issue 2176743004: MD Settings: Implementing a "no results" page. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address comment. Created 4 years, 4 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
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 /** @typedef {{id: number, rawQuery: ?string, regExp: ?RegExp}} */
6 var SearchContext;
7
8 cr.define('settings', function() { 5 cr.define('settings', function() {
9 /** @const {string} */ 6 /** @const {string} */
10 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; 7 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper';
11 8
12 /** @const {string} */ 9 /** @const {string} */
13 var HIT_CSS_CLASS = 'search-highlight-hit'; 10 var HIT_CSS_CLASS = 'search-highlight-hit';
14 11
15 /** 12 /**
16 * List of elements types that should not be searched at all. 13 * 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 14 * The only DOM-MODULE node is in <body> which is not searched, therefore
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
96 } 93 }
97 } 94 }
98 } 95 }
99 96
100 /** 97 /**
101 * Traverses the entire DOM (including Shadow DOM), finds text nodes that 98 * Traverses the entire DOM (including Shadow DOM), finds text nodes that
102 * match the given regular expression and applies the highlight UI. It also 99 * match the given regular expression and applies the highlight UI. It also
103 * ensures that <settings-section> instances become visible if any matches 100 * ensures that <settings-section> instances become visible if any matches
104 * occurred under their subtree. 101 * occurred under their subtree.
105 * 102 *
106 * @param {!SearchContext} context 103 * @param {!SearchRequest} request
107 * @param {!Node} root The root of the sub-tree to be searched 104 * @param {!Node} root The root of the sub-tree to be searched
108 * @private 105 * @private
109 */ 106 */
110 function findAndHighlightMatches_(context, root) { 107 function findAndHighlightMatches_(request, root) {
108 var foundMatches = false;
111 function doSearch(node) { 109 function doSearch(node) {
112 if (node.nodeName == 'TEMPLATE' && node.hasAttribute('name') && 110 if (node.nodeName == 'TEMPLATE' && node.hasAttribute('name') &&
113 !node.if) { 111 !node.if) {
114 getSearchManager().queue_.addRenderTask( 112 getSearchManager().queue_.addRenderTask(
115 new RenderTask(context, node)); 113 new RenderTask(request, node));
116 return; 114 return;
117 } 115 }
118 116
119 if (IGNORED_ELEMENTS.has(node.nodeName)) 117 if (IGNORED_ELEMENTS.has(node.nodeName))
120 return; 118 return;
121 119
122 if (node.nodeType == Node.TEXT_NODE) { 120 if (node.nodeType == Node.TEXT_NODE) {
123 var textContent = node.nodeValue.trim(); 121 var textContent = node.nodeValue.trim();
124 if (textContent.length == 0) 122 if (textContent.length == 0)
125 return; 123 return;
126 124
127 if (context.regExp.test(textContent)) { 125 if (request.regExp.test(textContent)) {
126 foundMatches = true;
128 revealParentSection_(node); 127 revealParentSection_(node);
129 highlight_(node, textContent.split(context.regExp)); 128 highlight_(node, textContent.split(request.regExp));
130 } 129 }
131 // Returning early since TEXT_NODE nodes never have children. 130 // Returning early since TEXT_NODE nodes never have children.
132 return; 131 return;
133 } 132 }
134 133
135 var child = node.firstChild; 134 var child = node.firstChild;
136 while (child !== null) { 135 while (child !== null) {
137 // Getting a reference to the |nextSibling| before calling doSearch() 136 // Getting a reference to the |nextSibling| before calling doSearch()
138 // because |child| could be removed from the DOM within doSearch(). 137 // because |child| could be removed from the DOM within doSearch().
139 var nextSibling = child.nextSibling; 138 var nextSibling = child.nextSibling;
140 doSearch(child); 139 doSearch(child);
141 child = nextSibling; 140 child = nextSibling;
142 } 141 }
143 142
144 var shadowRoot = node.shadowRoot; 143 var shadowRoot = node.shadowRoot;
145 if (shadowRoot) 144 if (shadowRoot)
146 doSearch(shadowRoot); 145 doSearch(shadowRoot);
147 } 146 }
148 147
149 doSearch(root); 148 doSearch(root);
149 return foundMatches;
150 } 150 }
151 151
152 /** 152 /**
153 * Finds and makes visible the <settings-section> parent of |node|. 153 * Finds and makes visible the <settings-section> parent of |node|.
154 * @param {!Node} node 154 * @param {!Node} node
155 */ 155 */
156 function revealParentSection_(node) { 156 function revealParentSection_(node) {
157 // Find corresponding SETTINGS-SECTION parent and make it visible. 157 // Find corresponding SETTINGS-SECTION parent and make it visible.
158 var parent = node; 158 var parent = node;
159 while (parent && parent.nodeName !== 'SETTINGS-SECTION') { 159 while (parent && parent.nodeName !== 'SETTINGS-SECTION') {
160 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? 160 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
161 parent.host : parent.parentNode; 161 parent.host : parent.parentNode;
162 } 162 }
163 if (parent) 163 if (parent)
164 parent.hidden = false; 164 parent.hidden = false;
165 } 165 }
166 166
167 /** 167 /**
168 * @constructor 168 * @constructor
169 * 169 *
170 * @param {!SearchContext} context 170 * @param {!SearchRequest} request
171 * @param {!Node} node 171 * @param {!Node} node
172 */ 172 */
173 function Task(context, node) { 173 function Task(request, node) {
174 /** @protected {!SearchContext} */ 174 /** @protected {!SearchRequest} */
175 this.context = context; 175 this.request = request;
176 176
177 /** @protected {!Node} */ 177 /** @protected {!Node} */
178 this.node = node; 178 this.node = node;
179 } 179 }
180 180
181 Task.prototype = { 181 Task.prototype = {
182 /** 182 /**
183 * @abstract 183 * @abstract
184 * @return {!Promise} 184 * @return {!Promise}
185 */ 185 */
186 exec: function() {}, 186 exec: function() {},
187 }; 187 };
188 188
189 /** 189 /**
190 * A task that takes a <template is="dom-if">...</template> node corresponding 190 * A task that takes a <template is="dom-if">...</template> node corresponding
191 * to a setting subpage and renders it. A SearchAndHighlightTask is posted for 191 * to a setting subpage and renders it. A SearchAndHighlightTask is posted for
192 * the newly rendered subtree, once rendering is done. 192 * the newly rendered subtree, once rendering is done.
193 * @constructor 193 * @constructor
194 * @extends {Task} 194 * @extends {Task}
195 * 195 *
196 * @param {!SearchContext} context 196 * @param {!SearchRequest} request
197 * @param {!Node} node 197 * @param {!Node} node
198 */ 198 */
199 function RenderTask(context, node) { 199 function RenderTask(request, node) {
200 Task.call(this, context, node); 200 Task.call(this, request, node);
201 } 201 }
202 202
203 RenderTask.prototype = { 203 RenderTask.prototype = {
204 /** @override */ 204 /** @override */
205 exec: function() { 205 exec: function() {
206 var subpageTemplate = 206 var subpageTemplate =
207 this.node['_content'].querySelector('settings-subpage'); 207 this.node['_content'].querySelector('settings-subpage');
208 subpageTemplate.id = subpageTemplate.id || this.node.getAttribute('name'); 208 subpageTemplate.id = subpageTemplate.id || this.node.getAttribute('name');
209 assert(!this.node.if); 209 assert(!this.node.if);
210 this.node.if = true; 210 this.node.if = true;
211 211
212 return new Promise(function(resolve, reject) { 212 return new Promise(function(resolve, reject) {
213 var parent = this.node.parentNode; 213 var parent = this.node.parentNode;
214 parent.async(function() { 214 parent.async(function() {
215 var renderedNode = parent.querySelector('#' + subpageTemplate.id); 215 var renderedNode = parent.querySelector('#' + subpageTemplate.id);
216 // Register a SearchAndHighlightTask for the part of the DOM that was 216 // Register a SearchAndHighlightTask for the part of the DOM that was
217 // just rendered. 217 // just rendered.
218 getSearchManager().queue_.addSearchAndHighlightTask( 218 getSearchManager().queue_.addSearchAndHighlightTask(
219 new SearchAndHighlightTask(this.context, assert(renderedNode))); 219 new SearchAndHighlightTask(this.request, assert(renderedNode)));
220 resolve(); 220 resolve();
221 }.bind(this)); 221 }.bind(this));
222 }.bind(this)); 222 }.bind(this));
223 }, 223 },
224 }; 224 };
225 225
226 /** 226 /**
227 * @constructor 227 * @constructor
228 * @extends {Task} 228 * @extends {Task}
229 * 229 *
230 * @param {!SearchContext} context 230 * @param {!SearchRequest} request
231 * @param {!Node} node 231 * @param {!Node} node
232 */ 232 */
233 function SearchAndHighlightTask(context, node) { 233 function SearchAndHighlightTask(request, node) {
234 Task.call(this, context, node); 234 Task.call(this, request, node);
235 } 235 }
236 236
237 SearchAndHighlightTask.prototype = { 237 SearchAndHighlightTask.prototype = {
238 /** @override */ 238 /** @override */
239 exec: function() { 239 exec: function() {
240 findAndHighlightMatches_(this.context, this.node); 240 var foundMatches = findAndHighlightMatches_(this.request, this.node);
241 this.request.updateMatches(foundMatches);
241 return Promise.resolve(); 242 return Promise.resolve();
242 }, 243 },
243 }; 244 };
244 245
245 /** 246 /**
246 * @constructor 247 * @constructor
247 * @extends {Task} 248 * @extends {Task}
248 * 249 *
249 * @param {!SearchContext} context 250 * @param {!SearchRequest} request
250 * @param {!Node} page 251 * @param {!Node} page
251 */ 252 */
252 function TopLevelSearchTask(context, page) { 253 function TopLevelSearchTask(request, page) {
253 Task.call(this, context, page); 254 Task.call(this, request, page);
254 } 255 }
255 256
256 TopLevelSearchTask.prototype = { 257 TopLevelSearchTask.prototype = {
257 /** @override */ 258 /** @override */
258 exec: function() { 259 exec: function() {
259 findAndRemoveHighlights_(this.node); 260 findAndRemoveHighlights_(this.node);
260 261
261 var shouldSearch = this.context.regExp !== null; 262 var shouldSearch = this.request.regExp !== null;
262 this.setSectionsVisibility_(!shouldSearch); 263 this.setSectionsVisibility_(!shouldSearch);
263 if (shouldSearch) 264 if (shouldSearch) {
264 findAndHighlightMatches_(this.context, this.node); 265 var foundMatches = findAndHighlightMatches_(this.request, this.node);
266 this.request.updateMatches(foundMatches);
267 }
265 268
266 return Promise.resolve(); 269 return Promise.resolve();
267 }, 270 },
268 271
269 /** 272 /**
270 * @param {boolean} visible 273 * @param {boolean} visible
271 * @private 274 * @private
272 */ 275 */
273 setSectionsVisibility_: function(visible) { 276 setSectionsVisibility_: function(visible) {
274 var sections = Polymer.dom( 277 var sections = Polymer.dom(
(...skipping 10 matching lines...) Expand all
285 /** 288 /**
286 * @private {{ 289 * @private {{
287 * high: !Array<!Task>, 290 * high: !Array<!Task>,
288 * middle: !Array<!Task>, 291 * middle: !Array<!Task>,
289 * low: !Array<!Task> 292 * low: !Array<!Task>
290 * }} 293 * }}
291 */ 294 */
292 this.queues_; 295 this.queues_;
293 this.reset(); 296 this.reset();
294 297
298 /** @private {?Function} */
299 this.onEmptyCallback_ = null;
300
295 /** 301 /**
296 * Whether a task is currently running. 302 * Whether a task is currently running.
297 * @private {boolean} 303 * @private {boolean}
298 */ 304 */
299 this.running_ = false; 305 this.running_ = false;
300 } 306 }
301 307
302 TaskQueue.prototype = { 308 TaskQueue.prototype = {
303 /** Drops all tasks. */ 309 /** Drops all tasks. */
304 reset: function() { 310 reset: function() {
(...skipping 12 matching lines...) Expand all
317 this.consumePending_(); 323 this.consumePending_();
318 }, 324 },
319 325
320 /** @param {!RenderTask} task */ 326 /** @param {!RenderTask} task */
321 addRenderTask: function(task) { 327 addRenderTask: function(task) {
322 this.queues_.low.push(task); 328 this.queues_.low.push(task);
323 this.consumePending_(); 329 this.consumePending_();
324 }, 330 },
325 331
326 /** 332 /**
333 * Registers a callback to be called every time the queue becomes empty.
334 * @param {function():void} onEmptyCallback
335 */
336 onEmpty: function(onEmptyCallback) {
337 this.onEmptyCallback_ = onEmptyCallback;
338 },
339
340 /**
327 * @return {!Task|undefined} 341 * @return {!Task|undefined}
328 * @private 342 * @private
329 */ 343 */
330 popNextTask_: function() { 344 popNextTask_: function() {
331 return this.queues_.high.shift() || 345 return this.queues_.high.shift() ||
332 this.queues_.middle.shift() || 346 this.queues_.middle.shift() ||
333 this.queues_.low.shift(); 347 this.queues_.low.shift();
334 }, 348 },
335 349
336 /** @private */ 350 /** @private */
337 consumePending_: function() { 351 consumePending_: function() {
338 if (this.running_) 352 if (this.running_)
339 return; 353 return;
340 354
341 while (1) { 355 while (1) {
342 var task = this.popNextTask_(); 356 var task = this.popNextTask_();
343 if (!task) { 357 if (!task) {
344 this.running_ = false; 358 this.running_ = false;
345 getSearchManager().notifyCallback(false); 359 if (this.onEmptyCallback_)
360 this.onEmptyCallback_();
346 return; 361 return;
347 } 362 }
348 363
349 this.running_ = true; 364 this.running_ = true;
350 window.requestIdleCallback(function() { 365 window.requestIdleCallback(function() {
351 function startNextTask() { 366 function startNextTask() {
352 this.running_ = false; 367 this.running_ = false;
353 this.consumePending_(); 368 this.consumePending_();
354 } 369 }
355 if (task.context.id == 370 if (task.request.id ==
356 getSearchManager().activeContext_.id) { 371 getSearchManager().activeRequest_.id) {
357 task.exec().then(startNextTask.bind(this)); 372 task.exec().then(startNextTask.bind(this));
358 } else { 373 } else {
359 // Dropping this task without ever executing it, since a new search 374 // Dropping this task without ever executing it, since a new search
360 // has been issued since this task was queued. 375 // has been issued since this task was queued.
361 startNextTask.call(this); 376 startNextTask.call(this);
362 } 377 }
363 }.bind(this)); 378 }.bind(this));
364 return; 379 return;
365 } 380 }
366 }, 381 },
367 }; 382 };
368 383
369 /** 384 /**
370 * @constructor 385 * @constructor
371 */ 386 */
387 var SearchRequest = function(rawQuery) {
388 /** @type {number} */
389 this.id = SearchRequest.nextId_++;
390
391 /** @private {string} */
392 this.rawQuery_ = rawQuery;
393
394 /** @type {?RegExp} */
395 this.regExp = this.generateRegExp_();
396
397 /**
398 * Whether this request was fully processed.
399 * @type {boolean}
400 */
401 this.finished = false;
402
403 /** @private {boolean} */
404 this.foundMatches_ = false;
405
406 /** @type {!PromiseResolver} */
407 this.resolver = new PromiseResolver();
408 };
409
410 /** @private {number} */
411 SearchRequest.nextId_ = 0;
412
413 /** @private {!RegExp} */
414 SearchRequest.SANITIZE_REGEX_ = /[-[\]{}()*+?.,\\^$|#\s]/g;
415
416 SearchRequest.prototype = {
417 /**
418 * @return {?RegExp}
419 * @private
420 */
421 generateRegExp_: function() {
422 var regExp = null;
423
424 // Generate search text by escaping any characters that would be
425 // problematic for regular expressions.
426 var searchText = this.rawQuery_.trim().replace(
427 SearchRequest.SANITIZE_REGEX_, '\\$&');
428 if (searchText.length > 0)
429 regExp = new RegExp('(' + searchText + ')', 'i');
430
431 return regExp;
432 },
433
434 /**
435 * @param {string} rawQuery
436 * @return {boolean} Whether this SearchRequest refers to an identical
437 * query.
438 */
439 isSame: function(rawQuery) {
440 return this.rawQuery_ == rawQuery;
441 },
442
443 /**
444 * Updates the result for this search request.
445 * @param {boolean} found
446 */
447 updateMatches: function(found) {
448 this.foundMatches_ = this.foundMatches_ || found;
449 },
450
451 /** @return {boolean} Whether any matches were found. */
452 didFindMatches: function() {
453 return this.foundMatches_;
454 },
455 };
456
457 /**
458 * @constructor
459 */
372 var SearchManager = function() { 460 var SearchManager = function() {
461 /** @private {?SearchRequest} */
462 this.activeRequest_ = null;
463
373 /** @private {!TaskQueue} */ 464 /** @private {!TaskQueue} */
374 this.queue_ = new TaskQueue(); 465 this.queue_ = new TaskQueue();
375 466 this.queue_.onEmpty(function() {
376 /** @private {!SearchContext} */ 467 this.activeRequest_.finished = true;
377 this.activeContext_ = {id: 0, rawQuery: null, regExp: null}; 468 this.activeRequest_.resolver.resolve(this.activeRequest_);
378 469 this.activeRequest_ = null;
379 /** @private {?function(boolean):void} */ 470 }.bind(this));
380 this.callbackFn_ = null;
381 }; 471 };
382 cr.addSingletonGetter(SearchManager); 472 cr.addSingletonGetter(SearchManager);
383 473
384 /** @private @const {!RegExp} */
385 SearchManager.SANITIZE_REGEX_ = /[-[\]{}()*+?.,\\^$|#\s]/g;
386
387 SearchManager.prototype = { 474 SearchManager.prototype = {
388 /** 475 /**
389 * Registers a callback function that will be called every time search
390 * starts/finishes.
391 * @param {?function(boolean):void} callbackFn
392 */
393 setCallback: function(callbackFn) {
394 this.callbackFn_ = callbackFn;
395 },
396
397 /** @param {boolean} isRunning */
398 notifyCallback: function(isRunning) {
399 if (this.callbackFn_)
400 this.callbackFn_(isRunning);
401 },
402
403 /**
404 * @param {string} text The text to search for. 476 * @param {string} text The text to search for.
405 * @param {!Node} page 477 * @param {!Node} page
478 * @return {!Promise<!SearchRequest>} A signal indicating that searching
479 * finished.
406 */ 480 */
407 search: function(text, page) { 481 search: function(text, page) {
408 if (this.activeContext_.rawQuery != text) { 482 // Creating a new request only if the |text| changed.
409 var newId = this.activeContext_.id + 1; 483 if (!this.activeRequest_ || !this.activeRequest_.isSame(text)) {
484 // Resolving previous search request without marking it as
485 // 'finisthed', if any, and droping all pending tasks.
486 this.queue_.reset();
487 if (this.activeRequest_)
488 this.activeRequest_.resolver.resolve(this.activeRequest_);
410 489
411 var regExp = null; 490 this.activeRequest_ = new SearchRequest(text);
412 // Generate search text by escaping any characters that would be
413 // problematic for regular expressions.
414 var searchText = text.trim().replace(
415 SearchManager.SANITIZE_REGEX_, '\\$&');
416 if (searchText.length > 0)
417 regExp = new RegExp('(' + searchText + ')', 'i');
418
419 this.activeContext_ = {id: newId, rawQuery: text, regExp: regExp};
420
421 // Drop all previously scheduled tasks, since a new search was just
422 // issued.
423 this.queue_.reset();
424 this.notifyCallback(true);
425 } 491 }
426 492
427 this.queue_.addTopLevelSearchTask( 493 this.queue_.addTopLevelSearchTask(
428 new TopLevelSearchTask(this.activeContext_, page)); 494 new TopLevelSearchTask(this.activeRequest_, page));
495
496 return this.activeRequest_.resolver.promise;
429 }, 497 },
430 }; 498 };
431 499
432 /** @return {!SearchManager} */ 500 /** @return {!SearchManager} */
433 function getSearchManager() { 501 function getSearchManager() {
434 return SearchManager.getInstance(); 502 return SearchManager.getInstance();
435 } 503 }
436 504
437 return { 505 return {
438 getSearchManager: getSearchManager, 506 getSearchManager: getSearchManager,
439 }; 507 };
440 }); 508 });
OLDNEW
« no previous file with comments | « chrome/app/settings_strings.grdp ('k') | chrome/browser/resources/settings/settings_main/settings_main.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698