OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 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 */ var FocusOutlineManager = cr.ui.FocusOutlineManager; | 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; |
| 7 /** @const */ var PageManager = cr.ui.pageManager.PageManager; |
7 | 8 |
8 ///////////////////////////////////////////////////////////////////////////// | 9 var OptionsPage = { |
9 // OptionsPage class: | 10 /** |
| 11 * This is the absolute difference maintained between standard and |
| 12 * fixed-width font sizes. Refer http://crbug.com/91922. |
| 13 * @const |
| 14 */ |
| 15 SIZE_DIFFERENCE_FIXED_STANDARD: 3, |
10 | 16 |
11 /** | 17 /** |
12 * Base class for options page. | 18 * Initializes the complete options page. This will cause all C++ handlers |
13 * @constructor | 19 * to be invoked to do final setup. |
14 * @param {string} name Options page name. | 20 */ |
15 * @param {string} title Options page title, used for history. | 21 initialize: function() { |
16 * @extends {EventTarget} | 22 chrome.send('coreOptionsInitialize'); |
17 */ | 23 uber.onContentFrameLoaded(); |
18 function OptionsPage(name, title, pageDivName) { | 24 PageManager.initialize(BrowserOptions.getInstance()); |
19 this.name = name; | 25 }, |
20 this.title = title; | |
21 this.pageDivName = pageDivName; | |
22 this.pageDiv = $(this.pageDivName); | |
23 // |pageDiv.page| is set to the page object (this) when the page is visible | |
24 // to track which page is being shown when multiple pages can share the same | |
25 // underlying div. | |
26 this.pageDiv.page = null; | |
27 this.tab = null; | |
28 this.lastFocusedElement = null; | |
29 } | |
30 | 26 |
31 /** | 27 /** |
32 * This is the absolute difference maintained between standard and | 28 * Shows the tab contents for the given navigation tab. |
33 * fixed-width font sizes. Refer http://crbug.com/91922. | 29 * @param {!Element} tab The tab that the user clicked. |
34 * @const | 30 */ |
35 */ | 31 showTab: function(tab) { |
36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; | 32 // Search parents until we find a tab, or the nav bar itself. This allows |
| 33 // tabs to have child nodes, e.g. labels in separately-styled spans. |
| 34 while (tab && !tab.classList.contains('subpages-nav-tabs') && |
| 35 !tab.classList.contains('tab')) { |
| 36 tab = tab.parentNode; |
| 37 } |
| 38 if (!tab || !tab.classList.contains('tab')) |
| 39 return; |
37 | 40 |
38 /** | 41 // Find tab bar of the tab. |
39 * Offset of page container in pixels, to allow room for side menu. | 42 var tabBar = tab; |
40 * Simplified settings pages can override this if they don't use the menu. | 43 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { |
41 * The default (155) comes from -webkit-margin-start in uber_shared.css | 44 tabBar = tabBar.parentNode; |
42 * @private | 45 } |
43 */ | 46 if (!tabBar) |
44 OptionsPage.horizontalOffset = 155; | 47 return; |
45 | 48 |
46 /** | 49 if (tabBar.activeNavTab != null) { |
47 * Main level option pages. Maps lower-case page names to the respective page | 50 tabBar.activeNavTab.classList.remove('active-tab'); |
48 * object. | 51 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. |
49 * @protected | 52 remove('active-tab-contents'); |
50 */ | |
51 OptionsPage.registeredPages = {}; | |
52 | |
53 /** | |
54 * Pages which are meant to behave like modal dialogs. Maps lower-case overlay | |
55 * names to the respective overlay object. | |
56 * @protected | |
57 */ | |
58 OptionsPage.registeredOverlayPages = {}; | |
59 | |
60 /** | |
61 * True if options page is served from a dialog. | |
62 */ | |
63 OptionsPage.isDialog = false; | |
64 | |
65 /** | |
66 * Gets the default page (to be shown on initial load). | |
67 */ | |
68 OptionsPage.getDefaultPage = function() { | |
69 return BrowserOptions.getInstance(); | |
70 }; | |
71 | |
72 /** | |
73 * Shows the default page. | |
74 */ | |
75 OptionsPage.showDefaultPage = function() { | |
76 this.navigateToPage(this.getDefaultPage().name); | |
77 }; | |
78 | |
79 /** | |
80 * "Navigates" to a page, meaning that the page will be shown and the | |
81 * appropriate entry is placed in the history. | |
82 * @param {string} pageName Page name. | |
83 */ | |
84 OptionsPage.navigateToPage = function(pageName) { | |
85 this.showPageByName(pageName, true); | |
86 }; | |
87 | |
88 /** | |
89 * Shows a registered page. This handles both top-level and overlay pages. | |
90 * @param {string} pageName Page name. | |
91 * @param {boolean} updateHistory True if we should update the history after | |
92 * showing the page. | |
93 * @param {Object=} opt_propertyBag An optional bag of properties including | |
94 * replaceState (if history state should be replaced instead of pushed). | |
95 * @private | |
96 */ | |
97 OptionsPage.showPageByName = function(pageName, | |
98 updateHistory, | |
99 opt_propertyBag) { | |
100 // If |opt_propertyBag| is non-truthy, homogenize to object. | |
101 opt_propertyBag = opt_propertyBag || {}; | |
102 | |
103 // If a bubble is currently being shown, hide it. | |
104 this.hideBubble(); | |
105 | |
106 // Find the currently visible root-level page. | |
107 var rootPage = null; | |
108 for (var name in this.registeredPages) { | |
109 var page = this.registeredPages[name]; | |
110 if (page.visible && !page.parentPage) { | |
111 rootPage = page; | |
112 break; | |
113 } | |
114 } | |
115 | |
116 // Find the target page. | |
117 var targetPage = this.registeredPages[pageName.toLowerCase()]; | |
118 if (!targetPage || !targetPage.canShowPage()) { | |
119 // If it's not a page, try it as an overlay. | |
120 if (!targetPage && this.showOverlay_(pageName, rootPage)) { | |
121 if (updateHistory) | |
122 this.updateHistoryState_(!!opt_propertyBag.replaceState); | |
123 this.updateTitle_(); | |
124 return; | |
125 } else { | |
126 targetPage = this.getDefaultPage(); | |
127 } | |
128 } | |
129 | |
130 pageName = targetPage.name.toLowerCase(); | |
131 var targetPageWasVisible = targetPage.visible; | |
132 | |
133 // Determine if the root page is 'sticky', meaning that it | |
134 // shouldn't change when showing an overlay. This can happen for special | |
135 // pages like Search. | |
136 var isRootPageLocked = | |
137 rootPage && rootPage.sticky && targetPage.parentPage; | |
138 | |
139 var allPageNames = Array.prototype.concat.call( | |
140 Object.keys(this.registeredPages), | |
141 Object.keys(this.registeredOverlayPages)); | |
142 | |
143 // Notify pages if they will be hidden. | |
144 for (var i = 0; i < allPageNames.length; ++i) { | |
145 var name = allPageNames[i]; | |
146 var page = this.registeredPages[name] || | |
147 this.registeredOverlayPages[name]; | |
148 if (!page.parentPage && isRootPageLocked) | |
149 continue; | |
150 if (page.willHidePage && name != pageName && | |
151 !page.isAncestorOfPage(targetPage)) { | |
152 page.willHidePage(); | |
153 } | |
154 } | |
155 | |
156 // Update visibilities to show only the hierarchy of the target page. | |
157 for (var i = 0; i < allPageNames.length; ++i) { | |
158 var name = allPageNames[i]; | |
159 var page = this.registeredPages[name] || | |
160 this.registeredOverlayPages[name]; | |
161 if (!page.parentPage && isRootPageLocked) | |
162 continue; | |
163 page.visible = name == pageName || page.isAncestorOfPage(targetPage); | |
164 } | |
165 | |
166 // Update the history and current location. | |
167 if (updateHistory) | |
168 this.updateHistoryState_(!!opt_propertyBag.replaceState); | |
169 | |
170 // Update focus if any other control was focused on the previous page, | |
171 // or the previous page is not known. | |
172 if (document.activeElement != document.body && | |
173 (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { | |
174 targetPage.focus(); | |
175 } | |
176 | |
177 // Notify pages if they were shown. | |
178 for (var i = 0; i < allPageNames.length; ++i) { | |
179 var name = allPageNames[i]; | |
180 var page = this.registeredPages[name] || | |
181 this.registeredOverlayPages[name]; | |
182 if (!page.parentPage && isRootPageLocked) | |
183 continue; | |
184 if (!targetPageWasVisible && page.didShowPage && | |
185 (name == pageName || page.isAncestorOfPage(targetPage))) { | |
186 page.didShowPage(); | |
187 } | |
188 } | |
189 | |
190 // Update the document title. Do this after didShowPage was called, in case | |
191 // a page decides to change its title. | |
192 this.updateTitle_(); | |
193 }; | |
194 | |
195 /** | |
196 * Scrolls the page to the correct position (the top when opening an overlay, | |
197 * or the old scroll position a previously hidden overlay becomes visible). | |
198 * @private | |
199 */ | |
200 OptionsPage.updateScrollPosition_ = function() { | |
201 var container = $('page-container'); | |
202 var scrollTop = container.oldScrollTop || 0; | |
203 container.oldScrollTop = undefined; | |
204 window.scroll(scrollLeftForDocument(document), scrollTop); | |
205 }; | |
206 | |
207 /** | |
208 * Updates the title to title of the current page. | |
209 * @private | |
210 */ | |
211 OptionsPage.updateTitle_ = function() { | |
212 var page = this.getTopmostVisiblePage(); | |
213 uber.setTitle(page.title); | |
214 }; | |
215 | |
216 /** | |
217 * Pushes the current page onto the history stack, replacing the current entry | |
218 * if appropriate. | |
219 * @param {boolean} replace If true, allow no history events to be created. | |
220 * @param {object=} opt_params A bag of optional params, including: | |
221 * {boolean} ignoreHash Whether to include the hash or not. | |
222 * @private | |
223 */ | |
224 OptionsPage.updateHistoryState_ = function(replace, opt_params) { | |
225 if (OptionsPage.isDialog) | |
226 return; | |
227 | |
228 var page = this.getTopmostVisiblePage(); | |
229 var path = window.location.pathname + window.location.hash; | |
230 if (path) | |
231 path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash. | |
232 | |
233 // If the page is already in history (the user may have clicked the same | |
234 // link twice, or this is the initial load), do nothing. | |
235 var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash; | |
236 var newPath = (page == this.getDefaultPage() ? '' : page.name) + hash; | |
237 if (path == newPath) | |
238 return; | |
239 | |
240 var historyFunction = replace ? uber.replaceState : uber.pushState; | |
241 historyFunction.call(uber, {}, newPath); | |
242 }; | |
243 | |
244 /** | |
245 * Shows a registered Overlay page. Does not update history. | |
246 * @param {string} overlayName Page name. | |
247 * @param {OptionPage} rootPage The currently visible root-level page. | |
248 * @return {boolean} whether we showed an overlay. | |
249 */ | |
250 OptionsPage.showOverlay_ = function(overlayName, rootPage) { | |
251 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; | |
252 if (!overlay || !overlay.canShowPage()) | |
253 return false; | |
254 | |
255 // Save the currently focused element in the page for restoration later. | |
256 var currentPage = this.getTopmostVisiblePage(); | |
257 if (currentPage) | |
258 currentPage.lastFocusedElement = document.activeElement; | |
259 | |
260 if ((!rootPage || !rootPage.sticky) && | |
261 overlay.parentPage && | |
262 !overlay.parentPage.visible) { | |
263 this.showPageByName(overlay.parentPage.name, false); | |
264 } | |
265 | |
266 if (!overlay.visible) { | |
267 overlay.visible = true; | |
268 if (overlay.didShowPage) overlay.didShowPage(); | |
269 } | |
270 | |
271 // Change focus to the overlay if any other control was focused by keyboard | |
272 // before. Otherwise, no one should have focus. | |
273 if (document.activeElement != document.body) { | |
274 if (FocusOutlineManager.forDocument(document).visible) { | |
275 overlay.focus(); | |
276 } else if (!overlay.pageDiv.contains(document.activeElement)) { | |
277 document.activeElement.blur(); | |
278 } | |
279 } | |
280 | |
281 if ($('search-field') && $('search-field').value == '') { | |
282 var section = overlay.associatedSection; | |
283 if (section) | |
284 options.BrowserOptions.scrollToSection(section); | |
285 } | |
286 | |
287 return true; | |
288 }; | |
289 | |
290 /** | |
291 * Returns whether or not an overlay is visible. | |
292 * @return {boolean} True if an overlay is visible. | |
293 * @private | |
294 */ | |
295 OptionsPage.isOverlayVisible_ = function() { | |
296 return this.getVisibleOverlay_() != null; | |
297 }; | |
298 | |
299 /** | |
300 * Returns the currently visible overlay, or null if no page is visible. | |
301 * @return {OptionPage} The visible overlay. | |
302 */ | |
303 OptionsPage.getVisibleOverlay_ = function() { | |
304 var topmostPage = null; | |
305 for (var name in this.registeredOverlayPages) { | |
306 var page = this.registeredOverlayPages[name]; | |
307 if (page.visible && | |
308 (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) { | |
309 topmostPage = page; | |
310 } | |
311 } | |
312 return topmostPage; | |
313 }; | |
314 | |
315 /** | |
316 * Restores the last focused element on a given page. | |
317 */ | |
318 OptionsPage.restoreLastFocusedElement_ = function() { | |
319 var currentPage = this.getTopmostVisiblePage(); | |
320 if (currentPage.lastFocusedElement) | |
321 currentPage.lastFocusedElement.focus(); | |
322 }; | |
323 | |
324 /** | |
325 * Closes the visible overlay. Updates the history state after closing the | |
326 * overlay. | |
327 */ | |
328 OptionsPage.closeOverlay = function() { | |
329 var overlay = this.getVisibleOverlay_(); | |
330 if (!overlay) | |
331 return; | |
332 | |
333 overlay.visible = false; | |
334 | |
335 if (overlay.didClosePage) overlay.didClosePage(); | |
336 this.updateHistoryState_(false, {ignoreHash: true}); | |
337 this.updateTitle_(); | |
338 | |
339 this.restoreLastFocusedElement_(); | |
340 }; | |
341 | |
342 /** | |
343 * Closes all overlays and updates the history after each closed overlay. | |
344 */ | |
345 OptionsPage.closeAllOverlays = function() { | |
346 while (this.isOverlayVisible_()) { | |
347 this.closeOverlay(); | |
348 } | |
349 }; | |
350 | |
351 /** | |
352 * Cancels (closes) the overlay, due to the user pressing <Esc>. | |
353 */ | |
354 OptionsPage.cancelOverlay = function() { | |
355 // Blur the active element to ensure any changed pref value is saved. | |
356 document.activeElement.blur(); | |
357 var overlay = this.getVisibleOverlay_(); | |
358 if (!overlay) | |
359 return; | |
360 // Let the overlay handle the <Esc> if it wants to. | |
361 if (overlay.handleCancel) { | |
362 overlay.handleCancel(); | |
363 this.restoreLastFocusedElement_(); | |
364 } else { | |
365 this.closeOverlay(); | |
366 } | |
367 }; | |
368 | |
369 /** | |
370 * Hides the visible overlay. Does not affect the history state. | |
371 * @private | |
372 */ | |
373 OptionsPage.hideOverlay_ = function() { | |
374 var overlay = this.getVisibleOverlay_(); | |
375 if (overlay) | |
376 overlay.visible = false; | |
377 }; | |
378 | |
379 /** | |
380 * Returns the pages which are currently visible, ordered by nesting level | |
381 * (ascending). | |
382 * @return {Array.OptionPage} The pages which are currently visible, ordered | |
383 * by nesting level (ascending). | |
384 */ | |
385 OptionsPage.getVisiblePages_ = function() { | |
386 var visiblePages = []; | |
387 for (var name in this.registeredPages) { | |
388 var page = this.registeredPages[name]; | |
389 if (page.visible) | |
390 visiblePages[page.nestingLevel] = page; | |
391 } | |
392 return visiblePages; | |
393 }; | |
394 | |
395 /** | |
396 * Returns the topmost visible page (overlays excluded). | |
397 * @return {OptionPage} The topmost visible page aside any overlay. | |
398 * @private | |
399 */ | |
400 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { | |
401 var topPage = null; | |
402 for (var name in this.registeredPages) { | |
403 var page = this.registeredPages[name]; | |
404 if (page.visible && | |
405 (!topPage || page.nestingLevel > topPage.nestingLevel)) | |
406 topPage = page; | |
407 } | |
408 | |
409 return topPage; | |
410 }; | |
411 | |
412 /** | |
413 * Returns the topmost visible page, or null if no page is visible. | |
414 * @return {OptionPage} The topmost visible page. | |
415 */ | |
416 OptionsPage.getTopmostVisiblePage = function() { | |
417 // Check overlays first since they're top-most if visible. | |
418 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); | |
419 }; | |
420 | |
421 /** | |
422 * Returns the currently visible bubble, or null if no bubble is visible. | |
423 * @return {AutoCloseBubble} The bubble currently being shown. | |
424 */ | |
425 OptionsPage.getVisibleBubble = function() { | |
426 var bubble = OptionsPage.bubble_; | |
427 return bubble && !bubble.hidden ? bubble : null; | |
428 }; | |
429 | |
430 /** | |
431 * Shows an informational bubble displaying |content| and pointing at the | |
432 * |target| element. If |content| has focusable elements, they join the | |
433 * current page's tab order as siblings of |domSibling|. | |
434 * @param {HTMLDivElement} content The content of the bubble. | |
435 * @param {HTMLElement} target The element at which the bubble points. | |
436 * @param {HTMLElement} domSibling The element after which the bubble is added | |
437 * to the DOM. | |
438 * @param {cr.ui.ArrowLocation} location The arrow location. | |
439 */ | |
440 OptionsPage.showBubble = function(content, target, domSibling, location) { | |
441 OptionsPage.hideBubble(); | |
442 | |
443 var bubble = new cr.ui.AutoCloseBubble; | |
444 bubble.anchorNode = target; | |
445 bubble.domSibling = domSibling; | |
446 bubble.arrowLocation = location; | |
447 bubble.content = content; | |
448 bubble.show(); | |
449 OptionsPage.bubble_ = bubble; | |
450 }; | |
451 | |
452 /** | |
453 * Hides the currently visible bubble, if any. | |
454 */ | |
455 OptionsPage.hideBubble = function() { | |
456 if (OptionsPage.bubble_) | |
457 OptionsPage.bubble_.hide(); | |
458 }; | |
459 | |
460 /** | |
461 * Shows the tab contents for the given navigation tab. | |
462 * @param {!Element} tab The tab that the user clicked. | |
463 */ | |
464 OptionsPage.showTab = function(tab) { | |
465 // Search parents until we find a tab, or the nav bar itself. This allows | |
466 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
467 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
468 !tab.classList.contains('tab')) { | |
469 tab = tab.parentNode; | |
470 } | |
471 if (!tab || !tab.classList.contains('tab')) | |
472 return; | |
473 | |
474 // Find tab bar of the tab. | |
475 var tabBar = tab; | |
476 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { | |
477 tabBar = tabBar.parentNode; | |
478 } | |
479 if (!tabBar) | |
480 return; | |
481 | |
482 if (tabBar.activeNavTab != null) { | |
483 tabBar.activeNavTab.classList.remove('active-tab'); | |
484 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. | |
485 remove('active-tab-contents'); | |
486 } | |
487 | |
488 tab.classList.add('active-tab'); | |
489 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); | |
490 tabBar.activeNavTab = tab; | |
491 }; | |
492 | |
493 /** | |
494 * Registers new options page. | |
495 * @param {OptionsPage} page Page to register. | |
496 */ | |
497 OptionsPage.register = function(page) { | |
498 this.registeredPages[page.name.toLowerCase()] = page; | |
499 page.initializePage(); | |
500 }; | |
501 | |
502 /** | |
503 * Find an enclosing section for an element if it exists. | |
504 * @param {Element} element Element to search. | |
505 * @return {OptionPage} The section element, or null. | |
506 * @private | |
507 */ | |
508 OptionsPage.findSectionForNode_ = function(node) { | |
509 while (node = node.parentNode) { | |
510 if (node.nodeName == 'SECTION') | |
511 return node; | |
512 } | |
513 return null; | |
514 }; | |
515 | |
516 /** | |
517 * Registers a new Overlay page. | |
518 * @param {OptionsPage} overlay Overlay to register. | |
519 * @param {OptionsPage} parentPage Associated parent page for this overlay. | |
520 * @param {Array} associatedControls Array of control elements associated with | |
521 * this page. | |
522 */ | |
523 OptionsPage.registerOverlay = function(overlay, | |
524 parentPage, | |
525 associatedControls) { | |
526 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; | |
527 overlay.parentPage = parentPage; | |
528 if (associatedControls) { | |
529 overlay.associatedControls = associatedControls; | |
530 if (associatedControls.length) { | |
531 overlay.associatedSection = | |
532 this.findSectionForNode_(associatedControls[0]); | |
533 } | 53 } |
534 | 54 |
535 // Sanity check. | 55 tab.classList.add('active-tab'); |
536 for (var i = 0; i < associatedControls.length; ++i) { | 56 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); |
537 assert(associatedControls[i], 'Invalid element passed.'); | 57 tabBar.activeNavTab = tab; |
538 } | 58 }, |
539 } | |
540 | |
541 // Reverse the button strip for Windows and CrOS. See the documentation of | |
542 // reverseButtonStripIfNecessary_() for an explanation of why this is done. | |
543 if (cr.isWindows || cr.isChromeOS) | |
544 this.reverseButtonStripIfNecessary_(overlay); | |
545 | |
546 overlay.tab = undefined; | |
547 overlay.isOverlay = true; | |
548 overlay.initializePage(); | |
549 }; | |
550 | |
551 /** | |
552 * Reverses the child elements of a button strip if it hasn't already been | |
553 * reversed. This is necessary because WebKit does not alter the tab order for | |
554 * elements that are visually reversed using -webkit-box-direction: reverse, | |
555 * and the button order is reversed for views. See http://webk.it/62664 for | |
556 * more information. | |
557 * @param {Object} overlay The overlay containing the button strip to reverse. | |
558 * @private | |
559 */ | |
560 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { | |
561 var buttonStrips = | |
562 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); | |
563 | |
564 // Reverse all button-strips in the overlay. | |
565 for (var j = 0; j < buttonStrips.length; j++) { | |
566 var buttonStrip = buttonStrips[j]; | |
567 | |
568 var childNodes = buttonStrip.childNodes; | |
569 for (var i = childNodes.length - 1; i >= 0; i--) | |
570 buttonStrip.appendChild(childNodes[i]); | |
571 | |
572 buttonStrip.setAttribute('reversed', ''); | |
573 } | |
574 }; | |
575 | |
576 /** | |
577 * Returns the name of the page from the current path. | |
578 */ | |
579 OptionsPage.getPageNameFromPath = function() { | |
580 var path = location.pathname; | |
581 if (path.length <= 1) | |
582 return this.getDefaultPage().name; | |
583 | |
584 // Skip starting slash and remove trailing slash (if any). | |
585 return path.slice(1).replace(/\/$/, ''); | |
586 }; | |
587 | |
588 /** | |
589 * Callback for window.onpopstate to handle back/forward navigations. | |
590 * @param {string} pageName The current page name. | |
591 * @param {Object} data State data pushed into history. | |
592 */ | |
593 OptionsPage.setState = function(pageName, data) { | |
594 var currentOverlay = this.getVisibleOverlay_(); | |
595 var lowercaseName = pageName.toLowerCase(); | |
596 var newPage = this.registeredPages[lowercaseName] || | |
597 this.registeredOverlayPages[lowercaseName] || | |
598 this.getDefaultPage(); | |
599 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { | |
600 currentOverlay.visible = false; | |
601 if (currentOverlay.didClosePage) currentOverlay.didClosePage(); | |
602 } | |
603 this.showPageByName(pageName, false); | |
604 }; | |
605 | |
606 /** | |
607 * Callback for window.onbeforeunload. Used to notify overlays that they will | |
608 * be closed. | |
609 */ | |
610 OptionsPage.willClose = function() { | |
611 var overlay = this.getVisibleOverlay_(); | |
612 if (overlay && overlay.didClosePage) | |
613 overlay.didClosePage(); | |
614 }; | |
615 | |
616 /** | |
617 * Freezes/unfreezes the scroll position of the root page container. | |
618 * @param {boolean} freeze Whether the page should be frozen. | |
619 * @private | |
620 */ | |
621 OptionsPage.setRootPageFrozen_ = function(freeze) { | |
622 var container = $('page-container'); | |
623 if (container.classList.contains('frozen') == freeze) | |
624 return; | |
625 | |
626 if (freeze) { | |
627 // Lock the width, since auto width computation may change. | |
628 container.style.width = window.getComputedStyle(container).width; | |
629 container.oldScrollTop = scrollTopForDocument(document); | |
630 container.classList.add('frozen'); | |
631 var verticalPosition = | |
632 container.getBoundingClientRect().top - container.oldScrollTop; | |
633 container.style.top = verticalPosition + 'px'; | |
634 this.updateFrozenElementHorizontalPosition_(container); | |
635 } else { | |
636 container.classList.remove('frozen'); | |
637 container.style.top = ''; | |
638 container.style.left = ''; | |
639 container.style.right = ''; | |
640 container.style.width = ''; | |
641 } | |
642 }; | |
643 | |
644 /** | |
645 * Freezes/unfreezes the scroll position of the root page based on the current | |
646 * page stack. | |
647 */ | |
648 OptionsPage.updateRootPageFreezeState = function() { | |
649 var topPage = OptionsPage.getTopmostVisiblePage(); | |
650 if (topPage) | |
651 this.setRootPageFrozen_(topPage.isOverlay); | |
652 }; | |
653 | |
654 /** | |
655 * Initializes the complete options page. This will cause all C++ handlers to | |
656 * be invoked to do final setup. | |
657 */ | |
658 OptionsPage.initialize = function() { | |
659 chrome.send('coreOptionsInitialize'); | |
660 uber.onContentFrameLoaded(); | |
661 FocusOutlineManager.forDocument(document); | |
662 document.addEventListener('scroll', this.handleScroll_.bind(this)); | |
663 | |
664 // Trigger the scroll handler manually to set the initial state. | |
665 this.handleScroll_(); | |
666 | |
667 // Shake the dialog if the user clicks outside the dialog bounds. | |
668 var containers = [$('overlay-container-1'), $('overlay-container-2')]; | |
669 for (var i = 0; i < containers.length; i++) { | |
670 var overlay = containers[i]; | |
671 cr.ui.overlay.setupOverlay(overlay); | |
672 overlay.addEventListener('cancelOverlay', | |
673 OptionsPage.cancelOverlay.bind(OptionsPage)); | |
674 } | |
675 | |
676 cr.ui.overlay.globalInitialization(); | |
677 }; | |
678 | |
679 /** | |
680 * Does a bounds check for the element on the given x, y client coordinates. | |
681 * @param {Element} e The DOM element. | |
682 * @param {number} x The client X to check. | |
683 * @param {number} y The client Y to check. | |
684 * @return {boolean} True if the point falls within the element's bounds. | |
685 * @private | |
686 */ | |
687 OptionsPage.elementContainsPoint_ = function(e, x, y) { | |
688 var clientRect = e.getBoundingClientRect(); | |
689 return x >= clientRect.left && x <= clientRect.right && | |
690 y >= clientRect.top && y <= clientRect.bottom; | |
691 }; | |
692 | |
693 /** | |
694 * Called when the page is scrolled; moves elements that are position:fixed | |
695 * but should only behave as if they are fixed for vertical scrolling. | |
696 * @private | |
697 */ | |
698 OptionsPage.handleScroll_ = function() { | |
699 this.updateAllFrozenElementPositions_(); | |
700 }; | |
701 | |
702 /** | |
703 * Updates all frozen pages to match the horizontal scroll position. | |
704 * @private | |
705 */ | |
706 OptionsPage.updateAllFrozenElementPositions_ = function() { | |
707 var frozenElements = document.querySelectorAll('.frozen'); | |
708 for (var i = 0; i < frozenElements.length; i++) | |
709 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); | |
710 }; | |
711 | |
712 /** | |
713 * Updates the given frozen element to match the horizontal scroll position. | |
714 * @param {HTMLElement} e The frozen element to update. | |
715 * @private | |
716 */ | |
717 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { | |
718 if (isRTL()) { | |
719 e.style.right = OptionsPage.horizontalOffset + 'px'; | |
720 } else { | |
721 var scrollLeft = scrollLeftForDocument(document); | |
722 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; | |
723 } | |
724 }; | |
725 | |
726 /** | |
727 * Change the horizontal offset used to reposition elements while showing an | |
728 * overlay from the default. | |
729 */ | |
730 OptionsPage.setHorizontalOffset = function(value) { | |
731 OptionsPage.horizontalOffset = value; | |
732 }; | |
733 | |
734 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { | |
735 if (enabled) { | |
736 document.documentElement.setAttribute( | |
737 'flashPluginSupportsClearSiteData', ''); | |
738 } else { | |
739 document.documentElement.removeAttribute( | |
740 'flashPluginSupportsClearSiteData'); | |
741 } | |
742 if (navigator.plugins['Shockwave Flash']) | |
743 document.documentElement.setAttribute('hasFlashPlugin', ''); | |
744 }; | |
745 | |
746 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { | |
747 if (enabled) { | |
748 document.documentElement.setAttribute( | |
749 'enablePepperFlashSettings', ''); | |
750 } else { | |
751 document.documentElement.removeAttribute( | |
752 'enablePepperFlashSettings'); | |
753 } | |
754 }; | |
755 | |
756 OptionsPage.setIsSettingsApp = function() { | |
757 document.documentElement.classList.add('settings-app'); | |
758 }; | |
759 | |
760 OptionsPage.isSettingsApp = function() { | |
761 return document.documentElement.classList.contains('settings-app'); | |
762 }; | |
763 | |
764 /** | |
765 * Whether the page is still loading (i.e. onload hasn't finished running). | |
766 * @return {boolean} Whether the page is still loading. | |
767 */ | |
768 OptionsPage.isLoading = function() { | |
769 return document.documentElement.classList.contains('loading'); | |
770 }; | |
771 | |
772 OptionsPage.prototype = { | |
773 __proto__: cr.EventTarget.prototype, | |
774 | 59 |
775 /** | 60 /** |
776 * The parent page of this option page, or null for top-level pages. | 61 * Shows or hides options for clearing Flash LSOs. |
777 * @type {OptionsPage} | 62 * @param {boolean} enabled Whether plugin data can be cleared. |
778 */ | 63 */ |
779 parentPage: null, | 64 setClearPluginLSODataEnabled: function(enabled) { |
| 65 if (enabled) { |
| 66 document.documentElement.setAttribute( |
| 67 'flashPluginSupportsClearSiteData', ''); |
| 68 } else { |
| 69 document.documentElement.removeAttribute( |
| 70 'flashPluginSupportsClearSiteData'); |
| 71 } |
| 72 if (navigator.plugins['Shockwave Flash']) |
| 73 document.documentElement.setAttribute('hasFlashPlugin', ''); |
| 74 }, |
780 | 75 |
781 /** | 76 /** |
782 * The section on the parent page that is associated with this page. | 77 * Shows or hides Pepper Flash settings. |
783 * Can be null. | 78 * @param {boolean} enabled Whether Pepper Flash settings should be enabled. |
784 * @type {Element} | |
785 */ | 79 */ |
786 associatedSection: null, | 80 setPepperFlashSettingsEnabled: function(enabled) { |
787 | 81 if (enabled) { |
788 /** | 82 document.documentElement.setAttribute( |
789 * An array of controls that are associated with this page. The first | 83 'enablePepperFlashSettings', ''); |
790 * control should be located on a top-level page. | 84 } else { |
791 * @type {OptionsPage} | 85 document.documentElement.removeAttribute( |
792 */ | 86 'enablePepperFlashSettings'); |
793 associatedControls: null, | |
794 | |
795 /** | |
796 * Initializes page content. | |
797 */ | |
798 initializePage: function() {}, | |
799 | |
800 /** | |
801 * Sets focus on the first focusable element. Override for a custom focus | |
802 * strategy. | |
803 */ | |
804 focus: function() { | |
805 // Do not change focus if any control on this page is already focused. | |
806 if (this.pageDiv.contains(document.activeElement)) | |
807 return; | |
808 | |
809 var elements = this.pageDiv.querySelectorAll( | |
810 'input, list, select, textarea, button'); | |
811 for (var i = 0; i < elements.length; i++) { | |
812 var element = elements[i]; | |
813 // Try to focus. If fails, then continue. | |
814 element.focus(); | |
815 if (document.activeElement == element) | |
816 return; | |
817 } | 87 } |
818 }, | 88 }, |
819 | 89 |
820 /** | 90 /** |
821 * Gets the container div for this page if it is an overlay. | 91 * Sets whether Settings is shown as a standalone page in a window for the |
822 * @type {HTMLElement} | 92 * app launcher settings "app". |
| 93 * @param {boolean} isSettingsApp Whether this page is shown standalone. |
823 */ | 94 */ |
824 get container() { | 95 setIsSettingsApp: function(isSettingsApp) { |
825 assert(this.isOverlay); | 96 document.documentElement.classList.toggle('settings-app', isSettingsApp); |
826 return this.pageDiv.parentNode; | |
827 }, | 97 }, |
828 | 98 |
829 /** | 99 /** |
830 * Gets page visibility state. | 100 * Returns true if Settings is shown as an "app" (in a window by itself) |
831 * @type {boolean} | 101 * for the app launcher settings "app". |
| 102 * @return {boolean} Whether this page is shown standalone. |
832 */ | 103 */ |
833 get visible() { | 104 isSettingsApp: function() { |
834 // If this is an overlay dialog it is no longer considered visible while | 105 return document.documentElement.classList.contains('settings-app'); |
835 // the overlay is fading out. See http://crbug.com/118629. | |
836 if (this.isOverlay && | |
837 this.container.classList.contains('transparent')) { | |
838 return false; | |
839 } | |
840 if (this.pageDiv.hidden) | |
841 return false; | |
842 return this.pageDiv.page == this; | |
843 }, | |
844 | |
845 /** | |
846 * Sets page visibility. | |
847 * @type {boolean} | |
848 */ | |
849 set visible(visible) { | |
850 if ((this.visible && visible) || (!this.visible && !visible)) | |
851 return; | |
852 | |
853 // If using an overlay, the visibility of the dialog is toggled at the | |
854 // same time as the overlay to show the dialog's out transition. This | |
855 // is handled in setOverlayVisible. | |
856 if (this.isOverlay) { | |
857 this.setOverlayVisible_(visible); | |
858 } else { | |
859 this.pageDiv.page = this; | |
860 this.pageDiv.hidden = !visible; | |
861 this.onVisibilityChanged_(); | |
862 } | |
863 | |
864 cr.dispatchPropertyChange(this, 'visible', visible, !visible); | |
865 }, | |
866 | |
867 /** | |
868 * Shows or hides an overlay (including any visible dialog). | |
869 * @param {boolean} visible Whether the overlay should be visible or not. | |
870 * @private | |
871 */ | |
872 setOverlayVisible_: function(visible) { | |
873 assert(this.isOverlay); | |
874 var pageDiv = this.pageDiv; | |
875 var container = this.container; | |
876 | |
877 if (visible) | |
878 uber.invokeMethodOnParent('beginInterceptingEvents'); | |
879 | |
880 if (container.hidden != visible) { | |
881 if (visible) { | |
882 // If the container is set hidden and then immediately set visible | |
883 // again, the fadeCompleted_ callback would cause it to be erroneously | |
884 // hidden again. Removing the transparent tag avoids that. | |
885 container.classList.remove('transparent'); | |
886 | |
887 // Hide all dialogs in this container since a different one may have | |
888 // been previously visible before fading out. | |
889 var pages = container.querySelectorAll('.page'); | |
890 for (var i = 0; i < pages.length; i++) | |
891 pages[i].hidden = true; | |
892 // Show the new dialog. | |
893 pageDiv.hidden = false; | |
894 pageDiv.page = this; | |
895 } | |
896 return; | |
897 } | |
898 | |
899 var self = this; | |
900 var loading = OptionsPage.isLoading(); | |
901 if (!loading) { | |
902 // TODO(flackr): Use an event delegate to avoid having to subscribe and | |
903 // unsubscribe for webkitTransitionEnd events. | |
904 container.addEventListener('webkitTransitionEnd', function f(e) { | |
905 var propName = e.propertyName; | |
906 if (e.target != e.currentTarget || | |
907 (propName && propName != 'opacity')) { | |
908 return; | |
909 } | |
910 container.removeEventListener('webkitTransitionEnd', f); | |
911 self.fadeCompleted_(); | |
912 }); | |
913 // -webkit-transition is 200ms. Let's wait for 400ms. | |
914 ensureTransitionEndEvent(container, 400); | |
915 } | |
916 | |
917 if (visible) { | |
918 container.hidden = false; | |
919 pageDiv.hidden = false; | |
920 pageDiv.page = this; | |
921 // NOTE: This is a hacky way to force the container to layout which | |
922 // will allow us to trigger the webkit transition. | |
923 container.scrollTop; | |
924 | |
925 this.pageDiv.removeAttribute('aria-hidden'); | |
926 if (this.parentPage) { | |
927 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', | |
928 true); | |
929 } | |
930 container.classList.remove('transparent'); | |
931 this.onVisibilityChanged_(); | |
932 } else { | |
933 // Kick change events for text fields. | |
934 if (pageDiv.contains(document.activeElement)) | |
935 document.activeElement.blur(); | |
936 container.classList.add('transparent'); | |
937 } | |
938 | |
939 if (loading) | |
940 this.fadeCompleted_(); | |
941 }, | |
942 | |
943 /** | |
944 * Called when a container opacity transition finishes. | |
945 * @private | |
946 */ | |
947 fadeCompleted_: function() { | |
948 if (this.container.classList.contains('transparent')) { | |
949 this.pageDiv.hidden = true; | |
950 this.container.hidden = true; | |
951 | |
952 if (this.parentPage) | |
953 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); | |
954 | |
955 if (this.nestingLevel == 1) | |
956 uber.invokeMethodOnParent('stopInterceptingEvents'); | |
957 | |
958 this.onVisibilityChanged_(); | |
959 } | |
960 }, | |
961 | |
962 /** | |
963 * Called when a page is shown or hidden to update the root options page | |
964 * based on this page's visibility. | |
965 * @private | |
966 */ | |
967 onVisibilityChanged_: function() { | |
968 OptionsPage.updateRootPageFreezeState(); | |
969 | |
970 if (this.isOverlay && !this.visible) | |
971 OptionsPage.updateScrollPosition_(); | |
972 }, | |
973 | |
974 /** | |
975 * The nesting level of this page. | |
976 * @type {number} The nesting level of this page (0 for top-level page) | |
977 */ | |
978 get nestingLevel() { | |
979 var level = 0; | |
980 var parent = this.parentPage; | |
981 while (parent) { | |
982 level++; | |
983 parent = parent.parentPage; | |
984 } | |
985 return level; | |
986 }, | |
987 | |
988 /** | |
989 * Whether the page is considered 'sticky', such that it will | |
990 * remain a top-level page even if sub-pages change. | |
991 * @type {boolean} True if this page is sticky. | |
992 */ | |
993 get sticky() { | |
994 return false; | |
995 }, | |
996 | |
997 /** | |
998 * Checks whether this page is an ancestor of the given page in terms of | |
999 * subpage nesting. | |
1000 * @param {OptionsPage} page The potential descendent of this page. | |
1001 * @return {boolean} True if |page| is nested under this page. | |
1002 */ | |
1003 isAncestorOfPage: function(page) { | |
1004 var parent = page.parentPage; | |
1005 while (parent) { | |
1006 if (parent == this) | |
1007 return true; | |
1008 parent = parent.parentPage; | |
1009 } | |
1010 return false; | |
1011 }, | |
1012 | |
1013 /** | |
1014 * Whether it should be possible to show the page. | |
1015 * @return {boolean} True if the page should be shown. | |
1016 */ | |
1017 canShowPage: function() { | |
1018 return true; | |
1019 }, | 106 }, |
1020 }; | 107 }; |
1021 | 108 |
1022 // Export | 109 // Export |
1023 return { | 110 return { |
1024 OptionsPage: OptionsPage | 111 OptionsPage: OptionsPage |
1025 }; | 112 }; |
1026 }); | 113 }); |
OLD | NEW |