OLD | NEW |
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 cr.exportPath('settings'); |
| 6 |
| 7 /** |
| 8 * A data structure used by callers to combine the results of multiple search |
| 9 * requests. |
| 10 * |
| 11 * @typedef {{ |
| 12 * canceled: Boolean, |
| 13 * didFindMatches: Boolean, |
| 14 * wasClearSearch: Boolean, |
| 15 * }} |
| 16 */ |
| 17 settings.SearchResult; |
| 18 |
5 cr.define('settings', function() { | 19 cr.define('settings', function() { |
6 /** @const {string} */ | 20 /** @const {string} */ |
7 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; | 21 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; |
8 | 22 |
9 /** @const {string} */ | 23 /** @const {string} */ |
10 var ORIGINAL_CONTENT_CSS_CLASS = 'search-highlight-original-content'; | 24 var ORIGINAL_CONTENT_CSS_CLASS = 'search-highlight-original-content'; |
11 | 25 |
12 /** @const {string} */ | 26 /** @const {string} */ |
13 var HIT_CSS_CLASS = 'search-highlight-hit'; | 27 var HIT_CSS_CLASS = 'search-highlight-hit'; |
14 | 28 |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
118 * | 132 * |
119 * @param {!settings.SearchRequest} request | 133 * @param {!settings.SearchRequest} request |
120 * @param {!Node} root The root of the sub-tree to be searched | 134 * @param {!Node} root The root of the sub-tree to be searched |
121 * @private | 135 * @private |
122 */ | 136 */ |
123 function findAndHighlightMatches_(request, root) { | 137 function findAndHighlightMatches_(request, root) { |
124 var foundMatches = false; | 138 var foundMatches = false; |
125 function doSearch(node) { | 139 function doSearch(node) { |
126 if (node.nodeName == 'TEMPLATE' && node.hasAttribute('route-path') && | 140 if (node.nodeName == 'TEMPLATE' && node.hasAttribute('route-path') && |
127 !node.if && !node.hasAttribute(SKIP_SEARCH_CSS_ATTRIBUTE)) { | 141 !node.if && !node.hasAttribute(SKIP_SEARCH_CSS_ATTRIBUTE)) { |
128 getSearchManager().queue_.addRenderTask( | 142 request.queue_.addRenderTask(new RenderTask(request, node)); |
129 new RenderTask(request, node)); | |
130 return; | 143 return; |
131 } | 144 } |
132 | 145 |
133 if (IGNORED_ELEMENTS.has(node.nodeName)) | 146 if (IGNORED_ELEMENTS.has(node.nodeName)) |
134 return; | 147 return; |
135 | 148 |
136 if (node instanceof HTMLElement) { | 149 if (node instanceof HTMLElement) { |
137 var element = /** @type {HTMLElement} */(node); | 150 var element = /** @type {HTMLElement} */(node); |
138 if (element.hasAttribute(SKIP_SEARCH_CSS_ATTRIBUTE) || | 151 if (element.hasAttribute(SKIP_SEARCH_CSS_ATTRIBUTE) || |
139 element.hasAttribute('hidden') || | 152 element.hasAttribute('hidden') || |
(...skipping 157 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
297 assert(!this.node.if); | 310 assert(!this.node.if); |
298 this.node.if = true; | 311 this.node.if = true; |
299 | 312 |
300 return new Promise(function(resolve, reject) { | 313 return new Promise(function(resolve, reject) { |
301 var parent = this.node.parentNode; | 314 var parent = this.node.parentNode; |
302 parent.async(function() { | 315 parent.async(function() { |
303 var renderedNode = | 316 var renderedNode = |
304 parent.querySelector('[route-path="' + routePath + '"]'); | 317 parent.querySelector('[route-path="' + routePath + '"]'); |
305 // Register a SearchAndHighlightTask for the part of the DOM that was | 318 // Register a SearchAndHighlightTask for the part of the DOM that was |
306 // just rendered. | 319 // just rendered. |
307 getSearchManager().queue_.addSearchAndHighlightTask( | 320 this.request.queue_.addSearchAndHighlightTask( |
308 new SearchAndHighlightTask(this.request, assert(renderedNode))); | 321 new SearchAndHighlightTask(this.request, assert(renderedNode))); |
309 resolve(); | 322 resolve(); |
310 }.bind(this)); | 323 }.bind(this)); |
311 }.bind(this)); | 324 }.bind(this)); |
312 }, | 325 }, |
313 }; | 326 }; |
314 | 327 |
315 /** | 328 /** |
316 * @constructor | 329 * @constructor |
317 * @extends {Task} | 330 * @extends {Task} |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
365 setSectionsVisibility_: function(visible) { | 378 setSectionsVisibility_: function(visible) { |
366 var sections = this.node.querySelectorAll('settings-section'); | 379 var sections = this.node.querySelectorAll('settings-section'); |
367 | 380 |
368 for (var i = 0; i < sections.length; i++) | 381 for (var i = 0; i < sections.length; i++) |
369 sections[i].hiddenBySearch = !visible; | 382 sections[i].hiddenBySearch = !visible; |
370 }, | 383 }, |
371 }; | 384 }; |
372 | 385 |
373 /** | 386 /** |
374 * @constructor | 387 * @constructor |
| 388 * @param {!settings.SearchRequest} request |
375 */ | 389 */ |
376 function TaskQueue() { | 390 function TaskQueue(request) { |
| 391 /** @private {!settings.SearchRequest} */ |
| 392 this.request_ = request; |
| 393 |
377 /** | 394 /** |
378 * @private {{ | 395 * @private {{ |
379 * high: !Array<!Task>, | 396 * high: !Array<!Task>, |
380 * middle: !Array<!Task>, | 397 * middle: !Array<!Task>, |
381 * low: !Array<!Task> | 398 * low: !Array<!Task> |
382 * }} | 399 * }} |
383 */ | 400 */ |
384 this.queues_; | 401 this.queues_; |
385 this.reset(); | 402 this.reset(); |
386 | 403 |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
445 var task = this.popNextTask_(); | 462 var task = this.popNextTask_(); |
446 if (!task) { | 463 if (!task) { |
447 this.running_ = false; | 464 this.running_ = false; |
448 if (this.onEmptyCallback_) | 465 if (this.onEmptyCallback_) |
449 this.onEmptyCallback_(); | 466 this.onEmptyCallback_(); |
450 return; | 467 return; |
451 } | 468 } |
452 | 469 |
453 this.running_ = true; | 470 this.running_ = true; |
454 window.requestIdleCallback(function() { | 471 window.requestIdleCallback(function() { |
455 function startNextTask() { | 472 if (!this.request_.canceled) { |
456 this.running_ = false; | 473 task.exec().then(function() { |
457 this.consumePending_(); | 474 this.running_ = false; |
| 475 this.consumePending_(); |
| 476 }.bind(this)); |
458 } | 477 } |
459 if (task.request.id == | 478 // Nothing to do otherwise. Since the request corresponding to this |
460 getSearchManager().activeRequest_.id) { | 479 // queue was canceled, the queue is disposed along with the request. |
461 task.exec().then(startNextTask.bind(this)); | |
462 } else { | |
463 // Dropping this task without ever executing it, since a new search | |
464 // has been issued since this task was queued. | |
465 startNextTask.call(this); | |
466 } | |
467 }.bind(this)); | 480 }.bind(this)); |
468 return; | 481 return; |
469 } | 482 } |
470 }, | 483 }, |
471 }; | 484 }; |
472 | 485 |
473 /** | 486 /** |
474 * @constructor | 487 * @constructor |
| 488 * |
| 489 * @param {string} rawQuery |
| 490 * @param {!HTMLElement} root |
475 */ | 491 */ |
476 var SearchRequest = function(rawQuery) { | 492 var SearchRequest = function(rawQuery, root) { |
477 /** @type {number} */ | |
478 this.id = SearchRequest.nextId_++; | |
479 | |
480 /** @private {string} */ | 493 /** @private {string} */ |
481 this.rawQuery_ = rawQuery; | 494 this.rawQuery_ = rawQuery; |
482 | 495 |
| 496 /** @private {!HTMLElement} */ |
| 497 this.root_ = root; |
| 498 |
483 /** @type {?RegExp} */ | 499 /** @type {?RegExp} */ |
484 this.regExp = this.generateRegExp_(); | 500 this.regExp = this.generateRegExp_(); |
485 | 501 |
486 /** | 502 /** |
487 * Whether this request was fully processed. | 503 * Whether this request was canceled before completing. |
488 * @type {boolean} | 504 * @type {boolean} |
489 */ | 505 */ |
490 this.finished = false; | 506 this.canceled = false; |
491 | 507 |
492 /** @private {boolean} */ | 508 /** @private {boolean} */ |
493 this.foundMatches_ = false; | 509 this.foundMatches_ = false; |
494 | 510 |
495 /** @type {!PromiseResolver} */ | 511 /** @type {!PromiseResolver} */ |
496 this.resolver = new PromiseResolver(); | 512 this.resolver = new PromiseResolver(); |
| 513 |
| 514 /** @private {!TaskQueue} */ |
| 515 this.queue_ = new TaskQueue(this); |
| 516 this.queue_.onEmpty(function() { |
| 517 this.resolver.resolve(this); |
| 518 }.bind(this)); |
497 }; | 519 }; |
498 | 520 |
499 /** @private {number} */ | |
500 SearchRequest.nextId_ = 0; | |
501 | |
502 /** @private {!RegExp} */ | 521 /** @private {!RegExp} */ |
503 SearchRequest.SANITIZE_REGEX_ = /[-[\]{}()*+?.,\\^$|#\s]/g; | 522 SearchRequest.SANITIZE_REGEX_ = /[-[\]{}()*+?.,\\^$|#\s]/g; |
504 | 523 |
505 SearchRequest.prototype = { | 524 SearchRequest.prototype = { |
506 /** | 525 /** |
| 526 * Fires this search request. |
| 527 */ |
| 528 start: function() { |
| 529 this.queue_.addTopLevelSearchTask( |
| 530 new TopLevelSearchTask(this, this.root_)); |
| 531 }, |
| 532 |
| 533 /** |
507 * @return {?RegExp} | 534 * @return {?RegExp} |
508 * @private | 535 * @private |
509 */ | 536 */ |
510 generateRegExp_: function() { | 537 generateRegExp_: function() { |
511 var regExp = null; | 538 var regExp = null; |
512 | 539 |
513 // Generate search text by escaping any characters that would be | 540 // Generate search text by escaping any characters that would be |
514 // problematic for regular expressions. | 541 // problematic for regular expressions. |
515 var searchText = this.rawQuery_.trim().replace( | 542 var searchText = this.rawQuery_.trim().replace( |
516 SearchRequest.SANITIZE_REGEX_, '\\$&'); | 543 SearchRequest.SANITIZE_REGEX_, '\\$&'); |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
554 * searching finished. | 581 * searching finished. |
555 */ | 582 */ |
556 search: function(text, page) {} | 583 search: function(text, page) {} |
557 }; | 584 }; |
558 | 585 |
559 /** | 586 /** |
560 * @constructor | 587 * @constructor |
561 * @implements {SearchManager} | 588 * @implements {SearchManager} |
562 */ | 589 */ |
563 var SearchManagerImpl = function() { | 590 var SearchManagerImpl = function() { |
564 /** @private {?settings.SearchRequest} */ | 591 /** @private {!Set<!settings.SearchRequest>} */ |
565 this.activeRequest_ = null; | 592 this.activeRequests_ = new Set(); |
566 | 593 |
567 /** @private {!TaskQueue} */ | 594 /** @private {?string} */ |
568 this.queue_ = new TaskQueue(); | 595 this.lastSearchedText_ = null; |
569 this.queue_.onEmpty(function() { | |
570 this.activeRequest_.finished = true; | |
571 this.activeRequest_.resolver.resolve(this.activeRequest_); | |
572 this.activeRequest_ = null; | |
573 }.bind(this)); | |
574 }; | 596 }; |
575 cr.addSingletonGetter(SearchManagerImpl); | 597 cr.addSingletonGetter(SearchManagerImpl); |
576 | 598 |
577 SearchManagerImpl.prototype = { | 599 SearchManagerImpl.prototype = { |
578 /** @override */ | 600 /** @override */ |
579 search: function(text, page) { | 601 search: function(text, page) { |
580 // Creating a new request only if the |text| changed. | 602 // Cancel any pending requests if a request with different text is |
581 if (!this.activeRequest_ || !this.activeRequest_.isSame(text)) { | 603 // submitted. |
582 // Resolving previous search request without marking it as | 604 if (text != this.lastSearchedText_) { |
583 // 'finished', if any, and dropping all pending tasks. | 605 this.activeRequests_.forEach(function(request) { |
584 this.queue_.reset(); | 606 request.canceled = true; |
585 if (this.activeRequest_) | 607 request.resolver.resolve(request); |
586 this.activeRequest_.resolver.resolve(this.activeRequest_); | 608 }); |
587 | 609 this.activeRequests_.clear(); |
588 this.activeRequest_ = new SearchRequest(text); | |
589 } | 610 } |
590 | 611 |
591 this.queue_.addTopLevelSearchTask( | 612 this.lastSearchedText_ = text; |
592 new TopLevelSearchTask(this.activeRequest_, page)); | 613 var request = new SearchRequest(text, page); |
593 | 614 this.activeRequests_.add(request); |
594 return this.activeRequest_.resolver.promise; | 615 request.start(); |
| 616 return request.resolver.promise.then(function() { |
| 617 // Stop tracking requests that finished. |
| 618 this.activeRequests_.delete(request); |
| 619 return request; |
| 620 }.bind(this)); |
595 }, | 621 }, |
596 }; | 622 }; |
597 | 623 |
598 /** @return {!SearchManager} */ | 624 /** @return {!SearchManager} */ |
599 function getSearchManager() { | 625 function getSearchManager() { |
600 return SearchManagerImpl.getInstance(); | 626 return SearchManagerImpl.getInstance(); |
601 } | 627 } |
602 | 628 |
603 /** | 629 /** |
604 * Sets the SearchManager singleton instance, useful for testing. | 630 * Sets the SearchManager singleton instance, useful for testing. |
605 * @param {!SearchManager} searchManager | 631 * @param {!SearchManager} searchManager |
606 */ | 632 */ |
607 function setSearchManagerForTesting(searchManager) { | 633 function setSearchManagerForTesting(searchManager) { |
608 SearchManagerImpl.instance_ = searchManager; | 634 SearchManagerImpl.instance_ = searchManager; |
609 } | 635 } |
610 | 636 |
611 return { | 637 return { |
612 getSearchManager: getSearchManager, | 638 getSearchManager: getSearchManager, |
613 setSearchManagerForTesting: setSearchManagerForTesting, | 639 setSearchManagerForTesting: setSearchManagerForTesting, |
614 SearchRequest: SearchRequest, | 640 SearchRequest: SearchRequest, |
615 }; | 641 }; |
616 }); | 642 }); |
OLD | NEW |