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 * Gets the default page (to be shown on initial load). |
Dan Beam
2014/07/28 20:57:58
@return
Dan Beam
2014/07/28 20:57:58
@override
michaelpg
2014/07/29 01:08:29
Getting rid of this by making the default page a p
| |
13 * @constructor | 19 */ |
14 * @param {string} name Options page name. | 20 getDefaultPage: function() { |
15 * @param {string} title Options page title, used for history. | 21 return BrowserOptions.getInstance(); |
16 * @extends {EventTarget} | 22 }, |
17 */ | |
18 function OptionsPage(name, title, pageDivName) { | |
19 this.name = name; | |
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 | 23 |
31 /** | 24 /** |
32 * This is the absolute difference maintained between standard and | 25 * Shows the tab contents for the given navigation tab. |
33 * fixed-width font sizes. Refer http://crbug.com/91922. | 26 * @param {!Element} tab The tab that the user clicked. |
34 * @const | 27 */ |
35 */ | 28 showTab: function(tab) { |
36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; | 29 // Search parents until we find a tab, or the nav bar itself. This allows |
30 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
31 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
32 !tab.classList.contains('tab')) { | |
33 tab = tab.parentNode; | |
34 } | |
35 if (!tab || !tab.classList.contains('tab')) | |
36 return; | |
37 | 37 |
38 /** | 38 // Find tab bar of the tab. |
39 * Offset of page container in pixels, to allow room for side menu. | 39 var tabBar = tab; |
40 * Simplified settings pages can override this if they don't use the menu. | 40 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { |
41 * The default (155) comes from -webkit-margin-start in uber_shared.css | 41 tabBar = tabBar.parentNode; |
42 * @private | 42 } |
43 */ | 43 if (!tabBar) |
44 OptionsPage.horizontalOffset = 155; | 44 return; |
45 | 45 |
46 /** | 46 if (tabBar.activeNavTab != null) { |
47 * Main level option pages. Maps lower-case page names to the respective page | 47 tabBar.activeNavTab.classList.remove('active-tab'); |
48 * object. | 48 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. |
49 * @protected | 49 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 // Let the overlay handle the <Esc> if it wants to. | |
359 if (overlay.handleCancel) { | |
360 overlay.handleCancel(); | |
361 this.restoreLastFocusedElement_(); | |
362 } else { | |
363 this.closeOverlay(); | |
364 } | |
365 }; | |
366 | |
367 /** | |
368 * Hides the visible overlay. Does not affect the history state. | |
369 * @private | |
370 */ | |
371 OptionsPage.hideOverlay_ = function() { | |
372 var overlay = this.getVisibleOverlay_(); | |
373 if (overlay) | |
374 overlay.visible = false; | |
375 }; | |
376 | |
377 /** | |
378 * Returns the pages which are currently visible, ordered by nesting level | |
379 * (ascending). | |
380 * @return {Array.OptionPage} The pages which are currently visible, ordered | |
381 * by nesting level (ascending). | |
382 */ | |
383 OptionsPage.getVisiblePages_ = function() { | |
384 var visiblePages = []; | |
385 for (var name in this.registeredPages) { | |
386 var page = this.registeredPages[name]; | |
387 if (page.visible) | |
388 visiblePages[page.nestingLevel] = page; | |
389 } | |
390 return visiblePages; | |
391 }; | |
392 | |
393 /** | |
394 * Returns the topmost visible page (overlays excluded). | |
395 * @return {OptionPage} The topmost visible page aside any overlay. | |
396 * @private | |
397 */ | |
398 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { | |
399 var topPage = null; | |
400 for (var name in this.registeredPages) { | |
401 var page = this.registeredPages[name]; | |
402 if (page.visible && | |
403 (!topPage || page.nestingLevel > topPage.nestingLevel)) | |
404 topPage = page; | |
405 } | |
406 | |
407 return topPage; | |
408 }; | |
409 | |
410 /** | |
411 * Returns the topmost visible page, or null if no page is visible. | |
412 * @return {OptionPage} The topmost visible page. | |
413 */ | |
414 OptionsPage.getTopmostVisiblePage = function() { | |
415 // Check overlays first since they're top-most if visible. | |
416 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); | |
417 }; | |
418 | |
419 /** | |
420 * Returns the currently visible bubble, or null if no bubble is visible. | |
421 * @return {AutoCloseBubble} The bubble currently being shown. | |
422 */ | |
423 OptionsPage.getVisibleBubble = function() { | |
424 var bubble = OptionsPage.bubble_; | |
425 return bubble && !bubble.hidden ? bubble : null; | |
426 }; | |
427 | |
428 /** | |
429 * Shows an informational bubble displaying |content| and pointing at the | |
430 * |target| element. If |content| has focusable elements, they join the | |
431 * current page's tab order as siblings of |domSibling|. | |
432 * @param {HTMLDivElement} content The content of the bubble. | |
433 * @param {HTMLElement} target The element at which the bubble points. | |
434 * @param {HTMLElement} domSibling The element after which the bubble is added | |
435 * to the DOM. | |
436 * @param {cr.ui.ArrowLocation} location The arrow location. | |
437 */ | |
438 OptionsPage.showBubble = function(content, target, domSibling, location) { | |
439 OptionsPage.hideBubble(); | |
440 | |
441 var bubble = new cr.ui.AutoCloseBubble; | |
442 bubble.anchorNode = target; | |
443 bubble.domSibling = domSibling; | |
444 bubble.arrowLocation = location; | |
445 bubble.content = content; | |
446 bubble.show(); | |
447 OptionsPage.bubble_ = bubble; | |
448 }; | |
449 | |
450 /** | |
451 * Hides the currently visible bubble, if any. | |
452 */ | |
453 OptionsPage.hideBubble = function() { | |
454 if (OptionsPage.bubble_) | |
455 OptionsPage.bubble_.hide(); | |
456 }; | |
457 | |
458 /** | |
459 * Shows the tab contents for the given navigation tab. | |
460 * @param {!Element} tab The tab that the user clicked. | |
461 */ | |
462 OptionsPage.showTab = function(tab) { | |
463 // Search parents until we find a tab, or the nav bar itself. This allows | |
464 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
465 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
466 !tab.classList.contains('tab')) { | |
467 tab = tab.parentNode; | |
468 } | |
469 if (!tab || !tab.classList.contains('tab')) | |
470 return; | |
471 | |
472 // Find tab bar of the tab. | |
473 var tabBar = tab; | |
474 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { | |
475 tabBar = tabBar.parentNode; | |
476 } | |
477 if (!tabBar) | |
478 return; | |
479 | |
480 if (tabBar.activeNavTab != null) { | |
481 tabBar.activeNavTab.classList.remove('active-tab'); | |
482 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. | |
483 remove('active-tab-contents'); | |
484 } | |
485 | |
486 tab.classList.add('active-tab'); | |
487 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); | |
488 tabBar.activeNavTab = tab; | |
489 }; | |
490 | |
491 /** | |
492 * Registers new options page. | |
493 * @param {OptionsPage} page Page to register. | |
494 */ | |
495 OptionsPage.register = function(page) { | |
496 this.registeredPages[page.name.toLowerCase()] = page; | |
497 page.initializePage(); | |
498 }; | |
499 | |
500 /** | |
501 * Find an enclosing section for an element if it exists. | |
502 * @param {Element} element Element to search. | |
503 * @return {OptionPage} The section element, or null. | |
504 * @private | |
505 */ | |
506 OptionsPage.findSectionForNode_ = function(node) { | |
507 while (node = node.parentNode) { | |
508 if (node.nodeName == 'SECTION') | |
509 return node; | |
510 } | |
511 return null; | |
512 }; | |
513 | |
514 /** | |
515 * Registers a new Overlay page. | |
516 * @param {OptionsPage} overlay Overlay to register. | |
517 * @param {OptionsPage} parentPage Associated parent page for this overlay. | |
518 * @param {Array} associatedControls Array of control elements associated with | |
519 * this page. | |
520 */ | |
521 OptionsPage.registerOverlay = function(overlay, | |
522 parentPage, | |
523 associatedControls) { | |
524 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; | |
525 overlay.parentPage = parentPage; | |
526 if (associatedControls) { | |
527 overlay.associatedControls = associatedControls; | |
528 if (associatedControls.length) { | |
529 overlay.associatedSection = | |
530 this.findSectionForNode_(associatedControls[0]); | |
531 } | 50 } |
532 | 51 |
533 // Sanity check. | 52 tab.classList.add('active-tab'); |
534 for (var i = 0; i < associatedControls.length; ++i) { | 53 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); |
535 assert(associatedControls[i], 'Invalid element passed.'); | 54 tabBar.activeNavTab = tab; |
536 } | 55 }, |
537 } | |
538 | |
539 // Reverse the button strip for Windows and CrOS. See the documentation of | |
540 // reverseButtonStripIfNecessary_() for an explanation of why this is done. | |
541 if (cr.isWindows || cr.isChromeOS) | |
542 this.reverseButtonStripIfNecessary_(overlay); | |
543 | |
544 overlay.tab = undefined; | |
545 overlay.isOverlay = true; | |
546 overlay.initializePage(); | |
547 }; | |
548 | |
549 /** | |
550 * Reverses the child elements of a button strip if it hasn't already been | |
551 * reversed. This is necessary because WebKit does not alter the tab order for | |
552 * elements that are visually reversed using -webkit-box-direction: reverse, | |
553 * and the button order is reversed for views. See http://webk.it/62664 for | |
554 * more information. | |
555 * @param {Object} overlay The overlay containing the button strip to reverse. | |
556 * @private | |
557 */ | |
558 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { | |
559 var buttonStrips = | |
560 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); | |
561 | |
562 // Reverse all button-strips in the overlay. | |
563 for (var j = 0; j < buttonStrips.length; j++) { | |
564 var buttonStrip = buttonStrips[j]; | |
565 | |
566 var childNodes = buttonStrip.childNodes; | |
567 for (var i = childNodes.length - 1; i >= 0; i--) | |
568 buttonStrip.appendChild(childNodes[i]); | |
569 | |
570 buttonStrip.setAttribute('reversed', ''); | |
571 } | |
572 }; | |
573 | |
574 /** | |
575 * Returns the name of the page from the current path. | |
576 */ | |
577 OptionsPage.getPageNameFromPath = function() { | |
578 var path = location.pathname; | |
579 if (path.length <= 1) | |
580 return this.getDefaultPage().name; | |
581 | |
582 // Skip starting slash and remove trailing slash (if any). | |
583 return path.slice(1).replace(/\/$/, ''); | |
584 }; | |
585 | |
586 /** | |
587 * Callback for window.onpopstate to handle back/forward navigations. | |
588 * @param {string} pageName The current page name. | |
589 * @param {Object} data State data pushed into history. | |
590 */ | |
591 OptionsPage.setState = function(pageName, data) { | |
592 var currentOverlay = this.getVisibleOverlay_(); | |
593 var lowercaseName = pageName.toLowerCase(); | |
594 var newPage = this.registeredPages[lowercaseName] || | |
595 this.registeredOverlayPages[lowercaseName] || | |
596 this.getDefaultPage(); | |
597 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { | |
598 currentOverlay.visible = false; | |
599 if (currentOverlay.didClosePage) currentOverlay.didClosePage(); | |
600 } | |
601 this.showPageByName(pageName, false); | |
602 }; | |
603 | |
604 /** | |
605 * Callback for window.onbeforeunload. Used to notify overlays that they will | |
606 * be closed. | |
607 */ | |
608 OptionsPage.willClose = function() { | |
609 var overlay = this.getVisibleOverlay_(); | |
610 if (overlay && overlay.didClosePage) | |
611 overlay.didClosePage(); | |
612 }; | |
613 | |
614 /** | |
615 * Freezes/unfreezes the scroll position of the root page container. | |
616 * @param {boolean} freeze Whether the page should be frozen. | |
617 * @private | |
618 */ | |
619 OptionsPage.setRootPageFrozen_ = function(freeze) { | |
620 var container = $('page-container'); | |
621 if (container.classList.contains('frozen') == freeze) | |
622 return; | |
623 | |
624 if (freeze) { | |
625 // Lock the width, since auto width computation may change. | |
626 container.style.width = window.getComputedStyle(container).width; | |
627 container.oldScrollTop = scrollTopForDocument(document); | |
628 container.classList.add('frozen'); | |
629 var verticalPosition = | |
630 container.getBoundingClientRect().top - container.oldScrollTop; | |
631 container.style.top = verticalPosition + 'px'; | |
632 this.updateFrozenElementHorizontalPosition_(container); | |
633 } else { | |
634 container.classList.remove('frozen'); | |
635 container.style.top = ''; | |
636 container.style.left = ''; | |
637 container.style.right = ''; | |
638 container.style.width = ''; | |
639 } | |
640 }; | |
641 | |
642 /** | |
643 * Freezes/unfreezes the scroll position of the root page based on the current | |
644 * page stack. | |
645 */ | |
646 OptionsPage.updateRootPageFreezeState = function() { | |
647 var topPage = OptionsPage.getTopmostVisiblePage(); | |
648 if (topPage) | |
649 this.setRootPageFrozen_(topPage.isOverlay); | |
650 }; | |
651 | |
652 /** | |
653 * Initializes the complete options page. This will cause all C++ handlers to | |
654 * be invoked to do final setup. | |
655 */ | |
656 OptionsPage.initialize = function() { | |
657 chrome.send('coreOptionsInitialize'); | |
658 uber.onContentFrameLoaded(); | |
659 FocusOutlineManager.forDocument(document); | |
660 document.addEventListener('scroll', this.handleScroll_.bind(this)); | |
661 | |
662 // Trigger the scroll handler manually to set the initial state. | |
663 this.handleScroll_(); | |
664 | |
665 // Shake the dialog if the user clicks outside the dialog bounds. | |
666 var containers = [$('overlay-container-1'), $('overlay-container-2')]; | |
667 for (var i = 0; i < containers.length; i++) { | |
668 var overlay = containers[i]; | |
669 cr.ui.overlay.setupOverlay(overlay); | |
670 overlay.addEventListener('cancelOverlay', | |
671 OptionsPage.cancelOverlay.bind(OptionsPage)); | |
672 } | |
673 | |
674 cr.ui.overlay.globalInitialization(); | |
675 }; | |
676 | |
677 /** | |
678 * Does a bounds check for the element on the given x, y client coordinates. | |
679 * @param {Element} e The DOM element. | |
680 * @param {number} x The client X to check. | |
681 * @param {number} y The client Y to check. | |
682 * @return {boolean} True if the point falls within the element's bounds. | |
683 * @private | |
684 */ | |
685 OptionsPage.elementContainsPoint_ = function(e, x, y) { | |
686 var clientRect = e.getBoundingClientRect(); | |
687 return x >= clientRect.left && x <= clientRect.right && | |
688 y >= clientRect.top && y <= clientRect.bottom; | |
689 }; | |
690 | |
691 /** | |
692 * Called when the page is scrolled; moves elements that are position:fixed | |
693 * but should only behave as if they are fixed for vertical scrolling. | |
694 * @private | |
695 */ | |
696 OptionsPage.handleScroll_ = function() { | |
697 this.updateAllFrozenElementPositions_(); | |
698 }; | |
699 | |
700 /** | |
701 * Updates all frozen pages to match the horizontal scroll position. | |
702 * @private | |
703 */ | |
704 OptionsPage.updateAllFrozenElementPositions_ = function() { | |
705 var frozenElements = document.querySelectorAll('.frozen'); | |
706 for (var i = 0; i < frozenElements.length; i++) | |
707 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); | |
708 }; | |
709 | |
710 /** | |
711 * Updates the given frozen element to match the horizontal scroll position. | |
712 * @param {HTMLElement} e The frozen element to update. | |
713 * @private | |
714 */ | |
715 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { | |
716 if (isRTL()) { | |
717 e.style.right = OptionsPage.horizontalOffset + 'px'; | |
718 } else { | |
719 var scrollLeft = scrollLeftForDocument(document); | |
720 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; | |
721 } | |
722 }; | |
723 | |
724 /** | |
725 * Change the horizontal offset used to reposition elements while showing an | |
726 * overlay from the default. | |
727 */ | |
728 OptionsPage.setHorizontalOffset = function(value) { | |
729 OptionsPage.horizontalOffset = value; | |
730 }; | |
731 | |
732 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { | |
733 if (enabled) { | |
734 document.documentElement.setAttribute( | |
735 'flashPluginSupportsClearSiteData', ''); | |
736 } else { | |
737 document.documentElement.removeAttribute( | |
738 'flashPluginSupportsClearSiteData'); | |
739 } | |
740 if (navigator.plugins['Shockwave Flash']) | |
741 document.documentElement.setAttribute('hasFlashPlugin', ''); | |
742 }; | |
743 | |
744 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { | |
745 if (enabled) { | |
746 document.documentElement.setAttribute( | |
747 'enablePepperFlashSettings', ''); | |
748 } else { | |
749 document.documentElement.removeAttribute( | |
750 'enablePepperFlashSettings'); | |
751 } | |
752 }; | |
753 | |
754 OptionsPage.setIsSettingsApp = function() { | |
755 document.documentElement.classList.add('settings-app'); | |
756 }; | |
757 | |
758 OptionsPage.isSettingsApp = function() { | |
759 return document.documentElement.classList.contains('settings-app'); | |
760 }; | |
761 | |
762 /** | |
763 * Whether the page is still loading (i.e. onload hasn't finished running). | |
764 * @return {boolean} Whether the page is still loading. | |
765 */ | |
766 OptionsPage.isLoading = function() { | |
767 return document.documentElement.classList.contains('loading'); | |
768 }; | |
769 | |
770 OptionsPage.prototype = { | |
771 __proto__: cr.EventTarget.prototype, | |
772 | 56 |
773 /** | 57 /** |
774 * The parent page of this option page, or null for top-level pages. | 58 * Initializes the complete options page. This will cause all C++ handlers |
Dan Beam
2014/07/28 20:57:58
nit: \s\s => \s
michaelpg
2014/07/29 01:08:28
Done.
| |
775 * @type {OptionsPage} | 59 * to be invoked to do final setup. |
776 */ | 60 */ |
777 parentPage: null, | 61 initialize: function() { |
62 chrome.send('coreOptionsInitialize'); | |
63 uber.onContentFrameLoaded(); | |
64 PageManager.initialize.call(PageManager); | |
Dan Beam
2014/07/28 20:57:58
can we call the super first?
michaelpg
2014/07/29 01:08:29
It's not a super. I'm not sure. The original Optio
| |
65 }, | |
778 | 66 |
Dan Beam
2014/07/28 20:57:58
doc on all these static methods
michaelpg
2014/07/29 01:08:29
Done.
| |
779 /** | 67 setClearPluginLSODataEnabled: function(enabled) { |
780 * The section on the parent page that is associated with this page. | 68 if (enabled) { |
781 * Can be null. | 69 document.documentElement.setAttribute( |
782 * @type {Element} | 70 'flashPluginSupportsClearSiteData', ''); |
783 */ | 71 } else { |
784 associatedSection: null, | 72 document.documentElement.removeAttribute( |
73 'flashPluginSupportsClearSiteData'); | |
74 } | |
75 if (navigator.plugins['Shockwave Flash']) | |
76 document.documentElement.setAttribute('hasFlashPlugin', ''); | |
77 }, | |
785 | 78 |
786 /** | 79 setPepperFlashSettingsEnabled: function(enabled) { |
787 * An array of controls that are associated with this page. The first | 80 if (enabled) { |
788 * control should be located on a top-level page. | 81 document.documentElement.setAttribute( |
789 * @type {OptionsPage} | 82 'enablePepperFlashSettings', ''); |
790 */ | 83 } else { |
791 associatedControls: null, | 84 document.documentElement.removeAttribute( |
792 | 85 'enablePepperFlashSettings'); |
793 /** | |
794 * Initializes page content. | |
795 */ | |
796 initializePage: function() {}, | |
797 | |
798 /** | |
799 * Sets focus on the first focusable element. Override for a custom focus | |
800 * strategy. | |
801 */ | |
802 focus: function() { | |
803 // Do not change focus if any control on this page is already focused. | |
804 if (this.pageDiv.contains(document.activeElement)) | |
805 return; | |
806 | |
807 var elements = this.pageDiv.querySelectorAll( | |
808 'input, list, select, textarea, button'); | |
809 for (var i = 0; i < elements.length; i++) { | |
810 var element = elements[i]; | |
811 // Try to focus. If fails, then continue. | |
812 element.focus(); | |
813 if (document.activeElement == element) | |
814 return; | |
815 } | 86 } |
816 }, | 87 }, |
817 | 88 |
818 /** | 89 setIsSettingsApp: function() { |
Dan Beam
2014/07/28 20:57:58
nit: it makes very little sense to me for a set* m
michaelpg
2014/07/29 01:08:29
Done.
| |
819 * Gets the container div for this page if it is an overlay. | 90 document.documentElement.classList.add('settings-app'); |
820 * @type {HTMLElement} | |
821 */ | |
822 get container() { | |
823 assert(this.isOverlay); | |
824 return this.pageDiv.parentNode; | |
825 }, | 91 }, |
826 | 92 |
827 /** | 93 isSettingsApp: function() { |
828 * Gets page visibility state. | 94 return document.documentElement.classList.contains('settings-app'); |
829 * @type {boolean} | |
830 */ | |
831 get visible() { | |
832 // If this is an overlay dialog it is no longer considered visible while | |
833 // the overlay is fading out. See http://crbug.com/118629. | |
834 if (this.isOverlay && | |
835 this.container.classList.contains('transparent')) { | |
836 return false; | |
837 } | |
838 if (this.pageDiv.hidden) | |
839 return false; | |
840 return this.pageDiv.page == this; | |
841 }, | |
842 | |
843 /** | |
844 * Sets page visibility. | |
845 * @type {boolean} | |
846 */ | |
847 set visible(visible) { | |
848 if ((this.visible && visible) || (!this.visible && !visible)) | |
849 return; | |
850 | |
851 // If using an overlay, the visibility of the dialog is toggled at the | |
852 // same time as the overlay to show the dialog's out transition. This | |
853 // is handled in setOverlayVisible. | |
854 if (this.isOverlay) { | |
855 this.setOverlayVisible_(visible); | |
856 } else { | |
857 this.pageDiv.page = this; | |
858 this.pageDiv.hidden = !visible; | |
859 this.onVisibilityChanged_(); | |
860 } | |
861 | |
862 cr.dispatchPropertyChange(this, 'visible', visible, !visible); | |
863 }, | |
864 | |
865 /** | |
866 * Shows or hides an overlay (including any visible dialog). | |
867 * @param {boolean} visible Whether the overlay should be visible or not. | |
868 * @private | |
869 */ | |
870 setOverlayVisible_: function(visible) { | |
871 assert(this.isOverlay); | |
872 var pageDiv = this.pageDiv; | |
873 var container = this.container; | |
874 | |
875 if (visible) | |
876 uber.invokeMethodOnParent('beginInterceptingEvents'); | |
877 | |
878 if (container.hidden != visible) { | |
879 if (visible) { | |
880 // If the container is set hidden and then immediately set visible | |
881 // again, the fadeCompleted_ callback would cause it to be erroneously | |
882 // hidden again. Removing the transparent tag avoids that. | |
883 container.classList.remove('transparent'); | |
884 | |
885 // Hide all dialogs in this container since a different one may have | |
886 // been previously visible before fading out. | |
887 var pages = container.querySelectorAll('.page'); | |
888 for (var i = 0; i < pages.length; i++) | |
889 pages[i].hidden = true; | |
890 // Show the new dialog. | |
891 pageDiv.hidden = false; | |
892 pageDiv.page = this; | |
893 } | |
894 return; | |
895 } | |
896 | |
897 var self = this; | |
898 var loading = OptionsPage.isLoading(); | |
899 if (!loading) { | |
900 // TODO(flackr): Use an event delegate to avoid having to subscribe and | |
901 // unsubscribe for webkitTransitionEnd events. | |
902 container.addEventListener('webkitTransitionEnd', function f(e) { | |
903 var propName = e.propertyName; | |
904 if (e.target != e.currentTarget || | |
905 (propName && propName != 'opacity')) { | |
906 return; | |
907 } | |
908 container.removeEventListener('webkitTransitionEnd', f); | |
909 self.fadeCompleted_(); | |
910 }); | |
911 // -webkit-transition is 200ms. Let's wait for 400ms. | |
912 ensureTransitionEndEvent(container, 400); | |
913 } | |
914 | |
915 if (visible) { | |
916 container.hidden = false; | |
917 pageDiv.hidden = false; | |
918 pageDiv.page = this; | |
919 // NOTE: This is a hacky way to force the container to layout which | |
920 // will allow us to trigger the webkit transition. | |
921 container.scrollTop; | |
922 | |
923 this.pageDiv.removeAttribute('aria-hidden'); | |
924 if (this.parentPage) { | |
925 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', | |
926 true); | |
927 } | |
928 container.classList.remove('transparent'); | |
929 this.onVisibilityChanged_(); | |
930 } else { | |
931 // Kick change events for text fields. | |
932 if (pageDiv.contains(document.activeElement)) | |
933 document.activeElement.blur(); | |
934 container.classList.add('transparent'); | |
935 } | |
936 | |
937 if (loading) | |
938 this.fadeCompleted_(); | |
939 }, | |
940 | |
941 /** | |
942 * Called when a container opacity transition finishes. | |
943 * @private | |
944 */ | |
945 fadeCompleted_: function() { | |
946 if (this.container.classList.contains('transparent')) { | |
947 this.pageDiv.hidden = true; | |
948 this.container.hidden = true; | |
949 | |
950 if (this.parentPage) | |
951 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); | |
952 | |
953 if (this.nestingLevel == 1) | |
954 uber.invokeMethodOnParent('stopInterceptingEvents'); | |
955 | |
956 this.onVisibilityChanged_(); | |
957 } | |
958 }, | |
959 | |
960 /** | |
961 * Called when a page is shown or hidden to update the root options page | |
962 * based on this page's visibility. | |
963 * @private | |
964 */ | |
965 onVisibilityChanged_: function() { | |
966 OptionsPage.updateRootPageFreezeState(); | |
967 | |
968 if (this.isOverlay && !this.visible) | |
969 OptionsPage.updateScrollPosition_(); | |
970 }, | |
971 | |
972 /** | |
973 * The nesting level of this page. | |
974 * @type {number} The nesting level of this page (0 for top-level page) | |
975 */ | |
976 get nestingLevel() { | |
977 var level = 0; | |
978 var parent = this.parentPage; | |
979 while (parent) { | |
980 level++; | |
981 parent = parent.parentPage; | |
982 } | |
983 return level; | |
984 }, | |
985 | |
986 /** | |
987 * Whether the page is considered 'sticky', such that it will | |
988 * remain a top-level page even if sub-pages change. | |
989 * @type {boolean} True if this page is sticky. | |
990 */ | |
991 get sticky() { | |
992 return false; | |
993 }, | |
994 | |
995 /** | |
996 * Checks whether this page is an ancestor of the given page in terms of | |
997 * subpage nesting. | |
998 * @param {OptionsPage} page The potential descendent of this page. | |
999 * @return {boolean} True if |page| is nested under this page. | |
1000 */ | |
1001 isAncestorOfPage: function(page) { | |
1002 var parent = page.parentPage; | |
1003 while (parent) { | |
1004 if (parent == this) | |
1005 return true; | |
1006 parent = parent.parentPage; | |
1007 } | |
1008 return false; | |
1009 }, | |
1010 | |
1011 /** | |
1012 * Whether it should be possible to show the page. | |
1013 * @return {boolean} True if the page should be shown. | |
1014 */ | |
1015 canShowPage: function() { | |
1016 return true; | |
1017 }, | 95 }, |
1018 }; | 96 }; |
1019 | 97 |
1020 // Export | 98 // Export |
1021 return { | 99 return { |
1022 OptionsPage: OptionsPage | 100 OptionsPage: OptionsPage |
1023 }; | 101 }; |
1024 }); | 102 }); |
OLD | NEW |