Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(23)

Side by Side Diff: chrome/browser/resources/options2/options_page.js

Issue 8895023: Options2: Pull the trigger. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: DIAF. Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/options2/options_page.css ('k') | chrome/browser/resources/options2/pack_extension_overlay.css » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698