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 |