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 bubbles. |
| 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(this.updatePosition.bind(this), 250); |
| 36 }, |
| 37 |
| 38 /** |
| 39 * Clear the interval timer and remove the element from the page. |
| 40 */ |
| 41 dispose: function() { |
| 42 clearInterval(this.intervalId); |
| 43 |
| 44 var parent = this.parentNode; |
| 45 if (parent) |
| 46 parent.removeChild(this); |
| 47 }, |
| 48 |
| 49 /** |
| 50 * Update the position of the bubble. Called at creation time and then |
| 51 * periodically while the bubble remains visible. |
| 52 */ |
| 53 updatePosition: function() { |
| 54 // This bubble is 'owned' by the next sibling. |
| 55 var owner = this.nextSibling; |
| 56 |
| 57 // If there isn't an offset parent, we have nothing to do. |
| 58 if (!owner.offsetParent) |
| 59 return; |
| 60 |
| 61 // Position the bubble below the location of the owner. |
| 62 var left = owner.offsetLeft + owner.offsetWidth / 2 - |
| 63 this.offsetWidth / 2; |
| 64 var top = owner.offsetTop + owner.offsetHeight; |
| 65 |
| 66 // Update the position in the CSS. Cache the last values for |
| 67 // best performance. |
| 68 if (left != this.lastLeft) { |
| 69 this.style.left = left + 'px'; |
| 70 this.lastLeft = left; |
| 71 } |
| 72 if (top != this.lastTop) { |
| 73 this.style.top = top + 'px'; |
| 74 this.lastTop = top; |
| 75 } |
| 76 } |
| 77 } |
| 78 |
| 79 /** |
9 * Encapsulated handling of the search page. | 80 * Encapsulated handling of the search page. |
10 * @constructor | 81 * @constructor |
11 */ | 82 */ |
12 function SearchPage() { | 83 function SearchPage() { |
13 OptionsPage.call(this, 'search', templateData.searchPage, 'searchPage'); | 84 OptionsPage.call(this, 'search', templateData.searchPage, 'searchPage'); |
14 this.searchActive = false; | 85 this.searchActive = false; |
15 } | 86 } |
16 | 87 |
17 cr.addSingletonGetter(SearchPage); | 88 cr.addSingletonGetter(SearchPage); |
18 | 89 |
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
101 var pagesToSearch = this.getSearchablePages_(); | 172 var pagesToSearch = this.getSearchablePages_(); |
102 for (var key in pagesToSearch) { | 173 for (var key in pagesToSearch) { |
103 var page = pagesToSearch[key]; | 174 var page = pagesToSearch[key]; |
104 | 175 |
105 if (!active) | 176 if (!active) |
106 page.visible = false; | 177 page.visible = false; |
107 | 178 |
108 // Update the visible state of all top-level elements that are not | 179 // Update the visible state of all top-level elements that are not |
109 // sections (ie titles, button strips). We do this before changing | 180 // sections (ie titles, button strips). We do this before changing |
110 // the page visibility to avoid excessive re-draw. | 181 // the page visibility to avoid excessive re-draw. |
111 var length = page.pageDiv.childNodes.length; | 182 for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) { |
112 var childDiv; | 183 if (active) { |
113 for (var i = 0; i < length; i++) { | 184 if (childDiv.tagName != 'SECTION') |
114 childDiv = page.pageDiv.childNodes[i]; | 185 childDiv.classList.add('search-hidden'); |
115 if (childDiv.nodeType == document.ELEMENT_NODE) { | 186 } else { |
116 if (active) { | 187 childDiv.classList.remove('search-hidden'); |
117 if (childDiv.nodeName.toLowerCase() != 'section') | |
118 childDiv.classList.add('search-hidden'); | |
119 } else { | |
120 childDiv.classList.remove('search-hidden'); | |
121 } | |
122 } | 188 } |
123 } | 189 } |
124 | 190 |
125 if (active) { | 191 if (active) { |
126 // When search is active, remove the 'hidden' tag. This tag may have | 192 // When search is active, remove the 'hidden' tag. This tag may have |
127 // been added by the OptionsPage. | 193 // been added by the OptionsPage. |
128 page.pageDiv.classList.remove('hidden'); | 194 page.pageDiv.classList.remove('hidden'); |
129 } | 195 } |
130 } | 196 } |
131 | 197 |
132 // After hiding all page content, remove any highlighted matches. | 198 // After hiding all page content, remove any search results. |
133 if (!active) | 199 if (!active) { |
134 this.unhighlightMatches_(); | 200 this.unhighlightMatches_(); |
| 201 this.removeSearchBubbles_(); |
| 202 } |
135 }, | 203 }, |
136 | 204 |
137 /** | 205 /** |
138 * Set the current search criteria. | 206 * Set the current search criteria. |
139 * @param {string} text Search text. | 207 * @param {string} text Search text. |
140 * @private | 208 * @private |
141 */ | 209 */ |
142 setSearchText_: function(text) { | 210 setSearchText_: function(text) { |
143 var foundMatches = false; | 211 var foundMatches = false; |
| 212 var bubbleControls = []; |
144 | 213 |
145 // Remove any highlighted matches. | 214 // Remove any prior search results. |
146 this.unhighlightMatches_(); | 215 this.unhighlightMatches_(); |
| 216 this.removeSearchBubbles_(); |
147 | 217 |
148 // Generate search text by applying lowercase and escaping any characters | 218 // Generate search text by applying lowercase and escaping any characters |
149 // that would be problematic for regular expressions. | 219 // that would be problematic for regular expressions. |
150 var searchText = | 220 var searchText = |
151 text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); | 221 text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); |
152 | 222 |
153 // Generate a regular expression and replace string for hilighting | 223 // Generate a regular expression and replace string for hilighting |
154 // search terms. | 224 // search terms. |
155 var regEx = new RegExp('(\\b' + searchText + ')', 'ig'); | 225 var regEx = new RegExp('(\\b' + searchText + ')', 'ig'); |
156 var replaceString = '<span class="search-highlighted">$1</span>'; | 226 var replaceString = '<span class="search-highlighted">$1</span>'; |
157 | 227 |
158 // Initialize all sections. If the search string matches a title page, | 228 // Initialize all sections. If the search string matches a title page, |
159 // show sections for that page. | 229 // show sections for that page. |
160 var page, pageMatch, childDiv; | 230 var page, pageMatch, childDiv, length; |
161 var pagesToSearch = this.getSearchablePages_(); | 231 var pagesToSearch = this.getSearchablePages_(); |
162 for (var key in pagesToSearch) { | 232 for (var key in pagesToSearch) { |
163 page = pagesToSearch[key]; | 233 page = pagesToSearch[key]; |
164 pageMatch = false; | 234 pageMatch = false; |
165 if (searchText.length) { | 235 if (searchText.length) { |
166 pageMatch = this.performReplace_(regEx, replaceString, page.tab); | 236 pageMatch = this.performReplace_(regEx, replaceString, page.tab); |
167 } | 237 } |
168 if (pageMatch) | 238 if (pageMatch) |
169 foundMatches = true; | 239 foundMatches = true; |
170 for (var i = 0; i < page.pageDiv.childNodes.length; i++) { | 240 for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) { |
171 childDiv = page.pageDiv.childNodes[i]; | 241 if (childDiv.tagName == 'SECTION') { |
172 if (childDiv.nodeType == document.ELEMENT_NODE && | |
173 childDiv.nodeName == 'SECTION') { | |
174 if (pageMatch) { | 242 if (pageMatch) { |
175 childDiv.classList.remove('search-hidden'); | 243 childDiv.classList.remove('search-hidden'); |
176 } else { | 244 } else { |
177 childDiv.classList.add('search-hidden'); | 245 childDiv.classList.add('search-hidden'); |
178 } | 246 } |
179 } | 247 } |
180 } | 248 } |
181 } | 249 } |
182 | 250 |
183 if (searchText.length) { | 251 if (searchText.length) { |
| 252 // Search all top-level sections for anchored string matches. |
| 253 for (var key in pagesToSearch) { |
| 254 page = pagesToSearch[key]; |
| 255 for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) { |
| 256 if (childDiv.tagName == 'SECTION' && |
| 257 this.performReplace_(regEx, replaceString, childDiv)) { |
| 258 childDiv.classList.remove('search-hidden'); |
| 259 foundMatches = true; |
| 260 } |
| 261 } |
| 262 } |
| 263 |
184 // Search all sub-pages, generating an array of top-level sections that | 264 // Search all sub-pages, generating an array of top-level sections that |
185 // we need to make visible. | 265 // we need to make visible. |
186 var subPagesToSearch = this.getSearchableSubPages_(); | 266 var subPagesToSearch = this.getSearchableSubPages_(); |
187 var control, node; | 267 var control, node; |
188 for (var key in subPagesToSearch) { | 268 for (var key in subPagesToSearch) { |
189 page = subPagesToSearch[key]; | 269 page = subPagesToSearch[key]; |
190 if (this.performReplace_(regEx, replaceString, page.pageDiv)) { | 270 if (this.performReplace_(regEx, replaceString, page.pageDiv)) { |
| 271 // Reveal the section for this search result. |
191 section = page.associatedSection; | 272 section = page.associatedSection; |
192 if (section) | 273 if (section) |
193 section.classList.remove('search-hidden'); | 274 section.classList.remove('search-hidden'); |
194 controls = page.associatedControls; | 275 |
| 276 // Identify any controls that should have bubbles. |
| 277 var controls = page.associatedControls; |
195 if (controls) { | 278 if (controls) { |
196 // TODO(csilv): highlight each control. | 279 length = controls.length; |
| 280 for (var i = 0; i < length; i++) |
| 281 bubbleControls.push(controls[i]); |
197 } | 282 } |
198 | 283 |
199 foundMatches = true; | 284 foundMatches = true; |
200 } | 285 } |
201 } | 286 } |
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 } | 287 } |
217 | 288 |
218 // Configure elements on the search results page based on search results. | 289 // Configure elements on the search results page based on search results. |
219 if (searchText.length == 0) { | 290 if (searchText.length == 0) { |
220 $('searchPageInfo').classList.remove('search-hidden'); | 291 $('searchPageInfo').classList.remove('search-hidden'); |
221 $('searchPageNoMatches').classList.add('search-hidden'); | 292 $('searchPageNoMatches').classList.add('search-hidden'); |
222 } else if (foundMatches) { | 293 } else if (foundMatches) { |
223 $('searchPageInfo').classList.add('search-hidden'); | 294 $('searchPageInfo').classList.add('search-hidden'); |
224 $('searchPageNoMatches').classList.add('search-hidden'); | 295 $('searchPageNoMatches').classList.add('search-hidden'); |
225 } else { | 296 } else { |
226 $('searchPageInfo').classList.add('search-hidden'); | 297 $('searchPageInfo').classList.add('search-hidden'); |
227 $('searchPageNoMatches').classList.remove('search-hidden'); | 298 $('searchPageNoMatches').classList.remove('search-hidden'); |
228 } | 299 } |
| 300 |
| 301 // Create search balloons for sub-page results. |
| 302 length = bubbleControls.length; |
| 303 for (var i = 0; i < length; i++) |
| 304 this.createSearchBubble_(bubbleControls[i], text); |
229 }, | 305 }, |
230 | 306 |
231 /** | 307 /** |
232 * Performs a string replacement based on a regex and replace string. | 308 * Performs a string replacement based on a regex and replace string. |
233 * @param {RegEx} regex A regular expression for finding search matches. | 309 * @param {RegEx} regex A regular expression for finding search matches. |
234 * @param {String} replace A string to apply the replace operation. | 310 * @param {String} replace A string to apply the replace operation. |
235 * @param {Element} element An HTML container element. | 311 * @param {Element} element An HTML container element. |
236 * @returns {Boolean} true if the element was changed. | 312 * @returns {Boolean} true if the element was changed. |
237 * @private | 313 * @private |
238 */ | 314 */ |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
281 | 357 |
282 /** | 358 /** |
283 * Removes all search highlight tags from the document. | 359 * Removes all search highlight tags from the document. |
284 * @private | 360 * @private |
285 */ | 361 */ |
286 unhighlightMatches_: function() { | 362 unhighlightMatches_: function() { |
287 // Find all search highlight elements. | 363 // Find all search highlight elements. |
288 var elements = document.querySelectorAll('.search-highlighted'); | 364 var elements = document.querySelectorAll('.search-highlighted'); |
289 | 365 |
290 // For each element, remove the highlighting. | 366 // For each element, remove the highlighting. |
291 var node, parent, i, length = elements.length; | 367 var parent, i; |
292 for (i = 0; i < length; i++) { | 368 for (var i = 0, node; node = elements[i]; i++) { |
293 node = elements[i]; | |
294 parent = node.parentNode; | 369 parent = node.parentNode; |
295 | 370 |
296 // Replace the highlight element with the first child (the text node). | 371 // Replace the highlight element with the first child (the text node). |
297 parent.replaceChild(node.firstChild, node); | 372 parent.replaceChild(node.firstChild, node); |
298 | 373 |
299 // Normalize the parent so that multiple text nodes will be combined. | 374 // Normalize the parent so that multiple text nodes will be combined. |
300 parent.normalize(); | 375 parent.normalize(); |
301 } | 376 } |
302 }, | 377 }, |
303 | 378 |
304 /** | 379 /** |
| 380 * Creates a search result bubble attached to an element. |
| 381 * @param {Element} element An HTML element, usually a button. |
| 382 * @param {string} text A string to show in the bubble. |
| 383 * @private |
| 384 */ |
| 385 createSearchBubble_: function(element, text) { |
| 386 // avoid appending multiple ballons to a button. |
| 387 var sibling = element.previousElementSibling; |
| 388 if (sibling && sibling.classList.contains('search-bubble')) |
| 389 return; |
| 390 |
| 391 var parent = element.parentElement; |
| 392 if (parent) { |
| 393 var bubble = new SearchBubble(text); |
| 394 parent.insertBefore(bubble, element); |
| 395 bubble.updatePosition(); |
| 396 } |
| 397 }, |
| 398 |
| 399 /** |
| 400 * Removes all search match bubbles. |
| 401 * @private |
| 402 */ |
| 403 removeSearchBubbles_: function() { |
| 404 var elements = document.querySelectorAll('.search-bubble'); |
| 405 var length = elements.length; |
| 406 for (var i = 0; i < length; i++) |
| 407 elements[i].dispose(); |
| 408 }, |
| 409 |
| 410 /** |
305 * Builds a list of top-level pages to search. Omits the search page and | 411 * Builds a list of top-level pages to search. Omits the search page and |
306 * all sub-pages. | 412 * all sub-pages. |
307 * @returns {Array} An array of pages to search. | 413 * @returns {Array} An array of pages to search. |
308 * @private | 414 * @private |
309 */ | 415 */ |
310 getSearchablePages_: function() { | 416 getSearchablePages_: function() { |
311 var name, page, pages = []; | 417 var name, page, pages = []; |
312 for (name in OptionsPage.registeredPages) { | 418 for (name in OptionsPage.registeredPages) { |
313 if (name != this.name) { | 419 if (name != this.name) { |
314 page = OptionsPage.registeredPages[name]; | 420 page = OptionsPage.registeredPages[name]; |
(...skipping 25 matching lines...) Expand all Loading... |
340 return pages; | 446 return pages; |
341 } | 447 } |
342 }; | 448 }; |
343 | 449 |
344 // Export | 450 // Export |
345 return { | 451 return { |
346 SearchPage: SearchPage | 452 SearchPage: SearchPage |
347 }; | 453 }; |
348 | 454 |
349 }); | 455 }); |
OLD | NEW |