// 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
};
});