| 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 |