OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 cr.define('options', function() { | |
6 ///////////////////////////////////////////////////////////////////////////// | |
7 // OptionsPage class: | |
8 | |
9 /** | |
10 * Base class for options page. | |
11 * @constructor | |
12 * @param {string} name Options page name, also defines id of the div element | |
13 * containing the options view and the name of options page navigation bar | |
14 * item as name+'PageNav'. | |
15 * @param {string} title Options page title, used for navigation bar | |
16 * @extends {EventTarget} | |
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 this.tab = null; | |
24 } | |
25 | |
26 const SUBPAGE_SHEET_COUNT = 2; | |
27 | |
28 /** | |
29 * Main level option pages. Maps lower-case page names to the respective page | |
30 * object. | |
31 * @protected | |
32 */ | |
33 OptionsPage.registeredPages = {}; | |
34 | |
35 /** | |
36 * Pages which are meant to behave like modal dialogs. Maps lower-case overlay | |
37 * names to the respective overlay object. | |
38 * @protected | |
39 */ | |
40 OptionsPage.registeredOverlayPages = {}; | |
41 | |
42 /** | |
43 * Whether or not |initialize| has been called. | |
44 * @private | |
45 */ | |
46 OptionsPage.initialized_ = false; | |
47 | |
48 /** | |
49 * Gets the default page (to be shown on initial load). | |
50 */ | |
51 OptionsPage.getDefaultPage = function() { | |
52 return BrowserOptions.getInstance(); | |
53 }; | |
54 | |
55 /** | |
56 * Shows the default page. | |
57 */ | |
58 OptionsPage.showDefaultPage = function() { | |
59 this.navigateToPage(this.getDefaultPage().name); | |
60 }; | |
61 | |
62 /** | |
63 * "Navigates" to a page, meaning that the page will be shown and the | |
64 * appropriate entry is placed in the history. | |
65 * @param {string} pageName Page name. | |
66 */ | |
67 OptionsPage.navigateToPage = function(pageName) { | |
68 this.showPageByName(pageName, true); | |
69 }; | |
70 | |
71 /** | |
72 * Shows a registered page. This handles both top-level pages and sub-pages. | |
73 * @param {string} pageName Page name. | |
74 * @param {boolean} updateHistory True if we should update the history after | |
75 * showing the page. | |
76 * @private | |
77 */ | |
78 OptionsPage.showPageByName = function(pageName, updateHistory) { | |
79 // Find the currently visible root-level page. | |
80 var rootPage = null; | |
81 for (var name in this.registeredPages) { | |
82 var page = this.registeredPages[name]; | |
83 if (page.visible && !page.parentPage) { | |
84 rootPage = page; | |
85 break; | |
86 } | |
87 } | |
88 | |
89 // Find the target page. | |
90 var targetPage = this.registeredPages[pageName.toLowerCase()]; | |
91 if (!targetPage || !targetPage.canShowPage()) { | |
92 // If it's not a page, try it as an overlay. | |
93 if (!targetPage && this.showOverlay_(pageName, rootPage)) { | |
94 if (updateHistory) | |
95 this.updateHistoryState_(); | |
96 return; | |
97 } else { | |
98 targetPage = this.getDefaultPage(); | |
99 } | |
100 } | |
101 | |
102 pageName = targetPage.name.toLowerCase(); | |
103 var targetPageWasVisible = targetPage.visible; | |
104 | |
105 // Determine if the root page is 'sticky', meaning that it | |
106 // shouldn't change when showing a sub-page. This can happen for special | |
107 // pages like Search. | |
108 var isRootPageLocked = | |
109 rootPage && rootPage.sticky && targetPage.parentPage; | |
110 | |
111 // Notify pages if they will be hidden. | |
112 for (var name in this.registeredPages) { | |
113 var page = this.registeredPages[name]; | |
114 if (!page.parentPage && isRootPageLocked) | |
115 continue; | |
116 if (page.willHidePage && name != pageName && | |
117 !page.isAncestorOfPage(targetPage)) | |
118 page.willHidePage(); | |
119 } | |
120 | |
121 // Update visibilities to show only the hierarchy of the target page. | |
122 for (var name in this.registeredPages) { | |
123 var page = this.registeredPages[name]; | |
124 if (!page.parentPage && isRootPageLocked) | |
125 continue; | |
126 page.visible = name == pageName || | |
127 (!document.documentElement.classList.contains('hide-menu') && | |
128 page.isAncestorOfPage(targetPage)); | |
129 } | |
130 | |
131 // Update the history and current location. | |
132 if (updateHistory) | |
133 this.updateHistoryState_(); | |
134 | |
135 // Always update the page title. | |
136 document.title = targetPage.title; | |
137 | |
138 // Notify pages if they were shown. | |
139 for (var name in this.registeredPages) { | |
140 var page = this.registeredPages[name]; | |
141 if (!page.parentPage && isRootPageLocked) | |
142 continue; | |
143 if (!targetPageWasVisible && page.didShowPage && (name == pageName || | |
144 page.isAncestorOfPage(targetPage))) | |
145 page.didShowPage(); | |
146 } | |
147 }; | |
148 | |
149 /** | |
150 * Updates the visibility and stacking order of the subpage backdrop | |
151 * according to which subpage is topmost and visible. | |
152 * @private | |
153 */ | |
154 OptionsPage.updateSubpageBackdrop_ = function () { | |
155 var topmostPage = this.getTopmostVisibleNonOverlayPage_(); | |
156 var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; | |
157 | |
158 var subpageBackdrop = $('subpage-backdrop'); | |
159 if (nestingLevel > 0) { | |
160 var container = $('subpage-sheet-container-' + nestingLevel); | |
161 subpageBackdrop.style.zIndex = | |
162 parseInt(window.getComputedStyle(container).zIndex) - 1; | |
163 subpageBackdrop.hidden = false; | |
164 } else { | |
165 subpageBackdrop.hidden = true; | |
166 } | |
167 }; | |
168 | |
169 /** | |
170 * Scrolls the page to the correct position (the top when opening a subpage, | |
171 * or the old scroll position a previously hidden subpage becomes visible). | |
172 * @private | |
173 */ | |
174 OptionsPage.updateScrollPosition_ = function () { | |
175 var topmostPage = this.getTopmostVisibleNonOverlayPage_(); | |
176 var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; | |
177 | |
178 var container = (nestingLevel > 0) ? | |
179 $('subpage-sheet-container-' + nestingLevel) : $('page-container'); | |
180 | |
181 var scrollTop = container.oldScrollTop || 0; | |
182 container.oldScrollTop = undefined; | |
183 window.scroll(document.body.scrollLeft, scrollTop); | |
184 }; | |
185 | |
186 /** | |
187 * Pushes the current page onto the history stack, overriding the last page | |
188 * if it is the generic chrome://settings/. | |
189 * @private | |
190 */ | |
191 OptionsPage.updateHistoryState_ = function() { | |
192 var page = this.getTopmostVisiblePage(); | |
193 var path = location.pathname; | |
194 if (path) | |
195 path = path.slice(1).replace(/\/$/, ''); // Remove trailing slash. | |
196 // The page is already in history (the user may have clicked the same link | |
197 // twice). Do nothing. | |
198 if (path == page.name) | |
199 return; | |
200 | |
201 // If there is no path, the current location is chrome://settings/. | |
202 // Override this with the new page. | |
203 var historyFunction = path ? window.history.pushState : | |
204 window.history.replaceState; | |
205 historyFunction.call(window.history, | |
206 {pageName: page.name}, | |
207 page.title, | |
208 '/' + page.name); | |
209 // Update tab title. | |
210 document.title = page.title; | |
211 }; | |
212 | |
213 /** | |
214 * Shows a registered Overlay page. Does not update history. | |
215 * @param {string} overlayName Page name. | |
216 * @param {OptionPage} rootPage The currently visible root-level page. | |
217 * @return {boolean} whether we showed an overlay. | |
218 */ | |
219 OptionsPage.showOverlay_ = function(overlayName, rootPage) { | |
220 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; | |
221 if (!overlay || !overlay.canShowPage()) | |
222 return false; | |
223 | |
224 if ((!rootPage || !rootPage.sticky) && overlay.parentPage) | |
225 this.showPageByName(overlay.parentPage.name, false); | |
226 | |
227 if (!overlay.visible) { | |
228 overlay.visible = true; | |
229 if (overlay.didShowPage) overlay.didShowPage(); | |
230 } | |
231 | |
232 return true; | |
233 }; | |
234 | |
235 /** | |
236 * Returns whether or not an overlay is visible. | |
237 * @return {boolean} True if an overlay is visible. | |
238 * @private | |
239 */ | |
240 OptionsPage.isOverlayVisible_ = function() { | |
241 return this.getVisibleOverlay_() != null; | |
242 }; | |
243 | |
244 /** | |
245 * Returns the currently visible overlay, or null if no page is visible. | |
246 * @return {OptionPage} The visible overlay. | |
247 */ | |
248 OptionsPage.getVisibleOverlay_ = function() { | |
249 for (var name in this.registeredOverlayPages) { | |
250 var page = this.registeredOverlayPages[name]; | |
251 if (page.visible) | |
252 return page; | |
253 } | |
254 return null; | |
255 }; | |
256 | |
257 /** | |
258 * Closes the visible overlay. Updates the history state after closing the | |
259 * overlay. | |
260 */ | |
261 OptionsPage.closeOverlay = function() { | |
262 var overlay = this.getVisibleOverlay_(); | |
263 if (!overlay) | |
264 return; | |
265 | |
266 overlay.visible = false; | |
267 if (overlay.didClosePage) overlay.didClosePage(); | |
268 this.updateHistoryState_(); | |
269 }; | |
270 | |
271 /** | |
272 * Hides the visible overlay. Does not affect the history state. | |
273 * @private | |
274 */ | |
275 OptionsPage.hideOverlay_ = function() { | |
276 var overlay = this.getVisibleOverlay_(); | |
277 if (overlay) | |
278 overlay.visible = false; | |
279 }; | |
280 | |
281 /** | |
282 * Returns the topmost visible page (overlays excluded). | |
283 * @return {OptionPage} The topmost visible page aside any overlay. | |
284 * @private | |
285 */ | |
286 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { | |
287 var topPage = null; | |
288 for (var name in this.registeredPages) { | |
289 var page = this.registeredPages[name]; | |
290 if (page.visible && | |
291 (!topPage || page.nestingLevel > topPage.nestingLevel)) | |
292 topPage = page; | |
293 } | |
294 | |
295 return topPage; | |
296 }; | |
297 | |
298 /** | |
299 * Returns the topmost visible page, or null if no page is visible. | |
300 * @return {OptionPage} The topmost visible page. | |
301 */ | |
302 OptionsPage.getTopmostVisiblePage = function() { | |
303 // Check overlays first since they're top-most if visible. | |
304 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); | |
305 }; | |
306 | |
307 /** | |
308 * Closes the topmost open subpage, if any. | |
309 * @private | |
310 */ | |
311 OptionsPage.closeTopSubPage_ = function() { | |
312 var topPage = this.getTopmostVisiblePage(); | |
313 if (topPage && !topPage.isOverlay && topPage.parentPage) { | |
314 if (topPage.willHidePage) | |
315 topPage.willHidePage(); | |
316 topPage.visible = false; | |
317 } | |
318 | |
319 this.updateHistoryState_(); | |
320 }; | |
321 | |
322 /** | |
323 * Closes all subpages below the given level. | |
324 * @param {number} level The nesting level to close below. | |
325 */ | |
326 OptionsPage.closeSubPagesToLevel = function(level) { | |
327 var topPage = this.getTopmostVisiblePage(); | |
328 while (topPage && topPage.nestingLevel > level) { | |
329 if (topPage.willHidePage) | |
330 topPage.willHidePage(); | |
331 topPage.visible = false; | |
332 topPage = topPage.parentPage; | |
333 } | |
334 | |
335 this.updateHistoryState_(); | |
336 }; | |
337 | |
338 /** | |
339 * Updates managed banner visibility state based on the topmost page. | |
340 */ | |
341 OptionsPage.updateManagedBannerVisibility = function() { | |
342 var topPage = this.getTopmostVisiblePage(); | |
343 if (topPage) | |
344 topPage.updateManagedBannerVisibility(); | |
345 }; | |
346 | |
347 /** | |
348 * Shows the tab contents for the given navigation tab. | |
349 * @param {!Element} tab The tab that the user clicked. | |
350 */ | |
351 OptionsPage.showTab = function(tab) { | |
352 // Search parents until we find a tab, or the nav bar itself. This allows | |
353 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
354 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
355 !tab.classList.contains('tab')) { | |
356 tab = tab.parentNode; | |
357 } | |
358 if (!tab || !tab.classList.contains('tab')) | |
359 return; | |
360 | |
361 // Find tab bar of the tab. | |
362 var tabBar = tab; | |
363 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { | |
364 tabBar = tabBar.parentNode; | |
365 } | |
366 if (!tabBar) | |
367 return; | |
368 | |
369 if (tabBar.activeNavTab != null) { | |
370 tabBar.activeNavTab.classList.remove('active-tab'); | |
371 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. | |
372 remove('active-tab-contents'); | |
373 } | |
374 | |
375 tab.classList.add('active-tab'); | |
376 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); | |
377 tabBar.activeNavTab = tab; | |
378 }; | |
379 | |
380 /** | |
381 * Registers new options page. | |
382 * @param {OptionsPage} page Page to register. | |
383 */ | |
384 OptionsPage.register = function(page) { | |
385 this.registeredPages[page.name.toLowerCase()] = page; | |
386 // Create and add new page <li> element to navbar. | |
387 var pageNav = document.createElement('li'); | |
388 pageNav.id = page.name + 'PageNav'; | |
389 pageNav.className = 'navbar-item'; | |
390 pageNav.setAttribute('pageName', page.name); | |
391 pageNav.setAttribute('role', 'tab'); | |
392 pageNav.textContent = page.pageDiv.querySelector('h1').textContent; | |
393 pageNav.tabIndex = -1; | |
394 pageNav.onclick = function(event) { | |
395 OptionsPage.navigateToPage(this.getAttribute('pageName')); | |
396 }; | |
397 pageNav.onkeydown = function(event) { | |
398 if ((event.keyCode == 37 || event.keyCode==38) && | |
399 this.previousSibling && this.previousSibling.onkeydown) { | |
400 // Left and up arrow moves back one tab. | |
401 OptionsPage.navigateToPage( | |
402 this.previousSibling.getAttribute('pageName')); | |
403 this.previousSibling.focus(); | |
404 } else if ((event.keyCode == 39 || event.keyCode == 40) && | |
405 this.nextSibling) { | |
406 // Right and down arrows move forward one tab. | |
407 OptionsPage.navigateToPage(this.nextSibling.getAttribute('pageName')); | |
408 this.nextSibling.focus(); | |
409 } | |
410 }; | |
411 pageNav.onkeypress = function(event) { | |
412 // Enter or space | |
413 if (event.keyCode == 13 || event.keyCode == 32) { | |
414 OptionsPage.navigateToPage(this.getAttribute('pageName')); | |
415 } | |
416 }; | |
417 var navbar = $('navbar'); | |
418 navbar.appendChild(pageNav); | |
419 page.tab = pageNav; | |
420 page.initializePage(); | |
421 }; | |
422 | |
423 /** | |
424 * Find an enclosing section for an element if it exists. | |
425 * @param {Element} element Element to search. | |
426 * @return {OptionPage} The section element, or null. | |
427 * @private | |
428 */ | |
429 OptionsPage.findSectionForNode_ = function(node) { | |
430 while (node = node.parentNode) { | |
431 if (node.nodeName == 'SECTION') | |
432 return node; | |
433 } | |
434 return null; | |
435 }; | |
436 | |
437 /** | |
438 * Registers a new Sub-page. | |
439 * @param {OptionsPage} subPage Sub-page to register. | |
440 * @param {OptionsPage} parentPage Associated parent page for this page. | |
441 * @param {Array} associatedControls Array of control elements that lead to | |
442 * this sub-page. The first item is typically a button in a root-level | |
443 * page. There may be additional buttons for nested sub-pages. | |
444 */ | |
445 OptionsPage.registerSubPage = function(subPage, | |
446 parentPage, | |
447 associatedControls) { | |
448 this.registeredPages[subPage.name.toLowerCase()] = subPage; | |
449 subPage.parentPage = parentPage; | |
450 if (associatedControls) { | |
451 subPage.associatedControls = associatedControls; | |
452 if (associatedControls.length) { | |
453 subPage.associatedSection = | |
454 this.findSectionForNode_(associatedControls[0]); | |
455 } | |
456 } | |
457 subPage.tab = undefined; | |
458 subPage.initializePage(); | |
459 }; | |
460 | |
461 /** | |
462 * Registers a new Overlay page. | |
463 * @param {OptionsPage} overlay Overlay to register. | |
464 * @param {OptionsPage} parentPage Associated parent page for this overlay. | |
465 * @param {Array} associatedControls Array of control elements associated with | |
466 * this page. | |
467 */ | |
468 OptionsPage.registerOverlay = function(overlay, | |
469 parentPage, | |
470 associatedControls) { | |
471 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; | |
472 overlay.parentPage = parentPage; | |
473 if (associatedControls) { | |
474 overlay.associatedControls = associatedControls; | |
475 if (associatedControls.length) { | |
476 overlay.associatedSection = | |
477 this.findSectionForNode_(associatedControls[0]); | |
478 } | |
479 } | |
480 | |
481 // Reverse the button strip for views. See the documentation of | |
482 // reverseButtonStrip_() for an explanation of why this is necessary. | |
483 if (cr.isViews) | |
484 this.reverseButtonStrip_(overlay); | |
485 | |
486 overlay.tab = undefined; | |
487 overlay.isOverlay = true; | |
488 overlay.initializePage(); | |
489 }; | |
490 | |
491 /** | |
492 * Reverses the child elements of a button strip. This is necessary because | |
493 * WebKit does not alter the tab order for elements that are visually reversed | |
494 * using -webkit-box-direction: reverse, and the button order is reversed for | |
495 * views. See https://bugs.webkit.org/show_bug.cgi?id=62664 for more | |
496 * information. | |
497 * @param {Object} overlay The overlay containing the button strip to reverse. | |
498 * @private | |
499 */ | |
500 OptionsPage.reverseButtonStrip_ = function(overlay) { | |
501 var buttonStrips = overlay.pageDiv.querySelectorAll('.button-strip'); | |
502 | |
503 // Reverse all button-strips in the overlay. | |
504 for (var j = 0; j < buttonStrips.length; j++) { | |
505 var buttonStrip = buttonStrips[j]; | |
506 | |
507 var childNodes = buttonStrip.childNodes; | |
508 for (var i = childNodes.length - 1; i >= 0; i--) | |
509 buttonStrip.appendChild(childNodes[i]); | |
510 } | |
511 }; | |
512 | |
513 /** | |
514 * Callback for window.onpopstate. | |
515 * @param {Object} data State data pushed into history. | |
516 */ | |
517 OptionsPage.setState = function(data) { | |
518 if (data && data.pageName) { | |
519 // It's possible an overlay may be the last top-level page shown. | |
520 if (this.isOverlayVisible_() && | |
521 !this.registeredOverlayPages[data.pageName.toLowerCase()]) { | |
522 this.hideOverlay_(); | |
523 } | |
524 | |
525 this.showPageByName(data.pageName, false); | |
526 } | |
527 }; | |
528 | |
529 /** | |
530 * Callback for window.onbeforeunload. Used to notify overlays that they will | |
531 * be closed. | |
532 */ | |
533 OptionsPage.willClose = function() { | |
534 var overlay = this.getVisibleOverlay_(); | |
535 if (overlay && overlay.didClosePage) | |
536 overlay.didClosePage(); | |
537 }; | |
538 | |
539 /** | |
540 * Freezes/unfreezes the scroll position of given level's page container. | |
541 * @param {boolean} freeze Whether the page should be frozen. | |
542 * @param {number} level The level to freeze/unfreeze. | |
543 * @private | |
544 */ | |
545 OptionsPage.setPageFrozenAtLevel_ = function(freeze, level) { | |
546 var container = level == 0 ? $('page-container') | |
547 : $('subpage-sheet-container-' + level); | |
548 | |
549 if (container.classList.contains('frozen') == freeze) | |
550 return; | |
551 | |
552 if (freeze) { | |
553 // Lock the width, since auto width computation may change. | |
554 container.style.width = window.getComputedStyle(container).width; | |
555 container.oldScrollTop = document.body.scrollTop; | |
556 container.classList.add('frozen'); | |
557 var verticalPosition = | |
558 container.getBoundingClientRect().top - container.oldScrollTop; | |
559 container.style.top = verticalPosition + 'px'; | |
560 this.updateFrozenElementHorizontalPosition_(container); | |
561 } else { | |
562 container.classList.remove('frozen'); | |
563 container.style.top = ''; | |
564 container.style.left = ''; | |
565 container.style.right = ''; | |
566 container.style.width = ''; | |
567 } | |
568 }; | |
569 | |
570 /** | |
571 * Freezes/unfreezes the scroll position of visible pages based on the current | |
572 * page stack. | |
573 */ | |
574 OptionsPage.updatePageFreezeStates = function() { | |
575 var topPage = OptionsPage.getTopmostVisiblePage(); | |
576 if (!topPage) | |
577 return; | |
578 var nestingLevel = topPage.isOverlay ? 100 : topPage.nestingLevel; | |
579 for (var i = 0; i <= SUBPAGE_SHEET_COUNT; i++) { | |
580 this.setPageFrozenAtLevel_(i < nestingLevel, i); | |
581 } | |
582 }; | |
583 | |
584 /** | |
585 * Initializes the complete options page. This will cause all C++ handlers to | |
586 * be invoked to do final setup. | |
587 */ | |
588 OptionsPage.initialize = function() { | |
589 chrome.send('coreOptionsInitialize'); | |
590 this.initialized_ = true; | |
591 | |
592 document.addEventListener('scroll', this.handleScroll_.bind(this)); | |
593 window.addEventListener('resize', this.handleResize_.bind(this)); | |
594 | |
595 if (!document.documentElement.classList.contains('hide-menu')) { | |
596 // Close subpages if the user clicks on the html body. Listen in the | |
597 // capturing phase so that we can stop the click from doing anything. | |
598 document.body.addEventListener('click', | |
599 this.bodyMouseEventHandler_.bind(this), | |
600 true); | |
601 // We also need to cancel mousedowns on non-subpage content. | |
602 document.body.addEventListener('mousedown', | |
603 this.bodyMouseEventHandler_.bind(this), | |
604 true); | |
605 | |
606 var self = this; | |
607 // Hook up the close buttons. | |
608 subpageCloseButtons = document.querySelectorAll('.close-subpage'); | |
609 for (var i = 0; i < subpageCloseButtons.length; i++) { | |
610 subpageCloseButtons[i].onclick = function() { | |
611 self.closeTopSubPage_(); | |
612 }; | |
613 }; | |
614 | |
615 // Install handler for key presses. | |
616 document.addEventListener('keydown', | |
617 this.keyDownEventHandler_.bind(this)); | |
618 | |
619 document.addEventListener('focus', this.manageFocusChange_.bind(this), | |
620 true); | |
621 } | |
622 | |
623 // Calculate and store the horizontal locations of elements that may be | |
624 // frozen later. | |
625 var sidebarWidth = | |
626 parseInt(window.getComputedStyle($('mainview')).webkitPaddingStart, 10); | |
627 $('page-container').horizontalOffset = sidebarWidth + | |
628 parseInt(window.getComputedStyle( | |
629 $('mainview-content')).webkitPaddingStart, 10); | |
630 for (var level = 1; level <= SUBPAGE_SHEET_COUNT; level++) { | |
631 var containerId = 'subpage-sheet-container-' + level; | |
632 $(containerId).horizontalOffset = sidebarWidth; | |
633 } | |
634 $('subpage-backdrop').horizontalOffset = sidebarWidth; | |
635 // Trigger the resize handler manually to set the initial state. | |
636 this.handleResize_(null); | |
637 }; | |
638 | |
639 /** | |
640 * Does a bounds check for the element on the given x, y client coordinates. | |
641 * @param {Element} e The DOM element. | |
642 * @param {number} x The client X to check. | |
643 * @param {number} y The client Y to check. | |
644 * @return {boolean} True if the point falls within the element's bounds. | |
645 * @private | |
646 */ | |
647 OptionsPage.elementContainsPoint_ = function(e, x, y) { | |
648 var clientRect = e.getBoundingClientRect(); | |
649 return x >= clientRect.left && x <= clientRect.right && | |
650 y >= clientRect.top && y <= clientRect.bottom; | |
651 }; | |
652 | |
653 /** | |
654 * Called when focus changes; ensures that focus doesn't move outside | |
655 * the topmost subpage/overlay. | |
656 * @param {Event} e The focus change event. | |
657 * @private | |
658 */ | |
659 OptionsPage.manageFocusChange_ = function(e) { | |
660 var focusableItemsRoot; | |
661 var topPage = this.getTopmostVisiblePage(); | |
662 if (!topPage) | |
663 return; | |
664 | |
665 if (topPage.isOverlay) { | |
666 // If an overlay is visible, that defines the tab loop. | |
667 focusableItemsRoot = topPage.pageDiv; | |
668 } else { | |
669 // If a subpage is visible, use its parent as the tab loop constraint. | |
670 // (The parent is used because it contains the close button.) | |
671 if (topPage.nestingLevel > 0) | |
672 focusableItemsRoot = topPage.pageDiv.parentNode; | |
673 } | |
674 | |
675 if (focusableItemsRoot && !focusableItemsRoot.contains(e.target)) | |
676 topPage.focusFirstElement(); | |
677 }; | |
678 | |
679 /** | |
680 * Called when the page is scrolled; moves elements that are position:fixed | |
681 * but should only behave as if they are fixed for vertical scrolling. | |
682 * @param {Event} e The scroll event. | |
683 * @private | |
684 */ | |
685 OptionsPage.handleScroll_ = function(e) { | |
686 var scrollHorizontalOffset = document.body.scrollLeft; | |
687 // position:fixed doesn't seem to work for horizontal scrolling in RTL mode, | |
688 // so only adjust in LTR mode (where scroll values will be positive). | |
689 if (scrollHorizontalOffset >= 0) { | |
690 $('navbar-container').style.left = -scrollHorizontalOffset + 'px'; | |
691 var subpageBackdrop = $('subpage-backdrop'); | |
692 subpageBackdrop.style.left = subpageBackdrop.horizontalOffset - | |
693 scrollHorizontalOffset + 'px'; | |
694 this.updateAllFrozenElementPositions_(); | |
695 } | |
696 }; | |
697 | |
698 /** | |
699 * Updates all frozen pages to match the horizontal scroll position. | |
700 * @private | |
701 */ | |
702 OptionsPage.updateAllFrozenElementPositions_ = function() { | |
703 var frozenElements = document.querySelectorAll('.frozen'); | |
704 for (var i = 0; i < frozenElements.length; i++) { | |
705 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); | |
706 } | |
707 }; | |
708 | |
709 /** | |
710 * Updates the given frozen element to match the horizontal scroll position. | |
711 * @param {HTMLElement} e The frozen element to update | |
712 * @private | |
713 */ | |
714 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { | |
715 if (document.documentElement.dir == 'rtl') | |
716 e.style.right = e.horizontalOffset + 'px'; | |
717 else | |
718 e.style.left = e.horizontalOffset - document.body.scrollLeft + 'px'; | |
719 }; | |
720 | |
721 /** | |
722 * Called when the page is resized; adjusts the size of elements that depend | |
723 * on the veiwport. | |
724 * @param {Event} e The resize event. | |
725 * @private | |
726 */ | |
727 OptionsPage.handleResize_ = function(e) { | |
728 // Set an explicit height equal to the viewport on all the subpage | |
729 // containers shorter than the viewport. This is used instead of | |
730 // min-height: 100% so that there is an explicit height for the subpages' | |
731 // min-height: 100%. | |
732 var viewportHeight = document.documentElement.clientHeight; | |
733 var subpageContainers = | |
734 document.querySelectorAll('.subpage-sheet-container'); | |
735 for (var i = 0; i < subpageContainers.length; i++) { | |
736 if (subpageContainers[i].scrollHeight > viewportHeight) | |
737 subpageContainers[i].style.removeProperty('height'); | |
738 else | |
739 subpageContainers[i].style.height = viewportHeight + 'px'; | |
740 } | |
741 }; | |
742 | |
743 /** | |
744 * A function to handle mouse events (mousedown or click) on the html body by | |
745 * closing subpages and/or stopping event propagation. | |
746 * @return {Event} a mousedown or click event. | |
747 * @private | |
748 */ | |
749 OptionsPage.bodyMouseEventHandler_ = function(event) { | |
750 // Do nothing if a subpage isn't showing. | |
751 var topPage = this.getTopmostVisiblePage(); | |
752 if (!topPage || topPage.isOverlay || !topPage.parentPage) | |
753 return; | |
754 | |
755 // Don't close subpages if a user is clicking in a select element. | |
756 // This is necessary because WebKit sends click events with strange | |
757 // coordinates when a user selects a new entry in a select element. | |
758 // See: http://crbug.com/87199 | |
759 if (event.srcElement.nodeName == 'SELECT') | |
760 return; | |
761 | |
762 // Do nothing if the client coordinates are not within the source element. | |
763 // This occurs if the user toggles a checkbox by pressing spacebar. | |
764 // This is a workaround to prevent keyboard events from closing the window. | |
765 // See: crosbug.com/15678 | |
766 if (event.clientX == -document.body.scrollLeft && | |
767 event.clientY == -document.body.scrollTop) { | |
768 return; | |
769 } | |
770 | |
771 // Don't interfere with navbar clicks. | |
772 if ($('navbar').contains(event.target)) | |
773 return; | |
774 | |
775 // Figure out which page the click happened in. | |
776 for (var level = topPage.nestingLevel; level >= 0; level--) { | |
777 var clickIsWithinLevel = level == 0 ? true : | |
778 OptionsPage.elementContainsPoint_( | |
779 $('subpage-sheet-' + level), event.clientX, event.clientY); | |
780 | |
781 if (!clickIsWithinLevel) | |
782 continue; | |
783 | |
784 // Event was within the topmost page; do nothing. | |
785 if (topPage.nestingLevel == level) | |
786 return; | |
787 | |
788 // Block propgation of both clicks and mousedowns, but only close subpages | |
789 // on click. | |
790 if (event.type == 'click') | |
791 this.closeSubPagesToLevel(level); | |
792 event.stopPropagation(); | |
793 event.preventDefault(); | |
794 return; | |
795 } | |
796 }; | |
797 | |
798 /** | |
799 * A function to handle key press events. | |
800 * @return {Event} a keydown event. | |
801 * @private | |
802 */ | |
803 OptionsPage.keyDownEventHandler_ = function(event) { | |
804 // Close the top overlay or sub-page on esc. | |
805 if (event.keyCode == 27) { // Esc | |
806 if (this.isOverlayVisible_()) | |
807 this.closeOverlay(); | |
808 else | |
809 this.closeTopSubPage_(); | |
810 } | |
811 }; | |
812 | |
813 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { | |
814 if (enabled) { | |
815 document.documentElement.setAttribute( | |
816 'flashPluginSupportsClearSiteData', ''); | |
817 } else { | |
818 document.documentElement.removeAttribute( | |
819 'flashPluginSupportsClearSiteData'); | |
820 } | |
821 }; | |
822 | |
823 /** | |
824 * Re-initializes the C++ handlers if necessary. This is called if the | |
825 * handlers are torn down and recreated but the DOM may not have been (in | |
826 * which case |initialize| won't be called again). If |initialize| hasn't been | |
827 * called, this does nothing (since it will be later, once the DOM has | |
828 * finished loading). | |
829 */ | |
830 OptionsPage.reinitializeCore = function() { | |
831 if (this.initialized_) | |
832 chrome.send('coreOptionsInitialize'); | |
833 } | |
834 | |
835 OptionsPage.prototype = { | |
836 __proto__: cr.EventTarget.prototype, | |
837 | |
838 /** | |
839 * The parent page of this option page, or null for top-level pages. | |
840 * @type {OptionsPage} | |
841 */ | |
842 parentPage: null, | |
843 | |
844 /** | |
845 * The section on the parent page that is associated with this page. | |
846 * Can be null. | |
847 * @type {Element} | |
848 */ | |
849 associatedSection: null, | |
850 | |
851 /** | |
852 * An array of controls that are associated with this page. The first | |
853 * control should be located on a top-level page. | |
854 * @type {OptionsPage} | |
855 */ | |
856 associatedControls: null, | |
857 | |
858 /** | |
859 * Initializes page content. | |
860 */ | |
861 initializePage: function() {}, | |
862 | |
863 /** | |
864 * Updates managed banner visibility state. This function iterates over | |
865 * all input fields of a window and if any of these is marked as managed | |
866 * it triggers the managed banner to be visible. The banner can be enforced | |
867 * being on through the managed flag of this class but it can not be forced | |
868 * being off if managed items exist. | |
869 */ | |
870 updateManagedBannerVisibility: function() { | |
871 var bannerDiv = $('managed-prefs-banner'); | |
872 | |
873 var controlledByPolicy = false; | |
874 var controlledByExtension = false; | |
875 var inputElements = this.pageDiv.querySelectorAll('input[controlled-by]'); | |
876 for (var i = 0, len = inputElements.length; i < len; i++) { | |
877 if (inputElements[i].controlledBy == 'policy') | |
878 controlledByPolicy = true; | |
879 else if (inputElements[i].controlledBy == 'extension') | |
880 controlledByExtension = true; | |
881 } | |
882 if (!controlledByPolicy && !controlledByExtension) { | |
883 bannerDiv.hidden = true; | |
884 } else { | |
885 bannerDiv.hidden = false; | |
886 var height = window.getComputedStyle(bannerDiv).height; | |
887 if (controlledByPolicy && !controlledByExtension) { | |
888 $('managed-prefs-text').textContent = | |
889 templateData.policyManagedPrefsBannerText; | |
890 } else if (!controlledByPolicy && controlledByExtension) { | |
891 $('managed-prefs-text').textContent = | |
892 templateData.extensionManagedPrefsBannerText; | |
893 } else if (controlledByPolicy && controlledByExtension) { | |
894 $('managed-prefs-text').textContent = | |
895 templateData.policyAndExtensionManagedPrefsBannerText; | |
896 } | |
897 } | |
898 }, | |
899 | |
900 /** | |
901 * Gets page visibility state. | |
902 */ | |
903 get visible() { | |
904 return !this.pageDiv.hidden; | |
905 }, | |
906 | |
907 /** | |
908 * Sets page visibility. | |
909 */ | |
910 set visible(visible) { | |
911 if ((this.visible && visible) || (!this.visible && !visible)) | |
912 return; | |
913 | |
914 this.setContainerVisibility_(visible); | |
915 if (visible) { | |
916 this.pageDiv.hidden = false; | |
917 | |
918 if (this.tab) { | |
919 this.tab.classList.add('navbar-item-selected'); | |
920 this.tab.setAttribute('aria-selected', 'true'); | |
921 this.tab.tabIndex = 0; | |
922 } | |
923 } else { | |
924 this.pageDiv.hidden = true; | |
925 | |
926 if (this.tab) { | |
927 this.tab.classList.remove('navbar-item-selected'); | |
928 this.tab.setAttribute('aria-selected', 'false'); | |
929 this.tab.tabIndex = -1; | |
930 } | |
931 } | |
932 | |
933 OptionsPage.updatePageFreezeStates(); | |
934 | |
935 // The managed prefs banner is global, so after any visibility change | |
936 // update it based on the topmost page, not necessarily this page | |
937 // (e.g., if an ancestor is made visible after a child). | |
938 OptionsPage.updateManagedBannerVisibility(); | |
939 | |
940 // A subpage was shown or hidden. | |
941 if (!this.isOverlay && this.nestingLevel > 0) { | |
942 OptionsPage.updateSubpageBackdrop_(); | |
943 OptionsPage.updateScrollPosition_(); | |
944 } | |
945 | |
946 cr.dispatchPropertyChange(this, 'visible', visible, !visible); | |
947 }, | |
948 | |
949 /** | |
950 * Shows or hides this page's container. | |
951 * @param {boolean} visible Whether the container should be visible or not. | |
952 * @private | |
953 */ | |
954 setContainerVisibility_: function(visible) { | |
955 var container = null; | |
956 if (this.isOverlay) { | |
957 container = $('overlay'); | |
958 } else { | |
959 var nestingLevel = this.nestingLevel; | |
960 if (nestingLevel > 0) | |
961 container = $('subpage-sheet-container-' + nestingLevel); | |
962 } | |
963 var isSubpage = !this.isOverlay; | |
964 | |
965 if (!container) | |
966 return; | |
967 | |
968 if (container.hidden != visible) { | |
969 if (visible) { | |
970 // If the container is set hidden and then immediately set visible | |
971 // again, the fadeCompleted_ callback would cause it to be erroneously | |
972 // hidden again. Removing the transparent tag avoids that. | |
973 container.classList.remove('transparent'); | |
974 } | |
975 return; | |
976 } | |
977 | |
978 if (visible) { | |
979 container.hidden = false; | |
980 if (isSubpage) { | |
981 var computedStyle = window.getComputedStyle(container); | |
982 container.style.WebkitPaddingStart = | |
983 parseInt(computedStyle.WebkitPaddingStart, 10) + 100 + 'px'; | |
984 } | |
985 // Separate animating changes from the removal of display:none. | |
986 window.setTimeout(function() { | |
987 container.classList.remove('transparent'); | |
988 if (isSubpage) | |
989 container.style.WebkitPaddingStart = ''; | |
990 }); | |
991 } else { | |
992 var self = this; | |
993 container.addEventListener('webkitTransitionEnd', function f(e) { | |
994 if (e.propertyName != 'opacity') | |
995 return; | |
996 container.removeEventListener('webkitTransitionEnd', f); | |
997 self.fadeCompleted_(container); | |
998 }); | |
999 container.classList.add('transparent'); | |
1000 } | |
1001 }, | |
1002 | |
1003 /** | |
1004 * Called when a container opacity transition finishes. | |
1005 * @param {HTMLElement} container The container element. | |
1006 * @private | |
1007 */ | |
1008 fadeCompleted_: function(container) { | |
1009 if (container.classList.contains('transparent')) | |
1010 container.hidden = true; | |
1011 }, | |
1012 | |
1013 /** | |
1014 * Focuses the first control on the page. | |
1015 */ | |
1016 focusFirstElement: function() { | |
1017 // Sets focus on the first interactive element in the page. | |
1018 var focusElement = | |
1019 this.pageDiv.querySelector('button, input, list, select'); | |
1020 if (focusElement) | |
1021 focusElement.focus(); | |
1022 }, | |
1023 | |
1024 /** | |
1025 * The nesting level of this page. | |
1026 * @type {number} The nesting level of this page (0 for top-level page) | |
1027 */ | |
1028 get nestingLevel() { | |
1029 var level = 0; | |
1030 var parent = this.parentPage; | |
1031 while (parent) { | |
1032 level++; | |
1033 parent = parent.parentPage; | |
1034 } | |
1035 return level; | |
1036 }, | |
1037 | |
1038 /** | |
1039 * Whether the page is considered 'sticky', such that it will | |
1040 * remain a top-level page even if sub-pages change. | |
1041 * @type {boolean} True if this page is sticky. | |
1042 */ | |
1043 get sticky() { | |
1044 return false; | |
1045 }, | |
1046 | |
1047 /** | |
1048 * Checks whether this page is an ancestor of the given page in terms of | |
1049 * subpage nesting. | |
1050 * @param {OptionsPage} page | |
1051 * @return {boolean} True if this page is nested under |page| | |
1052 */ | |
1053 isAncestorOfPage: function(page) { | |
1054 var parent = page.parentPage; | |
1055 while (parent) { | |
1056 if (parent == this) | |
1057 return true; | |
1058 parent = parent.parentPage; | |
1059 } | |
1060 return false; | |
1061 }, | |
1062 | |
1063 /** | |
1064 * Whether it should be possible to show the page. | |
1065 * @return {boolean} True if the page should be shown | |
1066 */ | |
1067 canShowPage: function() { | |
1068 return true; | |
1069 }, | |
1070 }; | |
1071 | |
1072 // Export | |
1073 return { | |
1074 OptionsPage: OptionsPage | |
1075 }; | |
1076 }); | |
OLD | NEW |