// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('options', function() { /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; ///////////////////////////////////////////////////////////////////////////// // OptionsPage class: /** * Base class for options page. * @constructor * @param {string} name Options page name. * @param {string} title Options page title, used for history. * @extends {EventTarget} */ function OptionsPage(name, title, pageDivName) { this.name = name; this.title = title; this.pageDivName = pageDivName; this.pageDiv = $(this.pageDivName); // |pageDiv.page| is set to the page object (this) when the page is visible // to track which page is being shown when multiple pages can share the same // underlying div. this.pageDiv.page = null; this.tab = null; this.lastFocusedElement = null; } /** * This is the absolute difference maintained between standard and * fixed-width font sizes. Refer http://crbug.com/91922. * @const */ OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; /** * Offset of page container in pixels, to allow room for side menu. * Simplified settings pages can override this if they don't use the menu. * The default (155) comes from -webkit-margin-start in uber_shared.css * @private */ OptionsPage.horizontalOffset = 155; /** * Main level option pages. Maps lower-case page names to the respective page * object. * @protected */ OptionsPage.registeredPages = {}; /** * Pages which are meant to behave like modal dialogs. Maps lower-case overlay * names to the respective overlay object. * @protected */ OptionsPage.registeredOverlayPages = {}; /** * Gets the default page (to be shown on initial load). */ OptionsPage.getDefaultPage = function() { return BrowserOptions.getInstance(); }; /** * Shows the default page. */ OptionsPage.showDefaultPage = function() { this.navigateToPage(this.getDefaultPage().name); }; /** * "Navigates" to a page, meaning that the page will be shown and the * appropriate entry is placed in the history. * @param {string} pageName Page name. */ OptionsPage.navigateToPage = function(pageName) { this.showPageByName(pageName, true); }; /** * Shows a registered page. This handles both top-level and overlay pages. * @param {string} pageName Page name. * @param {boolean} updateHistory True if we should update the history after * showing the page. * @param {Object=} opt_propertyBag An optional bag of properties including * replaceState (if history state should be replaced instead of pushed). * @private */ OptionsPage.showPageByName = function(pageName, updateHistory, opt_propertyBag) { // If |opt_propertyBag| is non-truthy, homogenize to object. opt_propertyBag = opt_propertyBag || {}; // If a bubble is currently being shown, hide it. this.hideBubble(); // Find the currently visible root-level page. var rootPage = null; for (var name in this.registeredPages) { var page = this.registeredPages[name]; if (page.visible && !page.parentPage) { rootPage = page; break; } } // Find the target page. var targetPage = this.registeredPages[pageName.toLowerCase()]; if (!targetPage || !targetPage.canShowPage()) { // If it's not a page, try it as an overlay. if (!targetPage && this.showOverlay_(pageName, rootPage)) { if (updateHistory) this.updateHistoryState_(!!opt_propertyBag.replaceState); return; } else { targetPage = this.getDefaultPage(); } } pageName = targetPage.name.toLowerCase(); var targetPageWasVisible = targetPage.visible; // Determine if the root page is 'sticky', meaning that it // shouldn't change when showing an overlay. This can happen for special // pages like Search. var isRootPageLocked = rootPage && rootPage.sticky && targetPage.parentPage; var allPageNames = Array.prototype.concat.call( Object.keys(this.registeredPages), Object.keys(this.registeredOverlayPages)); // Notify pages if they will be hidden. for (var i = 0; i < allPageNames.length; ++i) { var name = allPageNames[i]; var page = this.registeredPages[name] || this.registeredOverlayPages[name]; if (!page.parentPage && isRootPageLocked) continue; if (page.willHidePage && name != pageName && !page.isAncestorOfPage(targetPage)) { page.willHidePage(); } } // Update visibilities to show only the hierarchy of the target page. for (var i = 0; i < allPageNames.length; ++i) { var name = allPageNames[i]; var page = this.registeredPages[name] || this.registeredOverlayPages[name]; if (!page.parentPage && isRootPageLocked) continue; page.visible = name == pageName || page.isAncestorOfPage(targetPage); } // Update the history and current location. if (updateHistory) this.updateHistoryState_(!!opt_propertyBag.replaceState); // Update tab title. this.setTitle_(targetPage.title); // Update focus if any other control was focused on the previous page, // or the previous page is not known. if (document.activeElement != document.body && (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { targetPage.focus(); } // Notify pages if they were shown. for (var i = 0; i < allPageNames.length; ++i) { var name = allPageNames[i]; var page = this.registeredPages[name] || this.registeredOverlayPages[name]; if (!page.parentPage && isRootPageLocked) continue; if (!targetPageWasVisible && page.didShowPage && (name == pageName || page.isAncestorOfPage(targetPage))) { page.didShowPage(); } } }; /** * Sets the title of the page. This is accomplished by calling into the * parent page API. * @param {string} title The title string. * @private */ OptionsPage.setTitle_ = function(title) { uber.invokeMethodOnParent('setTitle', {title: title}); }; /** * Scrolls the page to the correct position (the top when opening an overlay, * or the old scroll position a previously hidden overlay becomes visible). * @private */ OptionsPage.updateScrollPosition_ = function() { var container = $('page-container'); var scrollTop = container.oldScrollTop || 0; container.oldScrollTop = undefined; window.scroll(scrollLeftForDocument(document), scrollTop); }; /** * Pushes the current page onto the history stack, overriding the last page * if it is the generic chrome://settings/. * @param {boolean} replace If true, allow no history events to be created. * @param {object=} opt_params A bag of optional params, including: * {boolean} ignoreHash Whether to include the hash or not. * @private */ OptionsPage.updateHistoryState_ = function(replace, opt_params) { var page = this.getTopmostVisiblePage(); var path = window.location.pathname + window.location.hash; if (path) path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash. // Update tab title. this.setTitle_(page.title); // The page is already in history (the user may have clicked the same link // twice). Do nothing. if (path == page.name && !OptionsPage.isLoading()) return; var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash; // If settings are embedded, tell the outer page to set its "path" to the // inner frame's path. var outerPath = (page == this.getDefaultPage() ? '' : page.name) + hash; uber.invokeMethodOnParent('setPath', {path: outerPath}); // If there is no path, the current location is chrome://settings/. // Override this with the new page. var historyFunction = path && !replace ? window.history.pushState : window.history.replaceState; historyFunction.call(window.history, {pageName: page.name}, page.title, '/' + page.name + hash); }; /** * Shows a registered Overlay page. Does not update history. * @param {string} overlayName Page name. * @param {OptionPage} rootPage The currently visible root-level page. * @return {boolean} whether we showed an overlay. */ OptionsPage.showOverlay_ = function(overlayName, rootPage) { var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; if (!overlay || !overlay.canShowPage()) return false; // Save the currently focused element in the page for restoration later. var currentPage = this.getTopmostVisiblePage(); if (currentPage) currentPage.lastFocusedElement = document.activeElement; if ((!rootPage || !rootPage.sticky) && overlay.parentPage && !overlay.parentPage.visible) { this.showPageByName(overlay.parentPage.name, false); } if (!overlay.visible) { overlay.visible = true; if (overlay.didShowPage) overlay.didShowPage(); } // Update tab title. this.setTitle_(overlay.title); // Change focus to the overlay if any other control was focused by keyboard // before. Otherwise, no one should have focus. if (document.activeElement != document.body) { if (FocusOutlineManager.forDocument(document).visible) { overlay.focus(); } else if (!overlay.pageDiv.contains(document.activeElement)) { document.activeElement.blur(); } } if ($('search-field').value == '') { var section = overlay.associatedSection; if (section) options.BrowserOptions.scrollToSection(section); } return true; }; /** * Returns whether or not an overlay is visible. * @return {boolean} True if an overlay is visible. * @private */ OptionsPage.isOverlayVisible_ = function() { return this.getVisibleOverlay_() != null; }; /** * Returns the currently visible overlay, or null if no page is visible. * @return {OptionPage} The visible overlay. */ OptionsPage.getVisibleOverlay_ = function() { var topmostPage = null; for (var name in this.registeredOverlayPages) { var page = this.registeredOverlayPages[name]; if (page.visible && (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) { topmostPage = page; } } return topmostPage; }; /** * Restores the last focused element on a given page. */ OptionsPage.restoreLastFocusedElement_ = function() { var currentPage = this.getTopmostVisiblePage(); if (currentPage.lastFocusedElement) currentPage.lastFocusedElement.focus(); }; /** * Closes the visible overlay. Updates the history state after closing the * overlay. */ OptionsPage.closeOverlay = function() { var overlay = this.getVisibleOverlay_(); if (!overlay) return; overlay.visible = false; if (overlay.didClosePage) overlay.didClosePage(); this.updateHistoryState_(false, {ignoreHash: true}); this.restoreLastFocusedElement_(); }; /** * Cancels (closes) the overlay, due to the user pressing <Esc>. */ OptionsPage.cancelOverlay = function() { // Blur the active element to ensure any changed pref value is saved. document.activeElement.blur(); var overlay = this.getVisibleOverlay_(); // Let the overlay handle the <Esc> if it wants to. if (overlay.handleCancel) { overlay.handleCancel(); this.restoreLastFocusedElement_(); } else { this.closeOverlay(); } }; /** * Hides the visible overlay. Does not affect the history state. * @private */ OptionsPage.hideOverlay_ = function() { var overlay = this.getVisibleOverlay_(); if (overlay) overlay.visible = false; }; /** * Returns the pages which are currently visible, ordered by nesting level * (ascending). * @return {Array.OptionPage} The pages which are currently visible, ordered * by nesting level (ascending). */ OptionsPage.getVisiblePages_ = function() { var visiblePages = []; for (var name in this.registeredPages) { var page = this.registeredPages[name]; if (page.visible) visiblePages[page.nestingLevel] = page; } return visiblePages; }; /** * Returns the topmost visible page (overlays excluded). * @return {OptionPage} The topmost visible page aside any overlay. * @private */ OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { var topPage = null; for (var name in this.registeredPages) { var page = this.registeredPages[name]; if (page.visible && (!topPage || page.nestingLevel > topPage.nestingLevel)) topPage = page; } return topPage; }; /** * Returns the topmost visible page, or null if no page is visible. * @return {OptionPage} The topmost visible page. */ OptionsPage.getTopmostVisiblePage = function() { // Check overlays first since they're top-most if visible. return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); }; /** * Returns the currently visible bubble, or null if no bubble is visible. * @return {AutoCloseBubble} The bubble currently being shown. */ OptionsPage.getVisibleBubble = function() { var bubble = OptionsPage.bubble_; return bubble && !bubble.hidden ? bubble : null; }; /** * Shows an informational bubble displaying |content| and pointing at the * |target| element. If |content| has focusable elements, they join the * current page's tab order as siblings of |domSibling|. * @param {HTMLDivElement} content The content of the bubble. * @param {HTMLElement} target The element at which the bubble points. * @param {HTMLElement} domSibling The element after which the bubble is added * to the DOM. * @param {cr.ui.ArrowLocation} location The arrow location. */ OptionsPage.showBubble = function(content, target, domSibling, location) { OptionsPage.hideBubble(); var bubble = new cr.ui.AutoCloseBubble; bubble.anchorNode = target; bubble.domSibling = domSibling; bubble.arrowLocation = location; bubble.content = content; bubble.show(); OptionsPage.bubble_ = bubble; }; /** * Hides the currently visible bubble, if any. */ OptionsPage.hideBubble = function() { if (OptionsPage.bubble_) OptionsPage.bubble_.hide(); }; /** * Shows the tab contents for the given navigation tab. * @param {!Element} tab The tab that the user clicked. */ OptionsPage.showTab = function(tab) { // Search parents until we find a tab, or the nav bar itself. This allows // tabs to have child nodes, e.g. labels in separately-styled spans. while (tab && !tab.classList.contains('subpages-nav-tabs') && !tab.classList.contains('tab')) { tab = tab.parentNode; } if (!tab || !tab.classList.contains('tab')) return; // Find tab bar of the tab. var tabBar = tab; while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { tabBar = tabBar.parentNode; } if (!tabBar) return; if (tabBar.activeNavTab != null) { tabBar.activeNavTab.classList.remove('active-tab'); $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. remove('active-tab-contents'); } tab.classList.add('active-tab'); $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); tabBar.activeNavTab = tab; }; /** * Registers new options page. * @param {OptionsPage} page Page to register. */ OptionsPage.register = function(page) { this.registeredPages[page.name.toLowerCase()] = page; page.initializePage(); }; /** * Find an enclosing section for an element if it exists. * @param {Element} element Element to search. * @return {OptionPage} The section element, or null. * @private */ OptionsPage.findSectionForNode_ = function(node) { while (node = node.parentNode) { if (node.nodeName == 'SECTION') return node; } return null; }; /** * Registers a new Overlay page. * @param {OptionsPage} overlay Overlay to register. * @param {OptionsPage} parentPage Associated parent page for this overlay. * @param {Array} associatedControls Array of control elements associated with * this page. */ OptionsPage.registerOverlay = function(overlay, parentPage, associatedControls) { this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; overlay.parentPage = parentPage; if (associatedControls) { overlay.associatedControls = associatedControls; if (associatedControls.length) { overlay.associatedSection = this.findSectionForNode_(associatedControls[0]); } // Sanity check. for (var i = 0; i < associatedControls.length; ++i) { assert(associatedControls[i], 'Invalid element passed.'); } } // Reverse the button strip for views. See the documentation of // reverseButtonStripIfNecessary_() for an explanation of why this is done. if (cr.isViews) this.reverseButtonStripIfNecessary_(overlay); overlay.tab = undefined; overlay.isOverlay = true; overlay.initializePage(); }; /** * Reverses the child elements of a button strip if it hasn't already been * reversed. This is necessary because WebKit does not alter the tab order for * elements that are visually reversed using -webkit-box-direction: reverse, * and the button order is reversed for views. See http://webk.it/62664 for * more information. * @param {Object} overlay The overlay containing the button strip to reverse. * @private */ OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { var buttonStrips = overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); // Reverse all button-strips in the overlay. for (var j = 0; j < buttonStrips.length; j++) { var buttonStrip = buttonStrips[j]; var childNodes = buttonStrip.childNodes; for (var i = childNodes.length - 1; i >= 0; i--) buttonStrip.appendChild(childNodes[i]); buttonStrip.setAttribute('reversed', ''); } }; /** * Callback for window.onpopstate to handle back/forward navigations. * @param {Object} data State data pushed into history. */ OptionsPage.setState = function(data) { if (data && data.pageName) { var currentOverlay = this.getVisibleOverlay_(); var lowercaseName = data.pageName.toLowerCase(); var newPage = this.registeredPages[lowercaseName] || this.registeredOverlayPages[lowercaseName] || this.getDefaultPage(); if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { currentOverlay.visible = false; if (currentOverlay.didClosePage) currentOverlay.didClosePage(); } this.showPageByName(data.pageName, false); } }; /** * Callback for window.onbeforeunload. Used to notify overlays that they will * be closed. */ OptionsPage.willClose = function() { var overlay = this.getVisibleOverlay_(); if (overlay && overlay.didClosePage) overlay.didClosePage(); }; /** * Freezes/unfreezes the scroll position of the root page container. * @param {boolean} freeze Whether the page should be frozen. * @private */ OptionsPage.setRootPageFrozen_ = function(freeze) { var container = $('page-container'); if (container.classList.contains('frozen') == freeze) return; if (freeze) { // Lock the width, since auto width computation may change. container.style.width = window.getComputedStyle(container).width; container.oldScrollTop = scrollTopForDocument(document); container.classList.add('frozen'); var verticalPosition = container.getBoundingClientRect().top - container.oldScrollTop; container.style.top = verticalPosition + 'px'; this.updateFrozenElementHorizontalPosition_(container); } else { container.classList.remove('frozen'); container.style.top = ''; container.style.left = ''; container.style.right = ''; container.style.width = ''; } }; /** * Freezes/unfreezes the scroll position of the root page based on the current * page stack. */ OptionsPage.updateRootPageFreezeState = function() { var topPage = OptionsPage.getTopmostVisiblePage(); if (topPage) this.setRootPageFrozen_(topPage.isOverlay); }; /** * Initializes the complete options page. This will cause all C++ handlers to * be invoked to do final setup. */ OptionsPage.initialize = function() { chrome.send('coreOptionsInitialize'); uber.onContentFrameLoaded(); FocusOutlineManager.forDocument(document); document.addEventListener('scroll', this.handleScroll_.bind(this)); // Trigger the scroll handler manually to set the initial state. this.handleScroll_(); // Shake the dialog if the user clicks outside the dialog bounds. var containers = [$('overlay-container-1'), $('overlay-container-2')]; for (var i = 0; i < containers.length; i++) { var overlay = containers[i]; cr.ui.overlay.setupOverlay(overlay); overlay.addEventListener('cancelOverlay', OptionsPage.cancelOverlay.bind(OptionsPage)); } cr.ui.overlay.globalInitialization(); }; /** * Does a bounds check for the element on the given x, y client coordinates. * @param {Element} e The DOM element. * @param {number} x The client X to check. * @param {number} y The client Y to check. * @return {boolean} True if the point falls within the element's bounds. * @private */ OptionsPage.elementContainsPoint_ = function(e, x, y) { var clientRect = e.getBoundingClientRect(); return x >= clientRect.left && x <= clientRect.right && y >= clientRect.top && y <= clientRect.bottom; }; /** * Called when the page is scrolled; moves elements that are position:fixed * but should only behave as if they are fixed for vertical scrolling. * @private */ OptionsPage.handleScroll_ = function() { this.updateAllFrozenElementPositions_(); }; /** * Updates all frozen pages to match the horizontal scroll position. * @private */ OptionsPage.updateAllFrozenElementPositions_ = function() { var frozenElements = document.querySelectorAll('.frozen'); for (var i = 0; i < frozenElements.length; i++) this.updateFrozenElementHorizontalPosition_(frozenElements[i]); }; /** * Updates the given frozen element to match the horizontal scroll position. * @param {HTMLElement} e The frozen element to update. * @private */ OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { if (isRTL()) { e.style.right = OptionsPage.horizontalOffset + 'px'; } else { var scrollLeft = scrollLeftForDocument(document); e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; } }; /** * Change the horizontal offset used to reposition elements while showing an * overlay from the default. */ OptionsPage.setHorizontalOffset = function(value) { OptionsPage.horizontalOffset = value; }; OptionsPage.setClearPluginLSODataEnabled = function(enabled) { if (enabled) { document.documentElement.setAttribute( 'flashPluginSupportsClearSiteData', ''); } else { document.documentElement.removeAttribute( 'flashPluginSupportsClearSiteData'); } if (navigator.plugins['Shockwave Flash']) document.documentElement.setAttribute('hasFlashPlugin', ''); }; OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { if (enabled) { document.documentElement.setAttribute( 'enablePepperFlashSettings', ''); } else { document.documentElement.removeAttribute( 'enablePepperFlashSettings'); } }; OptionsPage.setIsSettingsApp = function() { document.documentElement.classList.add('settings-app'); }; OptionsPage.isSettingsApp = function() { return document.documentElement.classList.contains('settings-app'); }; /** * Whether the page is still loading (i.e. onload hasn't finished running). * @return {boolean} Whether the page is still loading. */ OptionsPage.isLoading = function() { return document.documentElement.classList.contains('loading'); }; OptionsPage.prototype = { __proto__: cr.EventTarget.prototype, /** * The parent page of this option page, or null for top-level pages. * @type {OptionsPage} */ parentPage: null, /** * The section on the parent page that is associated with this page. * Can be null. * @type {Element} */ associatedSection: null, /** * An array of controls that are associated with this page. The first * control should be located on a top-level page. * @type {OptionsPage} */ associatedControls: null, /** * Initializes page content. */ initializePage: function() {}, /** * Sets focus on the first focusable element. Override for a custom focus * strategy. */ focus: function() { // Do not change focus if any control on this page is already focused. if (this.pageDiv.contains(document.activeElement)) return; var elements = this.pageDiv.querySelectorAll( 'input, list, select, textarea, button'); for (var i = 0; i < elements.length; i++) { var element = elements[i]; // Try to focus. If fails, then continue. element.focus(); if (document.activeElement == element) return; } }, /** * Gets the container div for this page if it is an overlay. * @type {HTMLElement} */ get container() { assert(this.isOverlay); return this.pageDiv.parentNode; }, /** * Gets page visibility state. * @type {boolean} */ get visible() { // If this is an overlay dialog it is no longer considered visible while // the overlay is fading out. See http://crbug.com/118629. if (this.isOverlay && this.container.classList.contains('transparent')) { return false; } if (this.pageDiv.hidden) return false; return this.pageDiv.page == this; }, /** * Sets page visibility. * @type {boolean} */ set visible(visible) { if ((this.visible && visible) || (!this.visible && !visible)) return; // If using an overlay, the visibility of the dialog is toggled at the // same time as the overlay to show the dialog's out transition. This // is handled in setOverlayVisible. if (this.isOverlay) { this.setOverlayVisible_(visible); } else { this.pageDiv.page = this; this.pageDiv.hidden = !visible; this.onVisibilityChanged_(); } cr.dispatchPropertyChange(this, 'visible', visible, !visible); }, /** * Shows or hides an overlay (including any visible dialog). * @param {boolean} visible Whether the overlay should be visible or not. * @private */ setOverlayVisible_: function(visible) { assert(this.isOverlay); var pageDiv = this.pageDiv; var container = this.container; if (visible) uber.invokeMethodOnParent('beginInterceptingEvents'); if (container.hidden != visible) { if (visible) { // If the container is set hidden and then immediately set visible // again, the fadeCompleted_ callback would cause it to be erroneously // hidden again. Removing the transparent tag avoids that. container.classList.remove('transparent'); // Hide all dialogs in this container since a different one may have // been previously visible before fading out. var pages = container.querySelectorAll('.page'); for (var i = 0; i < pages.length; i++) pages[i].hidden = true; // Show the new dialog. pageDiv.hidden = false; pageDiv.page = this; } return; } var self = this; var loading = OptionsPage.isLoading(); if (!loading) { // TODO(flackr): Use an event delegate to avoid having to subscribe and // unsubscribe for webkitTransitionEnd events. container.addEventListener('webkitTransitionEnd', function f(e) { if (e.target != e.currentTarget || e.propertyName != 'opacity') return; container.removeEventListener('webkitTransitionEnd', f); self.fadeCompleted_(); }); } if (visible) { container.hidden = false; pageDiv.hidden = false; pageDiv.page = this; // NOTE: This is a hacky way to force the container to layout which // will allow us to trigger the webkit transition. container.scrollTop; this.pageDiv.removeAttribute('aria-hidden'); if (this.parentPage) { this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', true); } container.classList.remove('transparent'); this.onVisibilityChanged_(); } else { // Kick change events for text fields. if (pageDiv.contains(document.activeElement)) document.activeElement.blur(); container.classList.add('transparent'); } if (loading) this.fadeCompleted_(); }, /** * Called when a container opacity transition finishes. * @private */ fadeCompleted_: function() { if (this.container.classList.contains('transparent')) { this.pageDiv.hidden = true; this.container.hidden = true; if (this.parentPage) this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); if (this.nestingLevel == 1) uber.invokeMethodOnParent('stopInterceptingEvents'); this.onVisibilityChanged_(); } }, /** * Called when a page is shown or hidden to update the root options page * based on this page's visibility. * @private */ onVisibilityChanged_: function() { OptionsPage.updateRootPageFreezeState(); if (this.isOverlay && !this.visible) OptionsPage.updateScrollPosition_(); }, /** * The nesting level of this page. * @type {number} The nesting level of this page (0 for top-level page) */ get nestingLevel() { var level = 0; var parent = this.parentPage; while (parent) { level++; parent = parent.parentPage; } return level; }, /** * Whether the page is considered 'sticky', such that it will * remain a top-level page even if sub-pages change. * @type {boolean} True if this page is sticky. */ get sticky() { return false; }, /** * Checks whether this page is an ancestor of the given page in terms of * subpage nesting. * @param {OptionsPage} page The potential descendent of this page. * @return {boolean} True if |page| is nested under this page. */ isAncestorOfPage: function(page) { var parent = page.parentPage; while (parent) { if (parent == this) return true; parent = parent.parentPage; } return false; }, /** * Whether it should be possible to show the page. * @return {boolean} True if the page should be shown. */ canShowPage: function() { return true; }, }; // Export return { OptionsPage: OptionsPage }; });