1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('options', function() {
6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
8 /////////////////////////////////////////////////////////////////////////////
12 * Base class for options page.
14 * @param {string} name Options page name.
15 * @param {string} title Options page title, used for history.
16 * @extends {EventTarget}
18 function OptionsPage(name, title, pageDivName) {
21 this.pageDivName = pageDivName;
22 this.pageDiv = $(this.pageDivName);
23 // |pageDiv.page| is set to the page object (this) when the page is visible
24 // to track which page is being shown when multiple pages can share the same
26 this.pageDiv.page = null;
28 this.lastFocusedElement = null;
32 * This is the absolute difference maintained between standard and
33 * fixed-width font sizes. Refer http://crbug.com/91922.
36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3;
39 * Offset of page container in pixels, to allow room for side menu.
40 * Simplified settings pages can override this if they don't use the menu.
41 * The default (155) comes from -webkit-margin-start in uber_shared.css
44 OptionsPage.horizontalOffset = 155;
47 * Main level option pages. Maps lower-case page names to the respective page
51 OptionsPage.registeredPages = {};
54 * Pages which are meant to behave like modal dialogs. Maps lower-case overlay
55 * names to the respective overlay object.
58 OptionsPage.registeredOverlayPages = {};
61 * Gets the default page (to be shown on initial load).
63 OptionsPage.getDefaultPage = function() {
64 return BrowserOptions.getInstance();
68 * Shows the default page.
70 OptionsPage.showDefaultPage = function() {
71 this.navigateToPage(this.getDefaultPage().name);
75 * "Navigates" to a page, meaning that the page will be shown and the
76 * appropriate entry is placed in the history.
77 * @param {string} pageName Page name.
79 OptionsPage.navigateToPage = function(pageName) {
80 this.showPageByName(pageName, true);
84 * Shows a registered page. This handles both top-level and overlay pages.
85 * @param {string} pageName Page name.
86 * @param {boolean} updateHistory True if we should update the history after
88 * @param {Object=} opt_propertyBag An optional bag of properties including
89 * replaceState (if history state should be replaced instead of pushed).
92 OptionsPage.showPageByName = function(pageName,
95 // If |opt_propertyBag| is non-truthy, homogenize to object.
96 opt_propertyBag = opt_propertyBag || {};
98 // If a bubble is currently being shown, hide it.
101 // Find the currently visible root-level page.
103 for (var name in this.registeredPages) {
104 var page = this.registeredPages[name];
105 if (page.visible && !page.parentPage) {
111 // Find the target page.
112 var targetPage = this.registeredPages[pageName.toLowerCase()];
113 if (!targetPage || !targetPage.canShowPage()) {
114 // If it's not a page, try it as an overlay.
115 if (!targetPage && this.showOverlay_(pageName, rootPage)) {
117 this.updateHistoryState_(!!opt_propertyBag.replaceState);
120 targetPage = this.getDefaultPage();
124 pageName = targetPage.name.toLowerCase();
125 var targetPageWasVisible = targetPage.visible;
127 // Determine if the root page is 'sticky', meaning that it
128 // shouldn't change when showing an overlay. This can happen for special
129 // pages like Search.
130 var isRootPageLocked =
131 rootPage && rootPage.sticky && targetPage.parentPage;
133 var allPageNames = Array.prototype.concat.call(
134 Object.keys(this.registeredPages),
135 Object.keys(this.registeredOverlayPages));
137 // Notify pages if they will be hidden.
138 for (var i = 0; i < allPageNames.length; ++i) {
139 var name = allPageNames[i];
140 var page = this.registeredPages[name] ||
141 this.registeredOverlayPages[name];
142 if (!page.parentPage && isRootPageLocked)
144 if (page.willHidePage && name != pageName &&
145 !page.isAncestorOfPage(targetPage)) {
150 // Update visibilities to show only the hierarchy of the target page.
151 for (var i = 0; i < allPageNames.length; ++i) {
152 var name = allPageNames[i];
153 var page = this.registeredPages[name] ||
154 this.registeredOverlayPages[name];
155 if (!page.parentPage && isRootPageLocked)
157 page.visible = name == pageName || page.isAncestorOfPage(targetPage);
160 // Update the history and current location.
162 this.updateHistoryState_(!!opt_propertyBag.replaceState);
165 this.setTitle_(targetPage.title);
167 // Update focus if any other control was focused on the previous page,
168 // or the previous page is not known.
169 if (document.activeElement != document.body &&
170 (!rootPage || rootPage.pageDiv.contains(document.activeElement))) {
174 // Notify pages if they were shown.
175 for (var i = 0; i < allPageNames.length; ++i) {
176 var name = allPageNames[i];
177 var page = this.registeredPages[name] ||
178 this.registeredOverlayPages[name];
179 if (!page.parentPage && isRootPageLocked)
181 if (!targetPageWasVisible && page.didShowPage &&
182 (name == pageName || page.isAncestorOfPage(targetPage))) {
189 * Sets the title of the page. This is accomplished by calling into the
191 * @param {string} title The title string.
194 OptionsPage.setTitle_ = function(title) {
195 uber.invokeMethodOnParent('setTitle', {title: title});
199 * Scrolls the page to the correct position (the top when opening an overlay,
200 * or the old scroll position a previously hidden overlay becomes visible).
203 OptionsPage.updateScrollPosition_ = function() {
204 var container = $('page-container');
205 var scrollTop = container.oldScrollTop || 0;
206 container.oldScrollTop = undefined;
207 window.scroll(scrollLeftForDocument(document), scrollTop);
211 * Pushes the current page onto the history stack, overriding the last page
212 * if it is the generic chrome://settings/.
213 * @param {boolean} replace If true, allow no history events to be created.
214 * @param {object=} opt_params A bag of optional params, including:
215 * {boolean} ignoreHash Whether to include the hash or not.
218 OptionsPage.updateHistoryState_ = function(replace, opt_params) {
219 var page = this.getTopmostVisiblePage();
220 var path = window.location.pathname + window.location.hash;
222 path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash.
225 this.setTitle_(page.title);
227 // The page is already in history (the user may have clicked the same link
228 // twice). Do nothing.
229 if (path == page.name && !OptionsPage.isLoading())
232 var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash;
234 // If settings are embedded, tell the outer page to set its "path" to the
235 // inner frame's path.
236 var outerPath = (page == this.getDefaultPage() ? '' : page.name) + hash;
237 uber.invokeMethodOnParent('setPath', {path: outerPath});
239 // If there is no path, the current location is chrome://settings/.
240 // Override this with the new page.
241 var historyFunction = path && !replace ? window.history.pushState :
242 window.history.replaceState;
243 historyFunction.call(window.history,
244 {pageName: page.name},
246 '/' + page.name + hash);
250 * Shows a registered Overlay page. Does not update history.
251 * @param {string} overlayName Page name.
252 * @param {OptionPage} rootPage The currently visible root-level page.
253 * @return {boolean} whether we showed an overlay.
255 OptionsPage.showOverlay_ = function(overlayName, rootPage) {
256 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()];
257 if (!overlay || !overlay.canShowPage())
260 // Save the currently focused element in the page for restoration later.
261 var currentPage = this.getTopmostVisiblePage();
263 currentPage.lastFocusedElement = document.activeElement;
265 if ((!rootPage || !rootPage.sticky) &&
266 overlay.parentPage &&
267 !overlay.parentPage.visible) {
268 this.showPageByName(overlay.parentPage.name, false);
271 if (!overlay.visible) {
272 overlay.visible = true;
273 if (overlay.didShowPage) overlay.didShowPage();
277 this.setTitle_(overlay.title);
279 // Change focus to the overlay if any other control was focused by keyboard
280 // before. Otherwise, no one should have focus.
281 if (document.activeElement != document.body) {
282 if (FocusOutlineManager.forDocument(document).visible) {
284 } else if (!overlay.pageDiv.contains(document.activeElement)) {
285 document.activeElement.blur();
289 if ($('search-field').value == '') {
290 var section = overlay.associatedSection;
292 options.BrowserOptions.scrollToSection(section);
299 * Returns whether or not an overlay is visible.
300 * @return {boolean} True if an overlay is visible.
303 OptionsPage.isOverlayVisible_ = function() {
304 return this.getVisibleOverlay_() != null;
308 * Returns the currently visible overlay, or null if no page is visible.
309 * @return {OptionPage} The visible overlay.
311 OptionsPage.getVisibleOverlay_ = function() {
312 var topmostPage = null;
313 for (var name in this.registeredOverlayPages) {
314 var page = this.registeredOverlayPages[name];
316 (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) {
324 * Restores the last focused element on a given page.
326 OptionsPage.restoreLastFocusedElement_ = function() {
327 var currentPage = this.getTopmostVisiblePage();
328 if (currentPage.lastFocusedElement)
329 currentPage.lastFocusedElement.focus();
333 * Closes the visible overlay. Updates the history state after closing the
336 OptionsPage.closeOverlay = function() {
337 var overlay = this.getVisibleOverlay_();
341 overlay.visible = false;
343 if (overlay.didClosePage) overlay.didClosePage();
344 this.updateHistoryState_(false, {ignoreHash: true});
346 this.restoreLastFocusedElement_();
350 * Cancels (closes) the overlay, due to the user pressing <Esc>.
352 OptionsPage.cancelOverlay = function() {
353 // Blur the active element to ensure any changed pref value is saved.
354 document.activeElement.blur();
355 var overlay = this.getVisibleOverlay_();
356 // Let the overlay handle the <Esc> if it wants to.
357 if (overlay.handleCancel) {
358 overlay.handleCancel();
359 this.restoreLastFocusedElement_();
366 * Hides the visible overlay. Does not affect the history state.
369 OptionsPage.hideOverlay_ = function() {
370 var overlay = this.getVisibleOverlay_();
372 overlay.visible = false;
376 * Returns the pages which are currently visible, ordered by nesting level
378 * @return {Array.OptionPage} The pages which are currently visible, ordered
379 * by nesting level (ascending).
381 OptionsPage.getVisiblePages_ = function() {
382 var visiblePages = [];
383 for (var name in this.registeredPages) {
384 var page = this.registeredPages[name];
386 visiblePages[page.nestingLevel] = page;
392 * Returns the topmost visible page (overlays excluded).
393 * @return {OptionPage} The topmost visible page aside any overlay.
396 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() {
398 for (var name in this.registeredPages) {
399 var page = this.registeredPages[name];
401 (!topPage || page.nestingLevel > topPage.nestingLevel))
409 * Returns the topmost visible page, or null if no page is visible.
410 * @return {OptionPage} The topmost visible page.
412 OptionsPage.getTopmostVisiblePage = function() {
413 // Check overlays first since they're top-most if visible.
414 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_();
418 * Returns the currently visible bubble, or null if no bubble is visible.
419 * @return {AutoCloseBubble} The bubble currently being shown.
421 OptionsPage.getVisibleBubble = function() {
422 var bubble = OptionsPage.bubble_;
423 return bubble && !bubble.hidden ? bubble : null;
427 * Shows an informational bubble displaying |content| and pointing at the
428 * |target| element. If |content| has focusable elements, they join the
429 * current page's tab order as siblings of |domSibling|.
430 * @param {HTMLDivElement} content The content of the bubble.
431 * @param {HTMLElement} target The element at which the bubble points.
432 * @param {HTMLElement} domSibling The element after which the bubble is added
434 * @param {cr.ui.ArrowLocation} location The arrow location.
436 OptionsPage.showBubble = function(content, target, domSibling, location) {
437 OptionsPage.hideBubble();
439 var bubble = new cr.ui.AutoCloseBubble;
440 bubble.anchorNode = target;
441 bubble.domSibling = domSibling;
442 bubble.arrowLocation = location;
443 bubble.content = content;
445 OptionsPage.bubble_ = bubble;
449 * Hides the currently visible bubble, if any.
451 OptionsPage.hideBubble = function() {
452 if (OptionsPage.bubble_)
453 OptionsPage.bubble_.hide();
457 * Shows the tab contents for the given navigation tab.
458 * @param {!Element} tab The tab that the user clicked.
460 OptionsPage.showTab = function(tab) {
461 // Search parents until we find a tab, or the nav bar itself. This allows
462 // tabs to have child nodes, e.g. labels in separately-styled spans.
463 while (tab && !tab.classList.contains('subpages-nav-tabs') &&
464 !tab.classList.contains('tab')) {
465 tab = tab.parentNode;
467 if (!tab || !tab.classList.contains('tab'))
470 // Find tab bar of the tab.
472 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) {
473 tabBar = tabBar.parentNode;
478 if (tabBar.activeNavTab != null) {
479 tabBar.activeNavTab.classList.remove('active-tab');
480 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList.
481 remove('active-tab-contents');
484 tab.classList.add('active-tab');
485 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
486 tabBar.activeNavTab = tab;
490 * Registers new options page.
491 * @param {OptionsPage} page Page to register.
493 OptionsPage.register = function(page) {
494 this.registeredPages[page.name.toLowerCase()] = page;
495 page.initializePage();
499 * Find an enclosing section for an element if it exists.
500 * @param {Element} element Element to search.
501 * @return {OptionPage} The section element, or null.
504 OptionsPage.findSectionForNode_ = function(node) {
505 while (node = node.parentNode) {
506 if (node.nodeName == 'SECTION')
513 * Registers a new Overlay page.
514 * @param {OptionsPage} overlay Overlay to register.
515 * @param {OptionsPage} parentPage Associated parent page for this overlay.
516 * @param {Array} associatedControls Array of control elements associated with
519 OptionsPage.registerOverlay = function(overlay,
521 associatedControls) {
522 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay;
523 overlay.parentPage = parentPage;
524 if (associatedControls) {
525 overlay.associatedControls = associatedControls;
526 if (associatedControls.length) {
527 overlay.associatedSection =
528 this.findSectionForNode_(associatedControls[0]);
532 for (var i = 0; i < associatedControls.length; ++i) {
533 assert(associatedControls[i], 'Invalid element passed.');
537 // Reverse the button strip for views. See the documentation of
538 // reverseButtonStripIfNecessary_() for an explanation of why this is done.
540 this.reverseButtonStripIfNecessary_(overlay);
542 overlay.tab = undefined;
543 overlay.isOverlay = true;
544 overlay.initializePage();
548 * Reverses the child elements of a button strip if it hasn't already been
549 * reversed. This is necessary because WebKit does not alter the tab order for
550 * elements that are visually reversed using -webkit-box-direction: reverse,
551 * and the button order is reversed for views. See http://webk.it/62664 for
553 * @param {Object} overlay The overlay containing the button strip to reverse.
556 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) {
558 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])');
560 // Reverse all button-strips in the overlay.
561 for (var j = 0; j < buttonStrips.length; j++) {
562 var buttonStrip = buttonStrips[j];
564 var childNodes = buttonStrip.childNodes;
565 for (var i = childNodes.length - 1; i >= 0; i--)
566 buttonStrip.appendChild(childNodes[i]);
568 buttonStrip.setAttribute('reversed', '');
573 * Callback for window.onpopstate to handle back/forward navigations.
574 * @param {Object} data State data pushed into history.
576 OptionsPage.setState = function(data) {
577 if (data && data.pageName) {
578 var currentOverlay = this.getVisibleOverlay_();
579 var lowercaseName = data.pageName.toLowerCase();
580 var newPage = this.registeredPages[lowercaseName] ||
581 this.registeredOverlayPages[lowercaseName] ||
582 this.getDefaultPage();
583 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) {
584 currentOverlay.visible = false;
585 if (currentOverlay.didClosePage) currentOverlay.didClosePage();
587 this.showPageByName(data.pageName, false);
592 * Callback for window.onbeforeunload. Used to notify overlays that they will
595 OptionsPage.willClose = function() {
596 var overlay = this.getVisibleOverlay_();
597 if (overlay && overlay.didClosePage)
598 overlay.didClosePage();
602 * Freezes/unfreezes the scroll position of the root page container.
603 * @param {boolean} freeze Whether the page should be frozen.
606 OptionsPage.setRootPageFrozen_ = function(freeze) {
607 var container = $('page-container');
608 if (container.classList.contains('frozen') == freeze)
612 // Lock the width, since auto width computation may change.
613 container.style.width = window.getComputedStyle(container).width;
614 container.oldScrollTop = scrollTopForDocument(document);
615 container.classList.add('frozen');
616 var verticalPosition =
617 container.getBoundingClientRect().top - container.oldScrollTop;
618 container.style.top = verticalPosition + 'px';
619 this.updateFrozenElementHorizontalPosition_(container);
621 container.classList.remove('frozen');
622 container.style.top = '';
623 container.style.left = '';
624 container.style.right = '';
625 container.style.width = '';
630 * Freezes/unfreezes the scroll position of the root page based on the current
633 OptionsPage.updateRootPageFreezeState = function() {
634 var topPage = OptionsPage.getTopmostVisiblePage();
636 this.setRootPageFrozen_(topPage.isOverlay);
640 * Initializes the complete options page. This will cause all C++ handlers to
641 * be invoked to do final setup.
643 OptionsPage.initialize = function() {
644 chrome.send('coreOptionsInitialize');
645 uber.onContentFrameLoaded();
646 FocusOutlineManager.forDocument(document);
647 document.addEventListener('scroll', this.handleScroll_.bind(this));
649 // Trigger the scroll handler manually to set the initial state.
650 this.handleScroll_();
652 // Shake the dialog if the user clicks outside the dialog bounds.
653 var containers = [$('overlay-container-1'), $('overlay-container-2')];
654 for (var i = 0; i < containers.length; i++) {
655 var overlay = containers[i];
656 cr.ui.overlay.setupOverlay(overlay);
657 overlay.addEventListener('cancelOverlay',
658 OptionsPage.cancelOverlay.bind(OptionsPage));
661 cr.ui.overlay.globalInitialization();
665 * Does a bounds check for the element on the given x, y client coordinates.
666 * @param {Element} e The DOM element.
667 * @param {number} x The client X to check.
668 * @param {number} y The client Y to check.
669 * @return {boolean} True if the point falls within the element's bounds.
672 OptionsPage.elementContainsPoint_ = function(e, x, y) {
673 var clientRect = e.getBoundingClientRect();
674 return x >= clientRect.left && x <= clientRect.right &&
675 y >= clientRect.top && y <= clientRect.bottom;
679 * Called when the page is scrolled; moves elements that are position:fixed
680 * but should only behave as if they are fixed for vertical scrolling.
683 OptionsPage.handleScroll_ = function() {
684 this.updateAllFrozenElementPositions_();
688 * Updates all frozen pages to match the horizontal scroll position.
691 OptionsPage.updateAllFrozenElementPositions_ = function() {
692 var frozenElements = document.querySelectorAll('.frozen');
693 for (var i = 0; i < frozenElements.length; i++)
694 this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
698 * Updates the given frozen element to match the horizontal scroll position.
699 * @param {HTMLElement} e The frozen element to update.
702 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) {
704 e.style.right = OptionsPage.horizontalOffset + 'px';
706 var scrollLeft = scrollLeftForDocument(document);
707 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px';
712 * Change the horizontal offset used to reposition elements while showing an
713 * overlay from the default.
715 OptionsPage.setHorizontalOffset = function(value) {
716 OptionsPage.horizontalOffset = value;
719 OptionsPage.setClearPluginLSODataEnabled = function(enabled) {
721 document.documentElement.setAttribute(
722 'flashPluginSupportsClearSiteData', '');
724 document.documentElement.removeAttribute(
725 'flashPluginSupportsClearSiteData');
727 if (navigator.plugins['Shockwave Flash'])
728 document.documentElement.setAttribute('hasFlashPlugin', '');
731 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) {
733 document.documentElement.setAttribute(
734 'enablePepperFlashSettings', '');
736 document.documentElement.removeAttribute(
737 'enablePepperFlashSettings');
741 OptionsPage.setIsSettingsApp = function() {
742 document.documentElement.classList.add('settings-app');
745 OptionsPage.isSettingsApp = function() {
746 return document.documentElement.classList.contains('settings-app');
750 * Whether the page is still loading (i.e. onload hasn't finished running).
751 * @return {boolean} Whether the page is still loading.
753 OptionsPage.isLoading = function() {
754 return document.documentElement.classList.contains('loading');
757 OptionsPage.prototype = {
758 __proto__: cr.EventTarget.prototype,
761 * The parent page of this option page, or null for top-level pages.
762 * @type {OptionsPage}
767 * The section on the parent page that is associated with this page.
771 associatedSection: null,
774 * An array of controls that are associated with this page. The first
775 * control should be located on a top-level page.
776 * @type {OptionsPage}
778 associatedControls: null,
781 * Initializes page content.
783 initializePage: function() {},
786 * Sets focus on the first focusable element. Override for a custom focus
790 // Do not change focus if any control on this page is already focused.
791 if (this.pageDiv.contains(document.activeElement))
794 var elements = this.pageDiv.querySelectorAll(
795 'input, list, select, textarea, button');
796 for (var i = 0; i < elements.length; i++) {
797 var element = elements[i];
798 // Try to focus. If fails, then continue.
800 if (document.activeElement == element)
806 * Gets the container div for this page if it is an overlay.
807 * @type {HTMLElement}
810 assert(this.isOverlay);
811 return this.pageDiv.parentNode;
815 * Gets page visibility state.
819 // If this is an overlay dialog it is no longer considered visible while
820 // the overlay is fading out. See http://crbug.com/118629.
821 if (this.isOverlay &&
822 this.container.classList.contains('transparent')) {
825 if (this.pageDiv.hidden)
827 return this.pageDiv.page == this;
831 * Sets page visibility.
834 set visible(visible) {
835 if ((this.visible && visible) || (!this.visible && !visible))
838 // If using an overlay, the visibility of the dialog is toggled at the
839 // same time as the overlay to show the dialog's out transition. This
840 // is handled in setOverlayVisible.
841 if (this.isOverlay) {
842 this.setOverlayVisible_(visible);
844 this.pageDiv.page = this;
845 this.pageDiv.hidden = !visible;
846 this.onVisibilityChanged_();
849 cr.dispatchPropertyChange(this, 'visible', visible, !visible);
853 * Shows or hides an overlay (including any visible dialog).
854 * @param {boolean} visible Whether the overlay should be visible or not.
857 setOverlayVisible_: function(visible) {
858 assert(this.isOverlay);
859 var pageDiv = this.pageDiv;
860 var container = this.container;
863 uber.invokeMethodOnParent('beginInterceptingEvents');
865 if (container.hidden != visible) {
867 // If the container is set hidden and then immediately set visible
868 // again, the fadeCompleted_ callback would cause it to be erroneously
869 // hidden again. Removing the transparent tag avoids that.
870 container.classList.remove('transparent');
872 // Hide all dialogs in this container since a different one may have
873 // been previously visible before fading out.
874 var pages = container.querySelectorAll('.page');
875 for (var i = 0; i < pages.length; i++)
876 pages[i].hidden = true;
877 // Show the new dialog.
878 pageDiv.hidden = false;
885 var loading = OptionsPage.isLoading();
887 // TODO(flackr): Use an event delegate to avoid having to subscribe and
888 // unsubscribe for webkitTransitionEnd events.
889 container.addEventListener('webkitTransitionEnd', function f(e) {
890 if (e.target != e.currentTarget || e.propertyName != 'opacity')
892 container.removeEventListener('webkitTransitionEnd', f);
893 self.fadeCompleted_();
898 container.hidden = false;
899 pageDiv.hidden = false;
901 // NOTE: This is a hacky way to force the container to layout which
902 // will allow us to trigger the webkit transition.
905 this.pageDiv.removeAttribute('aria-hidden');
906 if (this.parentPage) {
907 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden',
910 container.classList.remove('transparent');
911 this.onVisibilityChanged_();
913 // Kick change events for text fields.
914 if (pageDiv.contains(document.activeElement))
915 document.activeElement.blur();
916 container.classList.add('transparent');
920 this.fadeCompleted_();
924 * Called when a container opacity transition finishes.
927 fadeCompleted_: function() {
928 if (this.container.classList.contains('transparent')) {
929 this.pageDiv.hidden = true;
930 this.container.hidden = true;
933 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
935 if (this.nestingLevel == 1)
936 uber.invokeMethodOnParent('stopInterceptingEvents');
938 this.onVisibilityChanged_();
943 * Called when a page is shown or hidden to update the root options page
944 * based on this page's visibility.
947 onVisibilityChanged_: function() {
948 OptionsPage.updateRootPageFreezeState();
950 if (this.isOverlay && !this.visible)
951 OptionsPage.updateScrollPosition_();
955 * The nesting level of this page.
956 * @type {number} The nesting level of this page (0 for top-level page)
960 var parent = this.parentPage;
963 parent = parent.parentPage;
969 * Whether the page is considered 'sticky', such that it will
970 * remain a top-level page even if sub-pages change.
971 * @type {boolean} True if this page is sticky.
978 * Checks whether this page is an ancestor of the given page in terms of
980 * @param {OptionsPage} page The potential descendent of this page.
981 * @return {boolean} True if |page| is nested under this page.
983 isAncestorOfPage: function(page) {
984 var parent = page.parentPage;
988 parent = parent.parentPage;
994 * Whether it should be possible to show the page.
995 * @return {boolean} True if the page should be shown.
997 canShowPage: function() {
1004 OptionsPage: OptionsPage