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