Chromium Code Reviews| 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 /** | |
| 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 Loading... | |
| 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 }); |
| OLD | NEW |