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