Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 cr.define('options', function() {
      6   /////////////////////////////////////////////////////////////////////////////
      7   // OptionsPage class:
      8 
      9   /**
     10    * Base class for options page.
     11    * @constructor
     12    * @param {string} name Options page name, also defines id of the div element
     13    *     containing the options view and the name of options page navigation bar
     14    *     item as name+'PageNav'.
     15    * @param {string} title Options page title, used for navigation bar
     16    * @extends {EventTarget}
     17    */
     18   function OptionsPage(name, title, pageDivName) {
     19     this.name = name;
     20     this.title = title;
     21     this.pageDivName = pageDivName;
     22     this.pageDiv = $(this.pageDivName);
     23     this.tab = null;
     24     this.managed = false;
     25   }
     26 
     27   const SUBPAGE_SHEET_COUNT = 2;
     28 
     29   /**
     30    * Main level option pages.
     31    * @protected
     32    */
     33   OptionsPage.registeredPages = {};
     34 
     35   /**
     36    * Pages which are meant to behave like modal dialogs.
     37    * @protected
     38    */
     39   OptionsPage.registeredOverlayPages = {};
     40 
     41   /**
     42    * Whether or not |initialize| has been called.
     43    * @private
     44    */
     45   OptionsPage.initialized_ = false;
     46 
     47   /**
     48    * Gets the default page (to be shown on initial load).
     49    */
     50   OptionsPage.getDefaultPage = function() {
     51     return BrowserOptions.getInstance();
     52   };
     53 
     54   /**
     55    * Shows the default page.
     56    */
     57   OptionsPage.showDefaultPage = function() {
     58     this.navigateToPage(this.getDefaultPage().name);
     59   };
     60 
     61   /**
     62    * "Navigates" to a page, meaning that the page will be shown and the
     63    * appropriate entry is placed in the history.
     64    * @param {string} pageName Page name.
     65    */
     66   OptionsPage.navigateToPage = function(pageName) {
     67     this.showPageByName(pageName, true);
     68   };
     69 
     70   /**
     71    * Shows a registered page. This handles both top-level pages and sub-pages.
     72    * @param {string} pageName Page name.
     73    * @param {boolean} updateHistory True if we should update the history after
     74    *     showing the page.
     75    * @private
     76    */
     77   OptionsPage.showPageByName = function(pageName, updateHistory) {
     78     // Find the currently visible root-level page.
     79     var rootPage = null;
     80     for (var name in this.registeredPages) {
     81       var page = this.registeredPages[name];
     82       if (page.visible && !page.parentPage) {
     83         rootPage = page;
     84         break;
     85       }
     86     }
     87 
     88     // Find the target page.
     89     var targetPage = this.registeredPages[pageName];
     90     if (!targetPage || !targetPage.canShowPage()) {
     91       // If it's not a page, try it as an overlay.
     92       if (!targetPage && this.showOverlay_(pageName, rootPage)) {
     93         if (updateHistory)
     94           this.updateHistoryState_();
     95         return;
     96       } else {
     97         targetPage = this.getDefaultPage();
     98       }
     99     }
    100 
    101     pageName = targetPage.name;
    102 
    103     // Determine if the root page is 'sticky', meaning that it
    104     // shouldn't change when showing a sub-page.  This can happen for special
    105     // pages like Search.
    106     var isRootPageLocked =
    107         rootPage && rootPage.sticky && targetPage.parentPage;
    108 
    109     // Notify pages if they will be hidden.
    110     for (var name in this.registeredPages) {
    111       var page = this.registeredPages[name];
    112       if (!page.parentPage && isRootPageLocked)
    113         continue;
    114       if (page.willHidePage && name != pageName &&
    115           !page.isAncestorOfPage(targetPage))
    116         page.willHidePage();
    117     }
    118 
    119     // Update visibilities to show only the hierarchy of the target page.
    120     for (var name in this.registeredPages) {
    121       var page = this.registeredPages[name];
    122       if (!page.parentPage && isRootPageLocked)
    123         continue;
    124       page.visible = name == pageName ||
    125           (!document.documentElement.classList.contains('hide-menu') &&
    126            page.isAncestorOfPage(targetPage));
    127     }
    128 
    129     // Update the history and current location.
    130     if (updateHistory)
    131       this.updateHistoryState_();
    132 
    133     // Always update the page title.
    134     document.title = targetPage.title;
    135 
    136     // Notify pages if they were shown.
    137     for (var name in this.registeredPages) {
    138       var page = this.registeredPages[name];
    139       if (!page.parentPage && isRootPageLocked)
    140         continue;
    141       if (page.didShowPage && (name == pageName ||
    142           page.isAncestorOfPage(targetPage)))
    143         page.didShowPage();
    144     }
    145   };
    146 
    147   /**
    148    * Updates the visibility and stacking order of the subpage backdrop
    149    * according to which subpage is topmost and visible.
    150    * @private
    151    */
    152   OptionsPage.updateSubpageBackdrop_ = function () {
    153     var topmostPage = this.getTopmostVisibleNonOverlayPage_();
    154     var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0;
    155 
    156     var subpageBackdrop = $('subpage-backdrop');
    157     if (nestingLevel > 0) {
    158       var container = $('subpage-sheet-container-' + nestingLevel);
    159       subpageBackdrop.style.zIndex =
    160           parseInt(window.getComputedStyle(container).zIndex) - 1;
    161       subpageBackdrop.hidden = false;
    162     } else {
    163       subpageBackdrop.hidden = true;
    164     }
    165   };
    166 
    167   /**
    168    * Pushes the current page onto the history stack, overriding the last page
    169    * if it is the generic chrome://settings/.
    170    * @private
    171    */
    172   OptionsPage.updateHistoryState_ = function() {
    173     var page = this.getTopmostVisiblePage();
    174     var path = location.pathname;
    175     if (path)
    176       path = path.slice(1);
    177     // The page is already in history (the user may have clicked the same link
    178     // twice). Do nothing.
    179     if (path == page.name)
    180       return;
    181 
    182     // If there is no path, the current location is chrome://settings/.
    183     // Override this with the new page.
    184     var historyFunction = path ? window.history.pushState :
    185                                  window.history.replaceState;
    186     historyFunction.call(window.history,
    187                          {pageName: page.name},
    188                          page.title,
    189                          '/' + page.name);
    190     // Update tab title.
    191     document.title = page.title;
    192   };
    193 
    194   /**
    195    * Shows a registered Overlay page. Does not update history.
    196    * @param {string} overlayName Page name.
    197    * @param {OptionPage} rootPage The currently visible root-level page.
    198    * @return {boolean} whether we showed an overlay.
    199    */
    200   OptionsPage.showOverlay_ = function(overlayName, rootPage) {
    201     var overlay = this.registeredOverlayPages[overlayName];
    202     if (!overlay || !overlay.canShowPage())
    203       return false;
    204 
    205     if ((!rootPage || !rootPage.sticky) && overlay.parentPage)
    206       this.showPageByName(overlay.parentPage.name, false);
    207 
    208     overlay.visible = true;
    209     if (overlay.didShowPage) overlay.didShowPage();
    210     return true;
    211   };
    212 
    213   /**
    214    * Returns whether or not an overlay is visible.
    215    * @return {boolean} True if an overlay is visible.
    216    * @private
    217    */
    218   OptionsPage.isOverlayVisible_ = function() {
    219     return this.getVisibleOverlay_() != null;
    220   };
    221 
    222   /**
    223    * @return {boolean} True if the visible overlay should be closed.
    224    * @private
    225    */
    226   OptionsPage.shouldCloseOverlay_ = function() {
    227     var overlay = this.getVisibleOverlay_();
    228     return overlay && overlay.shouldClose();
    229   };
    230 
    231   /**
    232    * Returns the currently visible overlay, or null if no page is visible.
    233    * @return {OptionPage} The visible overlay.
    234    */
    235   OptionsPage.getVisibleOverlay_ = function() {
    236     for (var name in this.registeredOverlayPages) {
    237       var page = this.registeredOverlayPages[name];
    238       if (page.visible)
    239         return page;
    240     }
    241     return null;
    242   };
    243 
    244   /**
    245    * Closes the visible overlay. Updates the history state after closing the
    246    * overlay.
    247    */
    248   OptionsPage.closeOverlay = function() {
    249     var overlay = this.getVisibleOverlay_();
    250     if (!overlay)
    251       return;
    252 
    253     overlay.visible = false;
    254     if (overlay.didClosePage) overlay.didClosePage();
    255     this.updateHistoryState_();
    256   };
    257 
    258   /**
    259    * Hides the visible overlay. Does not affect the history state.
    260    * @private
    261    */
    262   OptionsPage.hideOverlay_ = function() {
    263     var overlay = this.getVisibleOverlay_();
    264     if (overlay)
    265       overlay.visible = false;
    266   };
    267 
    268   /**
    269    * Returns the topmost visible page (overlays excluded).
    270    * @return {OptionPage} The topmost visible page aside any overlay.
    271    * @private
    272    */
    273   OptionsPage.getTopmostVisibleNonOverlayPage_ = function() {
    274     var topPage = null;
    275     for (var name in this.registeredPages) {
    276       var page = this.registeredPages[name];
    277       if (page.visible &&
    278           (!topPage || page.nestingLevel > topPage.nestingLevel))
    279         topPage = page;
    280     }
    281 
    282     return topPage;
    283   };
    284 
    285   /**
    286    * Returns the topmost visible page, or null if no page is visible.
    287    * @return {OptionPage} The topmost visible page.
    288    */
    289   OptionsPage.getTopmostVisiblePage = function() {
    290     // Check overlays first since they're top-most if visible.
    291     return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_();
    292   };
    293 
    294   /**
    295    * Closes the topmost open subpage, if any.
    296    * @private
    297    */
    298   OptionsPage.closeTopSubPage_ = function() {
    299     var topPage = this.getTopmostVisiblePage();
    300     if (topPage && !topPage.isOverlay && topPage.parentPage)
    301       topPage.visible = false;
    302 
    303     this.updateHistoryState_();
    304   };
    305 
    306   /**
    307    * Closes all subpages below the given level.
    308    * @param {number} level The nesting level to close below.
    309    */
    310   OptionsPage.closeSubPagesToLevel = function(level) {
    311     var topPage = this.getTopmostVisiblePage();
    312     while (topPage && topPage.nestingLevel > level) {
    313       topPage.visible = false;
    314       topPage = topPage.parentPage;
    315     }
    316 
    317     this.updateHistoryState_();
    318   };
    319 
    320   /**
    321    * Updates managed banner visibility state based on the topmost page.
    322    */
    323   OptionsPage.updateManagedBannerVisibility = function() {
    324     var topPage = this.getTopmostVisiblePage();
    325     if (topPage)
    326       topPage.updateManagedBannerVisibility();
    327   };
    328 
    329   /**
    330   * Shows the tab contents for the given navigation tab.
    331   * @param {!Element} tab The tab that the user clicked.
    332   */
    333   OptionsPage.showTab = function(tab) {
    334     // Search parents until we find a tab, or the nav bar itself. This allows
    335     // tabs to have child nodes, e.g. labels in separately-styled spans.
    336     while (tab && !tab.classList.contains('subpages-nav-tabs') &&
    337            !tab.classList.contains('tab')) {
    338       tab = tab.parentNode;
    339     }
    340     if (!tab || !tab.classList.contains('tab'))
    341       return;
    342 
    343     if (this.activeNavTab != null) {
    344       this.activeNavTab.classList.remove('active-tab');
    345       $(this.activeNavTab.getAttribute('tab-contents')).classList.
    346           remove('active-tab-contents');
    347     }
    348 
    349     tab.classList.add('active-tab');
    350     $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
    351     this.activeNavTab = tab;
    352   };
    353 
    354   /**
    355    * Registers new options page.
    356    * @param {OptionsPage} page Page to register.
    357    */
    358   OptionsPage.register = function(page) {
    359     this.registeredPages[page.name] = page;
    360     // Create and add new page <li> element to navbar.
    361     var pageNav = document.createElement('li');
    362     pageNav.id = page.name + 'PageNav';
    363     pageNav.className = 'navbar-item';
    364     pageNav.setAttribute('pageName', page.name);
    365     pageNav.textContent = page.pageDiv.querySelector('h1').textContent;
    366     pageNav.tabIndex = 0;
    367     pageNav.onclick = function(event) {
    368       OptionsPage.navigateToPage(this.getAttribute('pageName'));
    369     };
    370     pageNav.onkeypress = function(event) {
    371       // Enter or space
    372       if (event.keyCode == 13 || event.keyCode == 32) {
    373         OptionsPage.navigateToPage(this.getAttribute('pageName'));
    374       }
    375     };
    376     var navbar = $('navbar');
    377     navbar.appendChild(pageNav);
    378     page.tab = pageNav;
    379     page.initializePage();
    380   };
    381 
    382   /**
    383    * Find an enclosing section for an element if it exists.
    384    * @param {Element} element Element to search.
    385    * @return {OptionPage} The section element, or null.
    386    * @private
    387    */
    388   OptionsPage.findSectionForNode_ = function(node) {
    389     while (node = node.parentNode) {
    390       if (node.nodeName == 'SECTION')
    391         return node;
    392     }
    393     return null;
    394   };
    395 
    396   /**
    397    * Registers a new Sub-page.
    398    * @param {OptionsPage} subPage Sub-page to register.
    399    * @param {OptionsPage} parentPage Associated parent page for this page.
    400    * @param {Array} associatedControls Array of control elements that lead to
    401    *     this sub-page. The first item is typically a button in a root-level
    402    *     page. There may be additional buttons for nested sub-pages.
    403    */
    404   OptionsPage.registerSubPage = function(subPage,
    405                                          parentPage,
    406                                          associatedControls) {
    407     this.registeredPages[subPage.name] = subPage;
    408     subPage.parentPage = parentPage;
    409     if (associatedControls) {
    410       subPage.associatedControls = associatedControls;
    411       if (associatedControls.length) {
    412         subPage.associatedSection =
    413             this.findSectionForNode_(associatedControls[0]);
    414       }
    415     }
    416     subPage.tab = undefined;
    417     subPage.initializePage();
    418   };
    419 
    420   /**
    421    * Registers a new Overlay page.
    422    * @param {OptionsPage} overlay Overlay to register.
    423    * @param {OptionsPage} parentPage Associated parent page for this overlay.
    424    * @param {Array} associatedControls Array of control elements associated with
    425    *   this page.
    426    */
    427   OptionsPage.registerOverlay = function(overlay,
    428                                          parentPage,
    429                                          associatedControls) {
    430     this.registeredOverlayPages[overlay.name] = overlay;
    431     overlay.parentPage = parentPage;
    432     if (associatedControls) {
    433       overlay.associatedControls = associatedControls;
    434       if (associatedControls.length) {
    435         overlay.associatedSection =
    436             this.findSectionForNode_(associatedControls[0]);
    437       }
    438     }
    439     overlay.tab = undefined;
    440     overlay.isOverlay = true;
    441     overlay.initializePage();
    442   };
    443 
    444   /**
    445    * Callback for window.onpopstate.
    446    * @param {Object} data State data pushed into history.
    447    */
    448   OptionsPage.setState = function(data) {
    449     if (data && data.pageName) {
    450       // It's possible an overlay may be the last top-level page shown.
    451       if (this.isOverlayVisible_() &&
    452           this.registeredOverlayPages[data.pageName] == undefined) {
    453         this.hideOverlay_();
    454       }
    455 
    456       this.showPageByName(data.pageName, false);
    457     }
    458   };
    459 
    460   /**
    461    * Callback for window.onbeforeunload. Used to notify overlays that they will
    462    * be closed.
    463    */
    464   OptionsPage.willClose = function() {
    465     var overlay = this.getVisibleOverlay_();
    466     if (overlay && overlay.didClosePage)
    467       overlay.didClosePage();
    468   };
    469 
    470   /**
    471    * Freezes/unfreezes the scroll position of given level's page container.
    472    * @param {boolean} freeze Whether the page should be frozen.
    473    * @param {number} level The level to freeze/unfreeze.
    474    * @private
    475    */
    476   OptionsPage.setPageFrozenAtLevel_ = function(freeze, level) {
    477     var container = level == 0 ? $('toplevel-page-container')
    478                                : $('subpage-sheet-container-' + level);
    479 
    480     if (container.classList.contains('frozen') == freeze)
    481       return;
    482 
    483     if (freeze) {
    484       var scrollPosition = document.body.scrollTop;
    485       // Lock the width, since auto width computation may change.
    486       container.style.width = window.getComputedStyle(container).width;
    487       container.classList.add('frozen');
    488       container.style.top = -scrollPosition + 'px';
    489       this.updateFrozenElementHorizontalPosition_(container);
    490     } else {
    491       var scrollPosition = - parseInt(container.style.top, 10);
    492       container.classList.remove('frozen');
    493       container.style.top = '';
    494       container.style.left = '';
    495       container.style.right = '';
    496       container.style.width = '';
    497       // Restore the scroll position.
    498       if (!container.hidden)
    499         window.scroll(document.body.scrollLeft, scrollPosition);
    500     }
    501   };
    502 
    503   /**
    504    * Freezes/unfreezes the scroll position of visible pages based on the current
    505    * page stack.
    506    */
    507   OptionsPage.updatePageFreezeStates = function() {
    508     var topPage = OptionsPage.getTopmostVisiblePage();
    509     if (!topPage)
    510       return;
    511     var nestingLevel = topPage.isOverlay ? 100 : topPage.nestingLevel;
    512     for (var i = 0; i <= SUBPAGE_SHEET_COUNT; i++) {
    513       this.setPageFrozenAtLevel_(i < nestingLevel, i);
    514     }
    515   };
    516 
    517   /**
    518    * Initializes the complete options page.  This will cause all C++ handlers to
    519    * be invoked to do final setup.
    520    */
    521   OptionsPage.initialize = function() {
    522     chrome.send('coreOptionsInitialize');
    523     this.initialized_ = true;
    524 
    525     document.addEventListener('scroll', this.handleScroll_.bind(this));
    526     window.addEventListener('resize', this.handleResize_.bind(this));
    527 
    528     if (!document.documentElement.classList.contains('hide-menu')) {
    529       // Close subpages if the user clicks on the html body. Listen in the
    530       // capturing phase so that we can stop the click from doing anything.
    531       document.body.addEventListener('click',
    532                                      this.bodyMouseEventHandler_.bind(this),
    533                                      true);
    534       // We also need to cancel mousedowns on non-subpage content.
    535       document.body.addEventListener('mousedown',
    536                                      this.bodyMouseEventHandler_.bind(this),
    537                                      true);
    538 
    539       var self = this;
    540       // Hook up the close buttons.
    541       subpageCloseButtons = document.querySelectorAll('.close-subpage');
    542       for (var i = 0; i < subpageCloseButtons.length; i++) {
    543         subpageCloseButtons[i].onclick = function() {
    544           self.closeTopSubPage_();
    545         };
    546       };
    547 
    548       // Install handler for key presses.
    549       document.addEventListener('keydown',
    550                                 this.keyDownEventHandler_.bind(this));
    551 
    552       document.addEventListener('focus', this.manageFocusChange_.bind(this),
    553                                 true);
    554     }
    555 
    556     // Calculate and store the horizontal locations of elements that may be
    557     // frozen later.
    558     var sidebarWidth =
    559         parseInt(window.getComputedStyle($('mainview')).webkitPaddingStart, 10);
    560     $('toplevel-page-container').horizontalOffset = sidebarWidth +
    561         parseInt(window.getComputedStyle(
    562             $('mainview-content')).webkitPaddingStart, 10);
    563     for (var level = 1; level <= SUBPAGE_SHEET_COUNT; level++) {
    564       var containerId = 'subpage-sheet-container-' + level;
    565       $(containerId).horizontalOffset = sidebarWidth;
    566     }
    567     $('subpage-backdrop').horizontalOffset = sidebarWidth;
    568     // Trigger the resize handler manually to set the initial state.
    569     this.handleResize_(null);
    570   };
    571 
    572   /**
    573    * Does a bounds check for the element on the given x, y client coordinates.
    574    * @param {Element} e The DOM element.
    575    * @param {number} x The client X to check.
    576    * @param {number} y The client Y to check.
    577    * @return {boolean} True if the point falls within the element's bounds.
    578    * @private
    579    */
    580   OptionsPage.elementContainsPoint_ = function(e, x, y) {
    581     var clientRect = e.getBoundingClientRect();
    582     return x >= clientRect.left && x <= clientRect.right &&
    583         y >= clientRect.top && y <= clientRect.bottom;
    584   };
    585 
    586   /**
    587    * Called when focus changes; ensures that focus doesn't move outside
    588    * the topmost subpage/overlay.
    589    * @param {Event} e The focus change event.
    590    * @private
    591    */
    592   OptionsPage.manageFocusChange_ = function(e) {
    593     var focusableItemsRoot;
    594     var topPage = this.getTopmostVisiblePage();
    595     if (!topPage)
    596       return;
    597 
    598     if (topPage.isOverlay) {
    599       // If an overlay is visible, that defines the tab loop.
    600       focusableItemsRoot = topPage.pageDiv;
    601     } else {
    602       // If a subpage is visible, use its parent as the tab loop constraint.
    603       // (The parent is used because it contains the close button.)
    604       if (topPage.nestingLevel > 0)
    605         focusableItemsRoot = topPage.pageDiv.parentNode;
    606     }
    607 
    608     if (focusableItemsRoot && !focusableItemsRoot.contains(e.target))
    609       topPage.focusFirstElement();
    610   };
    611 
    612   /**
    613    * Called when the page is scrolled; moves elements that are position:fixed
    614    * but should only behave as if they are fixed for vertical scrolling.
    615    * @param {Event} e The scroll event.
    616    * @private
    617    */
    618   OptionsPage.handleScroll_ = function(e) {
    619     var scrollHorizontalOffset = document.body.scrollLeft;
    620     // position:fixed doesn't seem to work for horizontal scrolling in RTL mode,
    621     // so only adjust in LTR mode (where scroll values will be positive).
    622     if (scrollHorizontalOffset >= 0) {
    623       $('navbar-container').style.left = -scrollHorizontalOffset + 'px';
    624       var subpageBackdrop = $('subpage-backdrop');
    625       subpageBackdrop.style.left = subpageBackdrop.horizontalOffset -
    626           scrollHorizontalOffset + 'px';
    627       this.updateAllFrozenElementPositions_();
    628     }
    629   };
    630 
    631   /**
    632    * Updates all frozen pages to match the horizontal scroll position.
    633    * @private
    634    */
    635   OptionsPage.updateAllFrozenElementPositions_ = function() {
    636     var frozenElements = document.querySelectorAll('.frozen');
    637     for (var i = 0; i < frozenElements.length; i++) {
    638       this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
    639     }
    640   };
    641 
    642   /**
    643    * Updates the given frozen element to match the horizontal scroll position.
    644    * @param {HTMLElement} e The frozen element to update
    645    * @private
    646    */
    647   OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) {
    648     if (document.documentElement.dir == 'rtl')
    649       e.style.right = e.horizontalOffset + 'px';
    650     else
    651       e.style.left = e.horizontalOffset - document.body.scrollLeft + 'px';
    652   };
    653 
    654   /**
    655    * Called when the page is resized; adjusts the size of elements that depend
    656    * on the veiwport.
    657    * @param {Event} e The resize event.
    658    * @private
    659    */
    660   OptionsPage.handleResize_ = function(e) {
    661     // Set an explicit height equal to the viewport on all the subpage
    662     // containers shorter than the viewport. This is used instead of
    663     // min-height: 100% so that there is an explicit height for the subpages'
    664     // min-height: 100%.
    665     var viewportHeight = document.documentElement.clientHeight;
    666     var subpageContainers =
    667         document.querySelectorAll('.subpage-sheet-container');
    668     for (var i = 0; i < subpageContainers.length; i++) {
    669       if (subpageContainers[i].scrollHeight > viewportHeight)
    670         subpageContainers[i].style.removeProperty('height');
    671       else
    672         subpageContainers[i].style.height = viewportHeight + 'px';
    673     }
    674   };
    675 
    676   /**
    677    * A function to handle mouse events (mousedown or click) on the html body by
    678    * closing subpages and/or stopping event propagation.
    679    * @return {Event} a mousedown or click event.
    680    * @private
    681    */
    682   OptionsPage.bodyMouseEventHandler_ = function(event) {
    683     // Do nothing if a subpage isn't showing.
    684     var topPage = this.getTopmostVisiblePage();
    685     if (!topPage || topPage.isOverlay || !topPage.parentPage)
    686       return;
    687 
    688     // Do nothing if the client coordinates are not within the source element.
    689     // This situation is indicative of a Webkit bug where clicking on a
    690     // radio/checkbox label span will generate an event with client coordinates
    691     // of (-scrollX, -scrollY).
    692     // See https://bugs.webkit.org/show_bug.cgi?id=56606
    693     if (event.clientX == -document.body.scrollLeft &&
    694         event.clientY == -document.body.scrollTop) {
    695       return;
    696     }
    697 
    698     // Don't interfere with navbar clicks.
    699     if ($('navbar').contains(event.target))
    700       return;
    701 
    702     // Figure out which page the click happened in.
    703     for (var level = topPage.nestingLevel; level >= 0; level--) {
    704       var clickIsWithinLevel = level == 0 ? true :
    705           OptionsPage.elementContainsPoint_(
    706               $('subpage-sheet-' + level), event.clientX, event.clientY);
    707 
    708       if (!clickIsWithinLevel)
    709         continue;
    710 
    711       // Event was within the topmost page; do nothing.
    712       if (topPage.nestingLevel == level)
    713         return;
    714 
    715       // Block propgation of both clicks and mousedowns, but only close subpages
    716       // on click.
    717       if (event.type == 'click')
    718         this.closeSubPagesToLevel(level);
    719       event.stopPropagation();
    720       event.preventDefault();
    721       return;
    722     }
    723   };
    724 
    725   /**
    726    * A function to handle key press events.
    727    * @return {Event} a keydown event.
    728    * @private
    729    */
    730   OptionsPage.keyDownEventHandler_ = function(event) {
    731     // Close the top overlay or sub-page on esc.
    732     if (event.keyCode == 27) {  // Esc
    733       if (this.isOverlayVisible_()) {
    734         if (this.shouldCloseOverlay_())
    735           this.closeOverlay();
    736       } else {
    737         this.closeTopSubPage_();
    738       }
    739     }
    740   };
    741 
    742   OptionsPage.setClearPluginLSODataEnabled = function(enabled) {
    743     if (enabled) {
    744       document.documentElement.setAttribute(
    745           'flashPluginSupportsClearSiteData', '');
    746     } else {
    747       document.documentElement.removeAttribute(
    748           'flashPluginSupportsClearSiteData');
    749     }
    750   };
    751 
    752   /**
    753    * Re-initializes the C++ handlers if necessary. This is called if the
    754    * handlers are torn down and recreated but the DOM may not have been (in
    755    * which case |initialize| won't be called again). If |initialize| hasn't been
    756    * called, this does nothing (since it will be later, once the DOM has
    757    * finished loading).
    758    */
    759   OptionsPage.reinitializeCore = function() {
    760     if (this.initialized_)
    761       chrome.send('coreOptionsInitialize');
    762   }
    763 
    764   OptionsPage.prototype = {
    765     __proto__: cr.EventTarget.prototype,
    766 
    767     /**
    768      * The parent page of this option page, or null for top-level pages.
    769      * @type {OptionsPage}
    770      */
    771     parentPage: null,
    772 
    773     /**
    774      * The section on the parent page that is associated with this page.
    775      * Can be null.
    776      * @type {Element}
    777      */
    778     associatedSection: null,
    779 
    780     /**
    781      * An array of controls that are associated with this page.  The first
    782      * control should be located on a top-level page.
    783      * @type {OptionsPage}
    784      */
    785     associatedControls: null,
    786 
    787     /**
    788      * Initializes page content.
    789      */
    790     initializePage: function() {},
    791 
    792     /**
    793      * Sets managed banner visibility state.
    794      */
    795     setManagedBannerVisibility: function(visible) {
    796       this.managed = visible;
    797       if (this.visible) {
    798         this.updateManagedBannerVisibility();
    799       }
    800     },
    801 
    802     /**
    803      * Updates managed banner visibility state. This function iterates over
    804      * all input fields of a window and if any of these is marked as managed
    805      * it triggers the managed banner to be visible. The banner can be enforced
    806      * being on through the managed flag of this class but it can not be forced
    807      * being off if managed items exist.
    808      */
    809     updateManagedBannerVisibility: function() {
    810       var bannerDiv = $('managed-prefs-banner');
    811 
    812       var hasManaged = this.managed;
    813       if (!hasManaged) {
    814         var inputElements = this.pageDiv.querySelectorAll('input');
    815         for (var i = 0, len = inputElements.length; i < len; i++) {
    816           if (inputElements[i].managed) {
    817             hasManaged = true;
    818             break;
    819           }
    820         }
    821       }
    822       if (hasManaged) {
    823         bannerDiv.hidden = false;
    824         var height = window.getComputedStyle($('managed-prefs-banner')).height;
    825         $('subpage-backdrop').style.top = height;
    826       } else {
    827         bannerDiv.hidden = true;
    828         $('subpage-backdrop').style.top = '0';
    829       }
    830     },
    831 
    832     /**
    833      * Gets page visibility state.
    834      */
    835     get visible() {
    836       var page = $(this.pageDivName);
    837       return page && page.ownerDocument.defaultView.getComputedStyle(
    838           page).display == 'block';
    839     },
    840 
    841     /**
    842      * Sets page visibility.
    843      */
    844     set visible(visible) {
    845       if ((this.visible && visible) || (!this.visible && !visible))
    846         return;
    847 
    848       this.setContainerVisibility_(visible);
    849       if (visible) {
    850         this.pageDiv.classList.remove('hidden');
    851 
    852         if (this.tab)
    853           this.tab.classList.add('navbar-item-selected');
    854       } else {
    855         this.pageDiv.classList.add('hidden');
    856 
    857         if (this.tab)
    858           this.tab.classList.remove('navbar-item-selected');
    859       }
    860 
    861       OptionsPage.updatePageFreezeStates();
    862 
    863       // A subpage was shown or hidden.
    864       if (!this.isOverlay && this.nestingLevel > 0) {
    865         OptionsPage.updateSubpageBackdrop_();
    866         if (visible) {
    867           // Scroll to the top of the newly-opened subpage.
    868           window.scroll(document.body.scrollLeft, 0)
    869         }
    870       }
    871 
    872       // The managed prefs banner is global, so after any visibility change
    873       // update it based on the topmost page, not necessarily this page
    874       // (e.g., if an ancestor is made visible after a child).
    875       OptionsPage.updateManagedBannerVisibility();
    876 
    877       cr.dispatchPropertyChange(this, 'visible', visible, !visible);
    878     },
    879 
    880     /**
    881      * Shows or hides this page's container.
    882      * @param {boolean} visible Whether the container should be visible or not.
    883      * @private
    884      */
    885     setContainerVisibility_: function(visible) {
    886       var container = null;
    887       if (this.isOverlay) {
    888         container = $('overlay');
    889       } else {
    890         var nestingLevel = this.nestingLevel;
    891         if (nestingLevel > 0)
    892           container = $('subpage-sheet-container-' + nestingLevel);
    893       }
    894       var isSubpage = !this.isOverlay;
    895 
    896       if (!container || container.hidden != visible)
    897         return;
    898 
    899       if (visible) {
    900         container.hidden = false;
    901         if (isSubpage) {
    902           var computedStyle = window.getComputedStyle(container);
    903           container.style.WebkitPaddingStart =
    904               parseInt(computedStyle.WebkitPaddingStart, 10) + 100 + 'px';
    905         }
    906         // Separate animating changes from the removal of display:none.
    907         window.setTimeout(function() {
    908           container.classList.remove('transparent');
    909           if (isSubpage)
    910             container.style.WebkitPaddingStart = '';
    911         });
    912       } else {
    913         var self = this;
    914         container.addEventListener('webkitTransitionEnd', function f(e) {
    915           if (e.propertyName != 'opacity')
    916             return;
    917           container.removeEventListener('webkitTransitionEnd', f);
    918           self.fadeCompleted_(container);
    919         });
    920         container.classList.add('transparent');
    921       }
    922     },
    923 
    924     /**
    925      * Called when a container opacity transition finishes.
    926      * @param {HTMLElement} container The container element.
    927      * @private
    928      */
    929     fadeCompleted_: function(container) {
    930       if (container.classList.contains('transparent'))
    931         container.hidden = true;
    932     },
    933 
    934     /**
    935      * Focuses the first control on the page.
    936      */
    937     focusFirstElement: function() {
    938       // Sets focus on the first interactive element in the page.
    939       var focusElement =
    940           this.pageDiv.querySelector('button, input, list, select');
    941       if (focusElement)
    942         focusElement.focus();
    943     },
    944 
    945     /**
    946      * The nesting level of this page.
    947      * @type {number} The nesting level of this page (0 for top-level page)
    948      */
    949     get nestingLevel() {
    950       var level = 0;
    951       var parent = this.parentPage;
    952       while (parent) {
    953         level++;
    954         parent = parent.parentPage;
    955       }
    956       return level;
    957     },
    958 
    959     /**
    960      * Whether the page is considered 'sticky', such that it will
    961      * remain a top-level page even if sub-pages change.
    962      * @type {boolean} True if this page is sticky.
    963      */
    964     get sticky() {
    965       return false;
    966     },
    967 
    968     /**
    969      * Checks whether this page is an ancestor of the given page in terms of
    970      * subpage nesting.
    971      * @param {OptionsPage} page
    972      * @return {boolean} True if this page is nested under |page|
    973      */
    974     isAncestorOfPage: function(page) {
    975       var parent = page.parentPage;
    976       while (parent) {
    977         if (parent == this)
    978           return true;
    979         parent = parent.parentPage;
    980       }
    981       return false;
    982     },
    983 
    984     /**
    985      * Whether it should be possible to show the page.
    986      * @return {boolean} True if the page should be shown
    987      */
    988     canShowPage: function() {
    989       return true;
    990     },
    991 
    992     /**
    993      * Whether an overlay should be closed. Used by overlay implementation to
    994      * handle special closing behaviors.
    995      * @return {boolean} True if the overlay should be closed.
    996      */
    997     shouldClose: function() {
    998       return true;
    999     },
   1000   };
   1001 
   1002   // Export
   1003   return {
   1004     OptionsPage: OptionsPage
   1005   };
   1006 });
   1007