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 {{ | |
| 7 * id: number, | |
| 8 * rawQuery: ?string, | |
| 9 * regExp: ?RegExp | |
| 10 * }} | |
| 11 */ | |
| 12 var SearchContext; | |
| 13 | |
| 5 cr.define('settings', function() { | 14 cr.define('settings', function() { |
| 6 /** @const {string} */ | 15 /** @const {string} */ |
| 7 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; | 16 var WRAPPER_CSS_CLASS = 'search-highlight-wrapper'; |
| 8 | 17 |
| 9 /** @const {string} */ | 18 /** @const {string} */ |
| 10 var HIT_CSS_CLASS = 'search-highlight-hit'; | 19 var HIT_CSS_CLASS = 'search-highlight-hit'; |
| 11 | 20 |
| 12 /** @const {!RegExp} */ | 21 /** @const {!RegExp} */ |
| 13 var SANITIZE_REGEX = /[-[\]{}()*+?.,\\^$|#\s]/g; | 22 var SANITIZE_REGEX = /[-[\]{}()*+?.,\\^$|#\s]/g; |
| 14 | 23 |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 90 var span = document.createElement('span'); | 99 var span = document.createElement('span'); |
| 91 span.classList.add(HIT_CSS_CLASS); | 100 span.classList.add(HIT_CSS_CLASS); |
| 92 span.style.backgroundColor = 'yellow'; | 101 span.style.backgroundColor = 'yellow'; |
| 93 span.textContent = tokens[i]; | 102 span.textContent = tokens[i]; |
| 94 wrapper.appendChild(span); | 103 wrapper.appendChild(span); |
| 95 } | 104 } |
| 96 } | 105 } |
| 97 } | 106 } |
| 98 | 107 |
| 99 /** | 108 /** |
| 109 * Checks whether the given |node| requires force rendering and schedules an | |
| 110 * async RenderTask if necessary. | |
| 111 * | |
| 112 * @param {!SearchContext} context | |
| 113 * @param {!Node} node | |
| 114 * @return {boolean} Whether a forced rendering task was scheduled. | |
| 115 * @private | |
| 116 */ | |
| 117 function forceRenderNeeded_(context, node) { | |
| 118 if (node.tagName != 'TEMPLATE' || | |
| 119 node.getAttribute('name') === null || | |
| 120 node.if) { | |
| 121 return false; | |
| 122 } | |
| 123 | |
| 124 // TODO(dpapad): Temporarily ignore site-settings because it throws an | |
| 125 // assertion error during force-rendering. | |
| 126 if (node.getAttribute('name').indexOf('site-') != -1) | |
| 127 return false; | |
| 128 | |
| 129 SearchManager.getInstance().queue_.addRenderTask( | |
| 130 new RenderTask(context, node)); | |
| 131 return true; | |
| 132 } | |
| 133 | |
| 134 /** | |
| 100 * Traverses the entire DOM (including Shadow DOM), finds text nodes that | 135 * Traverses the entire DOM (including Shadow DOM), finds text nodes that |
| 101 * match the given regular expression and applies the highlight UI. It also | 136 * match the given regular expression and applies the highlight UI. It also |
| 102 * ensures that <settings-section> instances become visible if any matches | 137 * ensures that <settings-section> instances become visible if any matches |
| 103 * occurred under their subtree. | 138 * occurred under their subtree. |
| 104 * | 139 * |
| 105 * @param {!Element} page The page to be searched, should be either | 140 * @param {!SearchContext} context |
| 106 * <settings-basic-page> or <settings-advanced-page>. | 141 * @param {!Node} root The root of the sub-tree to be searched |
| 107 * @param {!RegExp} regExp The regular expression to detect matches. | |
| 108 * @private | 142 * @private |
| 109 */ | 143 */ |
| 110 function findAndHighlightMatches_(page, regExp) { | 144 function findAndHighlightMatches_(context, root) { |
| 111 function doSearch(node) { | 145 function doSearch(node) { |
| 146 if (forceRenderNeeded_(context, node)) | |
| 147 return; | |
| 148 | |
| 112 if (IGNORED_ELEMENTS.has(node.tagName)) | 149 if (IGNORED_ELEMENTS.has(node.tagName)) |
| 113 return; | 150 return; |
| 114 | 151 |
| 115 if (node.nodeType == Node.TEXT_NODE) { | 152 if (node.nodeType == Node.TEXT_NODE) { |
| 116 var textContent = node.nodeValue.trim(); | 153 var textContent = node.nodeValue.trim(); |
| 117 if (textContent.length == 0) | 154 if (textContent.length == 0) |
| 118 return; | 155 return; |
| 119 | 156 |
| 120 if (regExp.test(textContent)) { | 157 if (context.regExp.test(textContent)) { |
| 121 revealParentSection_(node); | 158 revealParentSection_(node); |
| 122 highlight_(node, textContent.split(regExp)); | 159 highlight_(node, textContent.split(context.regExp)); |
| 123 } | 160 } |
| 124 // Returning early since TEXT_NODE nodes never have children. | 161 // Returning early since TEXT_NODE nodes never have children. |
| 125 return; | 162 return; |
| 126 } | 163 } |
| 127 | 164 |
| 128 var child = node.firstChild; | 165 var child = node.firstChild; |
| 129 while (child !== null) { | 166 while (child !== null) { |
| 130 // Getting a reference to the |nextSibling| before calling doSearch() | 167 // Getting a reference to the |nextSibling| before calling doSearch() |
| 131 // because |child| could be removed from the DOM within doSearch(). | 168 // because |child| could be removed from the DOM within doSearch(). |
| 132 var nextSibling = child.nextSibling; | 169 var nextSibling = child.nextSibling; |
| 133 doSearch(child); | 170 doSearch(child); |
| 134 child = nextSibling; | 171 child = nextSibling; |
| 135 } | 172 } |
| 136 | 173 |
| 137 var shadowRoot = node.shadowRoot; | 174 var shadowRoot = node.shadowRoot; |
| 138 if (shadowRoot) | 175 if (shadowRoot) |
| 139 doSearch(shadowRoot); | 176 doSearch(shadowRoot); |
| 140 } | 177 } |
| 141 | 178 |
| 142 doSearch(page); | 179 doSearch(root); |
| 143 } | 180 } |
| 144 | 181 |
| 145 /** | 182 /** |
| 146 * Finds and makes visible the <settings-section> parent of |node|. | 183 * Finds and makes visible the <settings-section> parent of |node|. |
| 147 * @param {!Node} node | 184 * @param {!Node} node |
| 148 */ | 185 */ |
| 149 function revealParentSection_(node) { | 186 function revealParentSection_(node) { |
| 150 // Find corresponding SETTINGS-SECTION parent and make it visible. | 187 // Find corresponding SETTINGS-SECTION parent and make it visible. |
| 151 var parent = node; | 188 var parent = node; |
| 152 while (parent && parent.tagName !== 'SETTINGS-SECTION') { | 189 while (parent && parent.tagName !== 'SETTINGS-SECTION') { |
| 153 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? | 190 parent = parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? |
| 154 parent.host : parent.parentNode; | 191 parent.host : parent.parentNode; |
| 155 } | 192 } |
| 156 if (parent) | 193 if (parent) |
| 157 parent.hidden = false; | 194 parent.hidden = false; |
| 158 } | 195 } |
| 159 | 196 |
| 160 /** | 197 /** |
| 161 * @param {!Element} page | 198 * @param {!Node} page |
| 162 * @param {boolean} visible | 199 * @param {boolean} visible |
| 163 * @private | 200 * @private |
| 164 */ | 201 */ |
| 165 function setSectionsVisibility_(page, visible) { | 202 function setSectionsVisibility_(page, visible) { |
| 166 var sections = Polymer.dom(page.root).querySelectorAll('settings-section'); | 203 var sections = Polymer.dom(page.root).querySelectorAll('settings-section'); |
| 167 for (var i = 0; i < sections.length; i++) | 204 for (var i = 0; i < sections.length; i++) |
| 168 sections[i].hidden = !visible; | 205 sections[i].hidden = !visible; |
| 169 } | 206 } |
| 170 | 207 |
| 171 /** | 208 /** |
| 172 * Performs hierarchical search, starting at the given page element. | 209 * @constructor |
| 210 * | |
| 211 * @param {!SearchContext} context | |
| 212 * @param {!Node} node | |
| 213 */ | |
| 214 function Task(context, node) { | |
| 215 /** @protected {!SearchContext} */ | |
| 216 this.context = context; | |
| 217 | |
| 218 /** @protected {!Node} */ | |
| 219 this.node = node; | |
| 220 } | |
| 221 | |
| 222 Task.prototype = { | |
| 223 /** | |
| 224 * @abstract | |
| 225 * @return {!Promise} | |
| 226 */ | |
| 227 exec: function() {}, | |
| 228 }; | |
| 229 | |
| 230 /** | |
| 231 * @constructor | |
| 232 * @extends {Task} | |
| 233 * | |
| 234 * @param {!SearchContext} context | |
| 235 * @param {!Node} node | |
| 236 */ | |
| 237 function RenderTask(context, node) { | |
| 238 Task.call(this, context, node); | |
| 239 } | |
| 240 | |
| 241 RenderTask.prototype = { | |
| 242 /** @override */ | |
| 243 exec: function() { | |
| 244 var subpageTemplate = | |
| 245 this.node['_content'].querySelector('settings-subpage'); | |
| 246 subpageTemplate.id = subpageTemplate.id || this.node.getAttribute('name'); | |
| 247 assert(!this.node.if); | |
| 248 this.node.if = true; | |
| 249 | |
| 250 return this.getRenderedNode_(subpageTemplate.id).then(function(node) { | |
| 251 // Register a SearchTask for the part of the DOM that was just | |
| 252 // rendered. | |
| 253 SearchManager.getInstance().queue_.addSearchTask( | |
| 254 new SearchTask(this.context, node)); | |
| 255 }.bind(this)); | |
| 256 }, | |
| 257 | |
| 258 /** | |
| 259 * @param {string} id | |
| 260 * @return {!Promise<!Node>} | |
| 261 */ | |
| 262 getRenderedNode_: function(id) { | |
| 263 var parent = this.node.parentNode; | |
| 264 return new Promise(function(resolve, reject) { | |
| 265 parent.async(function() { | |
| 266 resolve(parent.querySelector('#' + id)); | |
| 267 }); | |
| 268 }); | |
| 269 }, | |
| 270 }; | |
| 271 | |
| 272 /** | |
| 273 * @constructor | |
| 274 * @extends {Task} | |
| 275 * | |
| 276 * @param {!SearchContext} context | |
| 277 * @param {!Node} node | |
| 278 */ | |
| 279 function SearchTask(context, node) { | |
| 280 Task.call(this, context, node); | |
| 281 } | |
| 282 | |
| 283 SearchTask.prototype = { | |
| 284 /** @override */ | |
| 285 exec: function() { | |
| 286 findAndHighlightMatches_(this.context, this.node); | |
| 287 return Promise.resolve(); | |
| 288 }, | |
| 289 }; | |
| 290 | |
| 291 /** | |
| 292 * @constructor | |
| 293 * @extends {Task} | |
| 294 * | |
| 295 * @param {!SearchContext} context | |
| 296 * @param {!Node} page | |
| 297 */ | |
| 298 function TopLevelSearchTask(context, page) { | |
| 299 Task.call(this, context, page); | |
| 300 } | |
| 301 | |
| 302 TopLevelSearchTask.prototype = { | |
| 303 /** @override */ | |
| 304 exec: function() { | |
| 305 findAndRemoveHighlights_(this.node); | |
| 306 | |
| 307 if (this.context.regExp === null) { | |
| 308 setSectionsVisibility_(this.node, true); | |
| 309 return Promise.resolve(); | |
| 310 } | |
| 311 | |
| 312 setSectionsVisibility_(this.node, false); | |
| 313 findAndHighlightMatches_(this.context, this.node); | |
| 314 return Promise.resolve(); | |
| 315 }, | |
| 316 }; | |
| 317 | |
| 318 /** | |
| 319 * @constructor | |
| 320 */ | |
| 321 function TaskQueue() { | |
| 322 /** | |
| 323 * @private {{ | |
| 324 * high: !Array<!Task>, | |
| 325 * middle: !Array<!Task>, | |
| 326 * low: !Array<!Task> | |
| 327 * }} | |
| 328 */ | |
| 329 this.queues_ = {high: [], middle: [], low: []}; | |
| 330 | |
| 331 /** @private {?Task} */ | |
| 332 this.activeTask_ = null; | |
|
Dan Beam
2016/07/11 21:48:22
i understand why it's helpful to use a task here,
Dan Beam
2016/07/12 00:08:41
ping
dpapad
2016/07/12 02:20:27
Done.
| |
| 333 } | |
| 334 | |
| 335 TaskQueue.prototype = { | |
| 336 /** @param {!TopLevelSearchTask} task */ | |
| 337 addTopLevelSearchTask: function(task) { | |
| 338 this.queues_.high.push(task); | |
| 339 this.consumePending_(); | |
| 340 }, | |
| 341 | |
| 342 /** @param {!SearchTask} task */ | |
| 343 addSearchTask: function(task) { | |
| 344 this.queues_.middle.push(task); | |
| 345 this.consumePending_(); | |
| 346 }, | |
| 347 | |
| 348 /** @param {!RenderTask} task */ | |
| 349 addRenderTask: function(task) { | |
| 350 this.queues_.low.push(task); | |
| 351 this.consumePending_(); | |
| 352 }, | |
| 353 | |
| 354 /** | |
| 355 * @return {?Task} | |
| 356 * @private | |
| 357 */ | |
| 358 popNextTask_: function() { | |
| 359 return this.queues_.high.shift() || | |
| 360 this.queues_.middle.shift() || | |
| 361 this.queues_.low.shift() || null; | |
| 362 }, | |
| 363 | |
| 364 /** @private */ | |
| 365 consumePending_: function() { | |
| 366 if (this.activeTask_) | |
|
Dan Beam
2016/07/11 21:48:22
if (this.running_)
return;
while (1) {
var ta
Dan Beam
2016/07/12 00:08:41
that's not to say that we don't need to check its
dpapad
2016/07/12 02:20:27
Done.
| |
| 367 return; | |
| 368 | |
| 369 var manager = SearchManager.getInstance(); | |
| 370 while (1) { | |
| 371 this.activeTask_ = this.popNextTask_(); | |
| 372 if (!this.activeTask_) | |
| 373 return; | |
| 374 | |
| 375 if (this.activeTask_.context.id != manager.activeContext_.id) { | |
| 376 // Dropping this task without ever executing it, since a new search | |
| 377 // has been issued since this task was submitted. | |
| 378 continue; | |
| 379 } | |
| 380 | |
| 381 window.requestIdleCallback(function() { | |
| 382 this.activeTask_.exec().then(function(result) { | |
| 383 this.activeTask_ = null; | |
| 384 this.consumePending_(); | |
| 385 }.bind(this)); | |
| 386 }.bind(this)); | |
| 387 return; | |
| 388 } | |
| 389 }, | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * @constructor | |
| 394 */ | |
| 395 var SearchManager = function() { | |
| 396 /** @private {!TaskQueue} */ | |
| 397 this.queue_ = new TaskQueue(); | |
| 398 | |
| 399 /** @private {!SearchContext} */ | |
| 400 this.activeContext_ = { | |
| 401 id: 0, rawQuery: null, regExp: null, | |
| 402 }; | |
| 403 }; | |
| 404 cr.addSingletonGetter(SearchManager); | |
| 405 | |
| 406 SearchManager.prototype = { | |
| 407 /** | |
| 408 * @param {string} text The text to search for. | |
| 409 * @param {!Node} page | |
| 410 */ | |
| 411 search: function(text, page) { | |
| 412 if (this.activeContext_.rawQuery != text) { | |
| 413 var newId = this.activeContext_.id + 1; | |
| 414 | |
| 415 var regExp = null; | |
| 416 // Generate search text by escaping any characters that would be | |
| 417 // problematic for regular expressions. | |
| 418 var searchText = text.trim().replace(SANITIZE_REGEX, '\\$&'); | |
| 419 if (searchText.length > 0) | |
| 420 regExp = new RegExp('(' + searchText + ')', 'i'); | |
| 421 | |
| 422 this.activeContext_ = { | |
| 423 id: newId, rawQuery: text, regExp: regExp, | |
| 424 }; | |
| 425 } | |
| 426 | |
| 427 this.queue_.addTopLevelSearchTask( | |
| 428 new TopLevelSearchTask(this.activeContext_, page)); | |
| 429 }, | |
| 430 }; | |
| 431 | |
| 432 /** | |
| 173 * @param {string} text | 433 * @param {string} text |
| 174 * @param {!Element} page Must be either <settings-basic-page> or | 434 * @param {!Node} page |
| 175 * <settings-advanced-page>. | |
| 176 */ | 435 */ |
| 177 function search(text, page) { | 436 function search(text, page) { |
| 178 findAndRemoveHighlights_(page); | 437 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 } | 438 } |
| 191 | 439 |
| 192 return { | 440 return { |
| 193 search: search, | 441 search: search, |
| 194 }; | 442 }; |
| 195 }); | 443 }); |
| OLD | NEW |