// Copyright (c) 2011 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() { ///////////////////////////////////////////////////////////////////////////// // OptionsPage class: /** * Base class for options page. * @constructor * @param {string} name Options page name, also defines id of the div element * containing the options view and the name of options page navigation bar * item as name+'PageNav'. * @param {string} title Options page title, used for navigation bar * @extends {EventTarget} */ function OptionsPage(name, title, pageDivName) { this.name = name; this.title = title; this.pageDivName = pageDivName; this.pageDiv = $(this.pageDivName); this.tab = null; this.managed = false; } const SUBPAGE_SHEET_COUNT = 2; /** * Main level option pages. * @protected */ OptionsPage.registeredPages = {}; /** * Pages which are meant to behave like modal dialogs. * @protected */ OptionsPage.registeredOverlayPages = {}; /** * Whether or not |initialize| has been called. * @private */ OptionsPage.initialized_ = false; /** * 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 pages and sub-pages. * @param {string} pageName Page name. * @param {boolean} updateHistory True if we should update the history after * showing the page. * @private */ OptionsPage.showPageByName = function(pageName, updateHistory) { // 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]; 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_(); return; } else { targetPage = this.getDefaultPage(); } } pageName = targetPage.name; // Determine if the root page is 'sticky', meaning that it // shouldn't change when showing a sub-page. This can happen for special // pages like Search. var isRootPageLocked = rootPage && rootPage.sticky && targetPage.parentPage; // Notify pages if they will be hidden. for (var name in this.registeredPages) { var page = this.registeredPages[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 name in this.registeredPages) { var page = this.registeredPages[name]; if (!page.parentPage && isRootPageLocked) continue; page.visible = name == pageName || (!document.documentElement.classList.contains('hide-menu') && page.isAncestorOfPage(targetPage)); } // Update the history and current location. if (updateHistory) this.updateHistoryState_(); // Always update the page title. document.title = targetPage.title; // Notify pages if they were shown. for (var name in this.registeredPages) { var page = this.registeredPages[name]; if (!page.parentPage && isRootPageLocked) continue; if (page.didShowPage && (name == pageName || page.isAncestorOfPage(targetPage))) page.didShowPage(); } }; /** * Updates the visibility and stacking order of the subpage backdrop * according to which subpage is topmost and visible. * @private */ OptionsPage.updateSubpageBackdrop_ = function () { var topmostPage = this.getTopmostVisibleNonOverlayPage_(); var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; var subpageBackdrop = $('subpage-backdrop'); if (nestingLevel > 0) { var container = $('subpage-sheet-container-' + nestingLevel); subpageBackdrop.style.zIndex = parseInt(window.getComputedStyle(container).zIndex) - 1; subpageBackdrop.hidden = false; } else { subpageBackdrop.hidden = true; } }; /** * Pushes the current page onto the history stack, overriding the last page * if it is the generic chrome://settings/. * @private */ OptionsPage.updateHistoryState_ = function() { var page = this.getTopmostVisiblePage(); var path = location.pathname; if (path) path = path.slice(1); // The page is already in history (the user may have clicked the same link // twice). Do nothing. if (path == page.name) return; // If there is no path, the current location is chrome://settings/. // Override this with the new page. var historyFunction = path ? window.history.pushState : window.history.replaceState; historyFunction.call(window.history, {pageName: page.name}, page.title, '/' + page.name); // Update tab title. document.title = page.title; }; /** * 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]; if (!overlay || !overlay.canShowPage()) return false; if ((!rootPage || !rootPage.sticky) && overlay.parentPage) this.showPageByName(overlay.parentPage.name, false); overlay.visible = true; if (overlay.didShowPage) overlay.didShowPage(); 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; }; /** * @return {boolean} True if the visible overlay should be closed. * @private */ OptionsPage.shouldCloseOverlay_ = function() { var overlay = this.getVisibleOverlay_(); return overlay && overlay.shouldClose(); }; /** * Returns the currently visible overlay, or null if no page is visible. * @return {OptionPage} The visible overlay. */ OptionsPage.getVisibleOverlay_ = function() { for (var name in this.registeredOverlayPages) { var page = this.registeredOverlayPages[name]; if (page.visible) return page; } return null; }; /** * 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_(); }; /** * 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 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_(); }; /** * Closes the topmost open subpage, if any. * @private */ OptionsPage.closeTopSubPage_ = function() { var topPage = this.getTopmostVisiblePage(); if (topPage && !topPage.isOverlay && topPage.parentPage) topPage.visible = false; this.updateHistoryState_(); }; /** * Closes all subpages below the given level. * @param {number} level The nesting level to close below. */ OptionsPage.closeSubPagesToLevel = function(level) { var topPage = this.getTopmostVisiblePage(); while (topPage && topPage.nestingLevel > level) { topPage.visible = false; topPage = topPage.parentPage; } this.updateHistoryState_(); }; /** * Updates managed banner visibility state based on the topmost page. */ OptionsPage.updateManagedBannerVisibility = function() { var topPage = this.getTopmostVisiblePage(); if (topPage) topPage.updateManagedBannerVisibility(); }; /** * 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; if (this.activeNavTab != null) { this.activeNavTab.classList.remove('active-tab'); $(this.activeNavTab.getAttribute('tab-contents')).classList. remove('active-tab-contents'); } tab.classList.add('active-tab'); $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); this.activeNavTab = tab; }; /** * Registers new options page. * @param {OptionsPage} page Page to register. */ OptionsPage.register = function(page) { this.registeredPages[page.name] = page; // Create and add new page <li> element to navbar. var pageNav = document.createElement('li'); pageNav.id = page.name + 'PageNav'; pageNav.className = 'navbar-item'; pageNav.setAttribute('pageName', page.name); pageNav.textContent = page.pageDiv.querySelector('h1').textContent; pageNav.tabIndex = 0; pageNav.onclick = function(event) { OptionsPage.navigateToPage(this.getAttribute('pageName')); }; pageNav.onkeypress = function(event) { // Enter or space if (event.keyCode == 13 || event.keyCode == 32) { OptionsPage.navigateToPage(this.getAttribute('pageName')); } }; var navbar = $('navbar'); navbar.appendChild(pageNav); page.tab = pageNav; 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 Sub-page. * @param {OptionsPage} subPage Sub-page to register. * @param {OptionsPage} parentPage Associated parent page for this page. * @param {Array} associatedControls Array of control elements that lead to * this sub-page. The first item is typically a button in a root-level * page. There may be additional buttons for nested sub-pages. */ OptionsPage.registerSubPage = function(subPage, parentPage, associatedControls) { this.registeredPages[subPage.name] = subPage; subPage.parentPage = parentPage; if (associatedControls) { subPage.associatedControls = associatedControls; if (associatedControls.length) { subPage.associatedSection = this.findSectionForNode_(associatedControls[0]); } } subPage.tab = undefined; subPage.initializePage(); }; /** * 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] = overlay; overlay.parentPage = parentPage; if (associatedControls) { overlay.associatedControls = associatedControls; if (associatedControls.length) { overlay.associatedSection = this.findSectionForNode_(associatedControls[0]); } } overlay.tab = undefined; overlay.isOverlay = true; overlay.initializePage(); }; /** * Callback for window.onpopstate. * @param {Object} data State data pushed into history. */ OptionsPage.setState = function(data) { if (data && data.pageName) { // It's possible an overlay may be the last top-level page shown. if (this.isOverlayVisible_() && this.registeredOverlayPages[data.pageName] == undefined) { this.hideOverlay_(); } 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 given level's page container. * @param {boolean} freeze Whether the page should be frozen. * @param {number} level The level to freeze/unfreeze. * @private */ OptionsPage.setPageFrozenAtLevel_ = function(freeze, level) { var container = level == 0 ? $('toplevel-page-container') : $('subpage-sheet-container-' + level); if (container.classList.contains('frozen') == freeze) return; if (freeze) { var scrollPosition = document.body.scrollTop; // Lock the width, since auto width computation may change. container.style.width = window.getComputedStyle(container).width; container.classList.add('frozen'); container.style.top = -scrollPosition + 'px'; this.updateFrozenElementHorizontalPosition_(container); } else { var scrollPosition = - parseInt(container.style.top, 10); container.classList.remove('frozen'); container.style.top = ''; container.style.left = ''; container.style.right = ''; container.style.width = ''; // Restore the scroll position. if (!container.hidden) window.scroll(document.body.scrollLeft, scrollPosition); } }; /** * Freezes/unfreezes the scroll position of visible pages based on the current * page stack. */ OptionsPage.updatePageFreezeStates = function() { var topPage = OptionsPage.getTopmostVisiblePage(); if (!topPage) return; var nestingLevel = topPage.isOverlay ? 100 : topPage.nestingLevel; for (var i = 0; i <= SUBPAGE_SHEET_COUNT; i++) { this.setPageFrozenAtLevel_(i < nestingLevel, i); } }; /** * Initializes the complete options page. This will cause all C++ handlers to * be invoked to do final setup. */ OptionsPage.initialize = function() { chrome.send('coreOptionsInitialize'); this.initialized_ = true; document.addEventListener('scroll', this.handleScroll_.bind(this)); window.addEventListener('resize', this.handleResize_.bind(this)); if (!document.documentElement.classList.contains('hide-menu')) { // Close subpages if the user clicks on the html body. Listen in the // capturing phase so that we can stop the click from doing anything. document.body.addEventListener('click', this.bodyMouseEventHandler_.bind(this), true); // We also need to cancel mousedowns on non-subpage content. document.body.addEventListener('mousedown', this.bodyMouseEventHandler_.bind(this), true); var self = this; // Hook up the close buttons. subpageCloseButtons = document.querySelectorAll('.close-subpage'); for (var i = 0; i < subpageCloseButtons.length; i++) { subpageCloseButtons[i].onclick = function() { self.closeTopSubPage_(); }; }; // Install handler for key presses. document.addEventListener('keydown', this.keyDownEventHandler_.bind(this)); document.addEventListener('focus', this.manageFocusChange_.bind(this), true); } // Calculate and store the horizontal locations of elements that may be // frozen later. var sidebarWidth = parseInt(window.getComputedStyle($('mainview')).webkitPaddingStart, 10); $('toplevel-page-container').horizontalOffset = sidebarWidth + parseInt(window.getComputedStyle( $('mainview-content')).webkitPaddingStart, 10); for (var level = 1; level <= SUBPAGE_SHEET_COUNT; level++) { var containerId = 'subpage-sheet-container-' + level; $(containerId).horizontalOffset = sidebarWidth; } $('subpage-backdrop').horizontalOffset = sidebarWidth; // Trigger the resize handler manually to set the initial state. this.handleResize_(null); }; /** * 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 focus changes; ensures that focus doesn't move outside * the topmost subpage/overlay. * @param {Event} e The focus change event. * @private */ OptionsPage.manageFocusChange_ = function(e) { var focusableItemsRoot; var topPage = this.getTopmostVisiblePage(); if (!topPage) return; if (topPage.isOverlay) { // If an overlay is visible, that defines the tab loop. focusableItemsRoot = topPage.pageDiv; } else { // If a subpage is visible, use its parent as the tab loop constraint. // (The parent is used because it contains the close button.) if (topPage.nestingLevel > 0) focusableItemsRoot = topPage.pageDiv.parentNode; } if (focusableItemsRoot && !focusableItemsRoot.contains(e.target)) topPage.focusFirstElement(); }; /** * Called when the page is scrolled; moves elements that are position:fixed * but should only behave as if they are fixed for vertical scrolling. * @param {Event} e The scroll event. * @private */ OptionsPage.handleScroll_ = function(e) { var scrollHorizontalOffset = document.body.scrollLeft; // position:fixed doesn't seem to work for horizontal scrolling in RTL mode, // so only adjust in LTR mode (where scroll values will be positive). if (scrollHorizontalOffset >= 0) { $('navbar-container').style.left = -scrollHorizontalOffset + 'px'; var subpageBackdrop = $('subpage-backdrop'); subpageBackdrop.style.left = subpageBackdrop.horizontalOffset - scrollHorizontalOffset + 'px'; 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 (document.documentElement.dir == 'rtl') e.style.right = e.horizontalOffset + 'px'; else e.style.left = e.horizontalOffset - document.body.scrollLeft + 'px'; }; /** * Called when the page is resized; adjusts the size of elements that depend * on the veiwport. * @param {Event} e The resize event. * @private */ OptionsPage.handleResize_ = function(e) { // Set an explicit height equal to the viewport on all the subpage // containers shorter than the viewport. This is used instead of // min-height: 100% so that there is an explicit height for the subpages' // min-height: 100%. var viewportHeight = document.documentElement.clientHeight; var subpageContainers = document.querySelectorAll('.subpage-sheet-container'); for (var i = 0; i < subpageContainers.length; i++) { if (subpageContainers[i].scrollHeight > viewportHeight) subpageContainers[i].style.removeProperty('height'); else subpageContainers[i].style.height = viewportHeight + 'px'; } }; /** * A function to handle mouse events (mousedown or click) on the html body by * closing subpages and/or stopping event propagation. * @return {Event} a mousedown or click event. * @private */ OptionsPage.bodyMouseEventHandler_ = function(event) { // Do nothing if a subpage isn't showing. var topPage = this.getTopmostVisiblePage(); if (!topPage || topPage.isOverlay || !topPage.parentPage) return; // Do nothing if the client coordinates are not within the source element. // This situation is indicative of a Webkit bug where clicking on a // radio/checkbox label span will generate an event with client coordinates // of (-scrollX, -scrollY). // See https://bugs.webkit.org/show_bug.cgi?id=56606 if (event.clientX == -document.body.scrollLeft && event.clientY == -document.body.scrollTop) { return; } // Don't interfere with navbar clicks. if ($('navbar').contains(event.target)) return; // Figure out which page the click happened in. for (var level = topPage.nestingLevel; level >= 0; level--) { var clickIsWithinLevel = level == 0 ? true : OptionsPage.elementContainsPoint_( $('subpage-sheet-' + level), event.clientX, event.clientY); if (!clickIsWithinLevel) continue; // Event was within the topmost page; do nothing. if (topPage.nestingLevel == level) return; // Block propgation of both clicks and mousedowns, but only close subpages // on click. if (event.type == 'click') this.closeSubPagesToLevel(level); event.stopPropagation(); event.preventDefault(); return; } }; /** * A function to handle key press events. * @return {Event} a keydown event. * @private */ OptionsPage.keyDownEventHandler_ = function(event) { // Close the top overlay or sub-page on esc. if (event.keyCode == 27) { // Esc if (this.isOverlayVisible_()) { if (this.shouldCloseOverlay_()) this.closeOverlay(); } else { this.closeTopSubPage_(); } } }; OptionsPage.setClearPluginLSODataEnabled = function(enabled) { if (enabled) { document.documentElement.setAttribute( 'flashPluginSupportsClearSiteData', ''); } else { document.documentElement.removeAttribute( 'flashPluginSupportsClearSiteData'); } }; /** * Re-initializes the C++ handlers if necessary. This is called if the * handlers are torn down and recreated but the DOM may not have been (in * which case |initialize| won't be called again). If |initialize| hasn't been * called, this does nothing (since it will be later, once the DOM has * finished loading). */ OptionsPage.reinitializeCore = function() { if (this.initialized_) chrome.send('coreOptionsInitialize'); } 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 managed banner visibility state. */ setManagedBannerVisibility: function(visible) { this.managed = visible; if (this.visible) { this.updateManagedBannerVisibility(); } }, /** * Updates managed banner visibility state. This function iterates over * all input fields of a window and if any of these is marked as managed * it triggers the managed banner to be visible. The banner can be enforced * being on through the managed flag of this class but it can not be forced * being off if managed items exist. */ updateManagedBannerVisibility: function() { var bannerDiv = $('managed-prefs-banner'); var hasManaged = this.managed; if (!hasManaged) { var inputElements = this.pageDiv.querySelectorAll('input'); for (var i = 0, len = inputElements.length; i < len; i++) { if (inputElements[i].managed) { hasManaged = true; break; } } } if (hasManaged) { bannerDiv.hidden = false; var height = window.getComputedStyle($('managed-prefs-banner')).height; $('subpage-backdrop').style.top = height; } else { bannerDiv.hidden = true; $('subpage-backdrop').style.top = '0'; } }, /** * Gets page visibility state. */ get visible() { var page = $(this.pageDivName); return page && page.ownerDocument.defaultView.getComputedStyle( page).display == 'block'; }, /** * Sets page visibility. */ set visible(visible) { if ((this.visible && visible) || (!this.visible && !visible)) return; this.setContainerVisibility_(visible); if (visible) { this.pageDiv.classList.remove('hidden'); if (this.tab) this.tab.classList.add('navbar-item-selected'); } else { this.pageDiv.classList.add('hidden'); if (this.tab) this.tab.classList.remove('navbar-item-selected'); } OptionsPage.updatePageFreezeStates(); // A subpage was shown or hidden. if (!this.isOverlay && this.nestingLevel > 0) { OptionsPage.updateSubpageBackdrop_(); if (visible) { // Scroll to the top of the newly-opened subpage. window.scroll(document.body.scrollLeft, 0) } } // The managed prefs banner is global, so after any visibility change // update it based on the topmost page, not necessarily this page // (e.g., if an ancestor is made visible after a child). OptionsPage.updateManagedBannerVisibility(); cr.dispatchPropertyChange(this, 'visible', visible, !visible); }, /** * Shows or hides this page's container. * @param {boolean} visible Whether the container should be visible or not. * @private */ setContainerVisibility_: function(visible) { var container = null; if (this.isOverlay) { container = $('overlay'); } else { var nestingLevel = this.nestingLevel; if (nestingLevel > 0) container = $('subpage-sheet-container-' + nestingLevel); } var isSubpage = !this.isOverlay; if (!container || container.hidden != visible) return; if (visible) { container.hidden = false; if (isSubpage) { var computedStyle = window.getComputedStyle(container); container.style.WebkitPaddingStart = parseInt(computedStyle.WebkitPaddingStart, 10) + 100 + 'px'; } // Separate animating changes from the removal of display:none. window.setTimeout(function() { container.classList.remove('transparent'); if (isSubpage) container.style.WebkitPaddingStart = ''; }); } else { var self = this; container.addEventListener('webkitTransitionEnd', function f(e) { if (e.propertyName != 'opacity') return; container.removeEventListener('webkitTransitionEnd', f); self.fadeCompleted_(container); }); container.classList.add('transparent'); } }, /** * Called when a container opacity transition finishes. * @param {HTMLElement} container The container element. * @private */ fadeCompleted_: function(container) { if (container.classList.contains('transparent')) container.hidden = true; }, /** * Focuses the first control on the page. */ focusFirstElement: function() { // Sets focus on the first interactive element in the page. var focusElement = this.pageDiv.querySelector('button, input, list, select'); if (focusElement) focusElement.focus(); }, /** * 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 * @return {boolean} True if this page is nested under |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; }, /** * Whether an overlay should be closed. Used by overlay implementation to * handle special closing behaviors. * @return {boolean} True if the overlay should be closed. */ shouldClose: function() { return true; }, }; // Export return { OptionsPage: OptionsPage }; });