OLD | NEW |
---|---|
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2010 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.define('options', function() { | 5 cr.define('options', function() { |
6 const OptionsPage = options.OptionsPage; | 6 const OptionsPage = options.OptionsPage; |
7 | 7 |
8 /** | 8 /** |
9 * Encapsulated handling of a search bubble. | |
10 * @constructor | |
11 */ | |
12 function SearchBubble(text) { | |
13 var el = cr.doc.createElement('div'); | |
14 SearchBubble.decorate(el); | |
15 el.textContent = text; | |
16 return el; | |
17 } | |
18 | |
19 SearchBubble.decorate = function(el) { | |
20 el.__proto__ = SearchBubble.prototype; | |
21 el.decorate(); | |
22 }; | |
23 | |
24 SearchBubble.prototype = { | |
25 __proto__: HTMLDivElement.prototype, | |
26 | |
27 decorate: function() { | |
28 this.className = 'search-bubble'; | |
29 | |
30 // We create a timer to periodically update the position of the bubble. | |
31 // While this isn't all that desirable, it's the only sure-fire way of | |
32 // making sure the bubbles stay in the correct location as sections | |
33 // may dynamically change size at any time. | |
34 var self = this; | |
35 this.intervalId = setInterval( | |
36 function(e) { self.updatePosition(); }, 250); | |
arv (Not doing code reviews)
2011/01/07 22:09:45
setInterval(this.updatePosition.bind(this), 250);
csilv
2011/01/20 21:40:30
Done.
| |
37 }, | |
38 | |
39 /** | |
40 * Clear the interval timer and remove the element from the page. | |
41 */ | |
42 dispose: function() { | |
43 clearInterval(this.intervalId); | |
44 | |
45 var parent = this.parentNode; | |
46 if (parent) | |
47 parent.removeChild(this); | |
48 }, | |
49 | |
50 /** | |
51 * Update the position of the bubble. Called at creation time and then | |
52 * periodically while the bubble remains visible. | |
53 */ | |
54 updatePosition: function() { | |
55 // This bubble is 'owned' by the next sibling. | |
56 var owner = this.nextSibling; | |
57 | |
58 // If there isn't an offset parent, we have nothing to do. | |
59 if (!owner.offsetParent) | |
60 return; | |
61 | |
62 // Position the bubble below the location of the owner. | |
63 var left = Math.floor(owner.offsetLeft + | |
arv (Not doing code reviews)
2011/01/07 22:09:45
You can skip the floor here. left is not limited t
csilv
2011/01/20 21:40:30
Done.
Coming from other platforms, I was concerne
| |
64 owner.offsetWidth/2 - this.offsetWidth/2); | |
arv (Not doing code reviews)
2011/01/07 22:09:45
ws around binops
csilv
2011/01/20 21:40:30
Done.
| |
65 var top = Math.floor(owner.offsetTop + owner.offsetHeight); | |
66 | |
67 // Update the position in the CSS. Cache the last values for | |
68 // best performance. | |
arv (Not doing code reviews)
2011/01/07 22:09:45
The main slowdown is the offset* followed by setti
| |
69 if (left != this.lastLeft) { | |
70 this.style.left = left + 'px'; | |
71 this.lastLeft = left; | |
72 } | |
73 if (top != this.lastTop) { | |
74 this.style.top = top + 'px'; | |
75 this.lastTop = top; | |
76 } | |
77 } | |
78 } | |
79 | |
80 /** | |
9 * Encapsulated handling of the search page. | 81 * Encapsulated handling of the search page. |
10 * @constructor | 82 * @constructor |
11 */ | 83 */ |
12 function SearchPage() { | 84 function SearchPage() { |
13 OptionsPage.call(this, 'search', templateData.searchPage, 'searchPage'); | 85 OptionsPage.call(this, 'search', templateData.searchPage, 'searchPage'); |
14 this.searchActive = false; | 86 this.searchActive = false; |
15 } | 87 } |
16 | 88 |
17 cr.addSingletonGetter(SearchPage); | 89 cr.addSingletonGetter(SearchPage); |
18 | 90 |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
122 } | 194 } |
123 } | 195 } |
124 | 196 |
125 if (active) { | 197 if (active) { |
126 // When search is active, remove the 'hidden' tag. This tag may have | 198 // When search is active, remove the 'hidden' tag. This tag may have |
127 // been added by the OptionsPage. | 199 // been added by the OptionsPage. |
128 page.pageDiv.classList.remove('hidden'); | 200 page.pageDiv.classList.remove('hidden'); |
129 } | 201 } |
130 } | 202 } |
131 | 203 |
132 // After hiding all page content, remove any highlighted matches. | 204 // After hiding all page content, remove any search results. |
133 if (!active) | 205 if (!active) { |
134 this.unhighlightMatches_(); | 206 this.unhighlightMatches_(); |
207 this.removeSearchBubbles_(); | |
208 } | |
135 }, | 209 }, |
136 | 210 |
137 /** | 211 /** |
138 * Set the current search criteria. | 212 * Set the current search criteria. |
139 * @param {string} text Search text. | 213 * @param {string} text Search text. |
140 * @private | 214 * @private |
141 */ | 215 */ |
142 setSearchText_: function(text) { | 216 setSearchText_: function(text) { |
143 var foundMatches = false; | 217 var foundMatches = false; |
218 var bubbleControls = []; | |
144 | 219 |
145 // Remove any highlighted matches. | 220 // Remove any prior search results. |
146 this.unhighlightMatches_(); | 221 this.unhighlightMatches_(); |
222 this.removeSearchBubbles_(); | |
147 | 223 |
148 // Generate search text by applying lowercase and escaping any characters | 224 // Generate search text by applying lowercase and escaping any characters |
149 // that would be problematic for regular expressions. | 225 // that would be problematic for regular expressions. |
150 var searchText = | 226 var searchText = |
151 text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); | 227 text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); |
152 | 228 |
153 // Generate a regular expression and replace string for hilighting | 229 // Generate a regular expression and replace string for hilighting |
154 // search terms. | 230 // search terms. |
155 var regEx = new RegExp('(\\b' + searchText + ')', 'ig'); | 231 var regEx = new RegExp('(\\b' + searchText + ')', 'ig'); |
156 var replaceString = '<span class="search-highlighted">$1</span>'; | 232 var replaceString = '<span class="search-highlighted">$1</span>'; |
157 | 233 |
158 // Initialize all sections. If the search string matches a title page, | 234 // Initialize all sections. If the search string matches a title page, |
159 // show sections for that page. | 235 // show sections for that page. |
160 var page, pageMatch, childDiv; | 236 var page, pageMatch, childDiv, length; |
161 var pagesToSearch = this.getSearchablePages_(); | 237 var pagesToSearch = this.getSearchablePages_(); |
162 for (var key in pagesToSearch) { | 238 for (var key in pagesToSearch) { |
163 page = pagesToSearch[key]; | 239 page = pagesToSearch[key]; |
164 pageMatch = false; | 240 pageMatch = false; |
165 if (searchText.length) { | 241 if (searchText.length) { |
166 pageMatch = this.performReplace_(regEx, replaceString, page.tab); | 242 pageMatch = this.performReplace_(regEx, replaceString, page.tab); |
167 } | 243 } |
168 if (pageMatch) | 244 if (pageMatch) |
169 foundMatches = true; | 245 foundMatches = true; |
170 for (var i = 0; i < page.pageDiv.childNodes.length; i++) { | 246 length = page.pageDiv.childNodes.length; |
arv (Not doing code reviews)
2011/01/07 22:09:45
Change this to children and loop over the children
csilv
2011/01/20 21:40:30
Done.
Thanks for pointing this out. 'children' a
| |
247 for (var i = 0; i < length; i++) { | |
171 childDiv = page.pageDiv.childNodes[i]; | 248 childDiv = page.pageDiv.childNodes[i]; |
172 if (childDiv.nodeType == document.ELEMENT_NODE && | 249 if (childDiv.nodeType == document.ELEMENT_NODE && |
173 childDiv.nodeName == 'SECTION') { | 250 childDiv.nodeName == 'SECTION') { |
174 if (pageMatch) { | 251 if (pageMatch) { |
175 childDiv.classList.remove('search-hidden'); | 252 childDiv.classList.remove('search-hidden'); |
176 } else { | 253 } else { |
177 childDiv.classList.add('search-hidden'); | 254 childDiv.classList.add('search-hidden'); |
178 } | 255 } |
179 } | 256 } |
180 } | 257 } |
181 } | 258 } |
182 | 259 |
183 if (searchText.length) { | 260 if (searchText.length) { |
261 // Search all top-level sections for anchored string matches. | |
262 for (var key in pagesToSearch) { | |
263 page = pagesToSearch[key]; | |
264 length = page.pageDiv.childNodes.length; | |
265 for (var i = 0; i < length; i++) { | |
266 childDiv = page.pageDiv.childNodes[i]; | |
arv (Not doing code reviews)
2011/01/07 22:09:45
children
csilv
2011/01/20 21:40:30
Done.
| |
267 if (childDiv.nodeType == document.ELEMENT_NODE && | |
268 childDiv.nodeName == 'SECTION' && | |
269 this.performReplace_(regEx, replaceString, childDiv)) { | |
270 childDiv.classList.remove('search-hidden'); | |
271 foundMatches = true; | |
272 } | |
273 } | |
274 } | |
275 | |
184 // Search all sub-pages, generating an array of top-level sections that | 276 // Search all sub-pages, generating an array of top-level sections that |
185 // we need to make visible. | 277 // we need to make visible. |
186 var subPagesToSearch = this.getSearchableSubPages_(); | 278 var subPagesToSearch = this.getSearchableSubPages_(); |
187 var control, node; | 279 var control, node; |
188 for (var key in subPagesToSearch) { | 280 for (var key in subPagesToSearch) { |
189 page = subPagesToSearch[key]; | 281 page = subPagesToSearch[key]; |
190 if (this.performReplace_(regEx, replaceString, page.pageDiv)) { | 282 if (this.performReplace_(regEx, replaceString, page.pageDiv)) { |
283 // Reveal the section for this search result. | |
191 section = page.associatedSection; | 284 section = page.associatedSection; |
192 if (section) | 285 if (section) |
193 section.classList.remove('search-hidden'); | 286 section.classList.remove('search-hidden'); |
194 controls = page.associatedControls; | 287 |
288 // Identify any controls that should have bubbles. | |
289 var controls = page.associatedControls; | |
195 if (controls) { | 290 if (controls) { |
196 // TODO(csilv): highlight each control. | 291 length = controls.length; |
292 for (var i = 0; i < length; i++) | |
293 bubbleControls.push(controls[i]); | |
197 } | 294 } |
198 | 295 |
199 foundMatches = true; | 296 foundMatches = true; |
200 } | 297 } |
201 } | 298 } |
202 | |
203 // Search all top-level sections for anchored string matches. | |
204 for (var key in pagesToSearch) { | |
205 page = pagesToSearch[key]; | |
206 for (var i = 0; i < page.pageDiv.childNodes.length; i++) { | |
207 childDiv = page.pageDiv.childNodes[i]; | |
208 if (childDiv.nodeType == document.ELEMENT_NODE && | |
209 childDiv.nodeName == 'SECTION' && | |
210 this.performReplace_(regEx, replaceString, childDiv)) { | |
211 childDiv.classList.remove('search-hidden'); | |
212 foundMatches = true; | |
213 } | |
214 } | |
215 } | |
216 } | 299 } |
217 | 300 |
218 // Configure elements on the search results page based on search results. | 301 // Configure elements on the search results page based on search results. |
219 if (searchText.length == 0) { | 302 if (searchText.length == 0) { |
220 $('searchPageInfo').classList.remove('search-hidden'); | 303 $('searchPageInfo').classList.remove('search-hidden'); |
221 $('searchPageNoMatches').classList.add('search-hidden'); | 304 $('searchPageNoMatches').classList.add('search-hidden'); |
222 } else if (foundMatches) { | 305 } else if (foundMatches) { |
223 $('searchPageInfo').classList.add('search-hidden'); | 306 $('searchPageInfo').classList.add('search-hidden'); |
224 $('searchPageNoMatches').classList.add('search-hidden'); | 307 $('searchPageNoMatches').classList.add('search-hidden'); |
225 } else { | 308 } else { |
226 $('searchPageInfo').classList.add('search-hidden'); | 309 $('searchPageInfo').classList.add('search-hidden'); |
227 $('searchPageNoMatches').classList.remove('search-hidden'); | 310 $('searchPageNoMatches').classList.remove('search-hidden'); |
228 } | 311 } |
312 | |
313 // Create search balloons for sub-page results. | |
314 length = bubbleControls.length; | |
315 for (var i = 0; i < length; i++) | |
316 this.createSearchBubble_(bubbleControls[i], text); | |
229 }, | 317 }, |
230 | 318 |
231 /** | 319 /** |
232 * Performs a string replacement based on a regex and replace string. | 320 * Performs a string replacement based on a regex and replace string. |
233 * @param {RegEx} regex A regular expression for finding search matches. | 321 * @param {RegEx} regex A regular expression for finding search matches. |
234 * @param {String} replace A string to apply the replace operation. | 322 * @param {String} replace A string to apply the replace operation. |
235 * @param {Element} element An HTML container element. | 323 * @param {Element} element An HTML container element. |
236 * @returns {Boolean} true if the element was changed. | 324 * @returns {Boolean} true if the element was changed. |
237 * @private | 325 * @private |
238 */ | 326 */ |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
295 | 383 |
296 // Replace the highlight element with the first child (the text node). | 384 // Replace the highlight element with the first child (the text node). |
297 parent.replaceChild(node.firstChild, node); | 385 parent.replaceChild(node.firstChild, node); |
298 | 386 |
299 // Normalize the parent so that multiple text nodes will be combined. | 387 // Normalize the parent so that multiple text nodes will be combined. |
300 parent.normalize(); | 388 parent.normalize(); |
301 } | 389 } |
302 }, | 390 }, |
303 | 391 |
304 /** | 392 /** |
393 * Creates a search result bubble attached to an element. | |
394 * @param {Element} element An HTML element, usually a button. | |
395 * @param {string} text A string to show in the bubble. | |
396 * @private | |
397 */ | |
398 createSearchBubble_: function(element, text) { | |
399 // avoid appending multiple ballons to a button. | |
400 var sibling = element.previousSibling; | |
401 if (sibling && sibling.classList && | |
402 sibling.classList.contains('search-bubble')) | |
403 return; | |
404 | |
405 var parent = element.parentNode; | |
406 if (parent) { | |
407 var bubble = SearchBubble(text); | |
408 parent.insertBefore(bubble, element); | |
409 bubble.updatePosition(); | |
410 } | |
411 }, | |
412 | |
413 /** | |
414 * Removes all search match bubbles. | |
415 * @private | |
416 */ | |
417 removeSearchBubbles_: function() { | |
418 var elements = document.querySelectorAll('.search-bubble'); | |
419 var length = elements.length; | |
420 for (var i = 0; i < length; i++) | |
421 elements[i].dispose(); | |
422 }, | |
423 | |
424 /** | |
305 * Builds a list of top-level pages to search. Omits the search page and | 425 * Builds a list of top-level pages to search. Omits the search page and |
306 * all sub-pages. | 426 * all sub-pages. |
307 * @returns {Array} An array of pages to search. | 427 * @returns {Array} An array of pages to search. |
308 * @private | 428 * @private |
309 */ | 429 */ |
310 getSearchablePages_: function() { | 430 getSearchablePages_: function() { |
311 var name, page, pages = []; | 431 var name, page, pages = []; |
312 for (name in OptionsPage.registeredPages) { | 432 for (name in OptionsPage.registeredPages) { |
313 if (name != this.name) { | 433 if (name != this.name) { |
314 page = OptionsPage.registeredPages[name]; | 434 page = OptionsPage.registeredPages[name]; |
(...skipping 25 matching lines...) Expand all Loading... | |
340 return pages; | 460 return pages; |
341 } | 461 } |
342 }; | 462 }; |
343 | 463 |
344 // Export | 464 // Export |
345 return { | 465 return { |
346 SearchPage: SearchPage | 466 SearchPage: SearchPage |
347 }; | 467 }; |
348 | 468 |
349 }); | 469 }); |
OLD | NEW |