Index: chrome/browser/resources/options2/options_page.js |
diff --git a/chrome/browser/resources/options2/options_page.js b/chrome/browser/resources/options2/options_page.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0599e0e46d9beac65c823e850c553cd4e7238b02 |
--- /dev/null |
+++ b/chrome/browser/resources/options2/options_page.js |
@@ -0,0 +1,1076 @@ |
+// 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; |
+ } |
+ |
+ const SUBPAGE_SHEET_COUNT = 2; |
+ |
+ /** |
+ * 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 = {}; |
+ |
+ /** |
+ * 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.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_(); |
+ 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 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 (!targetPageWasVisible && 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; |
+ } |
+ }; |
+ |
+ /** |
+ * Scrolls the page to the correct position (the top when opening a subpage, |
+ * or the old scroll position a previously hidden subpage becomes visible). |
+ * @private |
+ */ |
+ OptionsPage.updateScrollPosition_ = function () { |
+ var topmostPage = this.getTopmostVisibleNonOverlayPage_(); |
+ var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; |
+ |
+ var container = (nestingLevel > 0) ? |
+ $('subpage-sheet-container-' + nestingLevel) : $('page-container'); |
+ |
+ var scrollTop = container.oldScrollTop || 0; |
+ container.oldScrollTop = undefined; |
+ window.scroll(document.body.scrollLeft, scrollTop); |
+ }; |
+ |
+ /** |
+ * 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).replace(/\/$/, ''); // Remove trailing slash. |
+ // 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.toLowerCase()]; |
+ if (!overlay || !overlay.canShowPage()) |
+ return false; |
+ |
+ if ((!rootPage || !rootPage.sticky) && overlay.parentPage) |
+ this.showPageByName(overlay.parentPage.name, false); |
+ |
+ if (!overlay.visible) { |
+ 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; |
+ }; |
+ |
+ /** |
+ * 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) { |
+ if (topPage.willHidePage) |
+ topPage.willHidePage(); |
+ 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) { |
+ if (topPage.willHidePage) |
+ topPage.willHidePage(); |
+ 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; |
+ |
+ // 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; |
+ // 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.setAttribute('role', 'tab'); |
+ pageNav.textContent = page.pageDiv.querySelector('h1').textContent; |
+ pageNav.tabIndex = -1; |
+ pageNav.onclick = function(event) { |
+ OptionsPage.navigateToPage(this.getAttribute('pageName')); |
+ }; |
+ pageNav.onkeydown = function(event) { |
+ if ((event.keyCode == 37 || event.keyCode==38) && |
+ this.previousSibling && this.previousSibling.onkeydown) { |
+ // Left and up arrow moves back one tab. |
+ OptionsPage.navigateToPage( |
+ this.previousSibling.getAttribute('pageName')); |
+ this.previousSibling.focus(); |
+ } else if ((event.keyCode == 39 || event.keyCode == 40) && |
+ this.nextSibling) { |
+ // Right and down arrows move forward one tab. |
+ OptionsPage.navigateToPage(this.nextSibling.getAttribute('pageName')); |
+ this.nextSibling.focus(); |
+ } |
+ }; |
+ 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.toLowerCase()] = 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.toLowerCase()] = overlay; |
+ overlay.parentPage = parentPage; |
+ if (associatedControls) { |
+ overlay.associatedControls = associatedControls; |
+ if (associatedControls.length) { |
+ overlay.associatedSection = |
+ this.findSectionForNode_(associatedControls[0]); |
+ } |
+ } |
+ |
+ // Reverse the button strip for views. See the documentation of |
+ // reverseButtonStrip_() for an explanation of why this is necessary. |
+ if (cr.isViews) |
+ this.reverseButtonStrip_(overlay); |
+ |
+ overlay.tab = undefined; |
+ overlay.isOverlay = true; |
+ overlay.initializePage(); |
+ }; |
+ |
+ /** |
+ * Reverses the child elements of a button strip. 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 https://bugs.webkit.org/show_bug.cgi?id=62664 for more |
+ * information. |
+ * @param {Object} overlay The overlay containing the button strip to reverse. |
+ * @private |
+ */ |
+ OptionsPage.reverseButtonStrip_ = function(overlay) { |
+ var buttonStrips = overlay.pageDiv.querySelectorAll('.button-strip'); |
+ |
+ // 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]); |
+ } |
+ }; |
+ |
+ /** |
+ * 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.toLowerCase()]) { |
+ 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 ? $('page-container') |
+ : $('subpage-sheet-container-' + level); |
+ |
+ 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 = document.body.scrollTop; |
+ 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 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); |
+ $('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; |
+ |
+ // Don't close subpages if a user is clicking in a select element. |
+ // This is necessary because WebKit sends click events with strange |
+ // coordinates when a user selects a new entry in a select element. |
+ // See: http://crbug.com/87199 |
+ if (event.srcElement.nodeName == 'SELECT') |
+ return; |
+ |
+ // Do nothing if the client coordinates are not within the source element. |
+ // This occurs if the user toggles a checkbox by pressing spacebar. |
+ // This is a workaround to prevent keyboard events from closing the window. |
+ // See: crosbug.com/15678 |
+ 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_()) |
+ 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() {}, |
+ |
+ /** |
+ * 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 controlledByPolicy = false; |
+ var controlledByExtension = false; |
+ var inputElements = this.pageDiv.querySelectorAll('input[controlled-by]'); |
+ for (var i = 0, len = inputElements.length; i < len; i++) { |
+ if (inputElements[i].controlledBy == 'policy') |
+ controlledByPolicy = true; |
+ else if (inputElements[i].controlledBy == 'extension') |
+ controlledByExtension = true; |
+ } |
+ if (!controlledByPolicy && !controlledByExtension) { |
+ bannerDiv.hidden = true; |
+ } else { |
+ bannerDiv.hidden = false; |
+ var height = window.getComputedStyle(bannerDiv).height; |
+ if (controlledByPolicy && !controlledByExtension) { |
+ $('managed-prefs-text').textContent = |
+ templateData.policyManagedPrefsBannerText; |
+ } else if (!controlledByPolicy && controlledByExtension) { |
+ $('managed-prefs-text').textContent = |
+ templateData.extensionManagedPrefsBannerText; |
+ } else if (controlledByPolicy && controlledByExtension) { |
+ $('managed-prefs-text').textContent = |
+ templateData.policyAndExtensionManagedPrefsBannerText; |
+ } |
+ } |
+ }, |
+ |
+ /** |
+ * Gets page visibility state. |
+ */ |
+ get visible() { |
+ return !this.pageDiv.hidden; |
+ }, |
+ |
+ /** |
+ * Sets page visibility. |
+ */ |
+ set visible(visible) { |
+ if ((this.visible && visible) || (!this.visible && !visible)) |
+ return; |
+ |
+ this.setContainerVisibility_(visible); |
+ if (visible) { |
+ this.pageDiv.hidden = false; |
+ |
+ if (this.tab) { |
+ this.tab.classList.add('navbar-item-selected'); |
+ this.tab.setAttribute('aria-selected', 'true'); |
+ this.tab.tabIndex = 0; |
+ } |
+ } else { |
+ this.pageDiv.hidden = true; |
+ |
+ if (this.tab) { |
+ this.tab.classList.remove('navbar-item-selected'); |
+ this.tab.setAttribute('aria-selected', 'false'); |
+ this.tab.tabIndex = -1; |
+ } |
+ } |
+ |
+ OptionsPage.updatePageFreezeStates(); |
+ |
+ // 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(); |
+ |
+ // A subpage was shown or hidden. |
+ if (!this.isOverlay && this.nestingLevel > 0) { |
+ OptionsPage.updateSubpageBackdrop_(); |
+ OptionsPage.updateScrollPosition_(); |
+ } |
+ |
+ 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) |
+ return; |
+ |
+ 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'); |
+ } |
+ 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; |
+ }, |
+ }; |
+ |
+ // Export |
+ return { |
+ OptionsPage: OptionsPage |
+ }; |
+}); |