Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2012 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   /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
      7 
      8   /////////////////////////////////////////////////////////////////////////////
      9   // OptionsPage class:
     10 
     11   /**
     12    * Base class for options page.
     13    * @constructor
     14    * @param {string} name Options page name.
     15    * @param {string} title Options page title, used for history.
     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     // |pageDiv.page| is set to the page object (this) when the page is visible
     24     // to track which page is being shown when multiple pages can share the same
     25     // underlying div.
     26     this.pageDiv.page = null;
     27     this.tab = null;
     28     this.lastFocusedElement = null;
     29   }
     30 
     31   /**
     32    * This is the absolute difference maintained between standard and
     33    * fixed-width font sizes. Refer http://crbug.com/91922.
     34    * @const
     35    */
     36   OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3;
     37 
     38   /**
     39    * Offset of page container in pixels, to allow room for side menu.
     40    * Simplified settings pages can override this if they don't use the menu.
     41    * The default (155) comes from -webkit-margin-start in uber_shared.css
     42    * @private
     43    */
     44   OptionsPage.horizontalOffset = 155;
     45 
     46   /**
     47    * Main level option pages. Maps lower-case page names to the respective page
     48    * object.
     49    * @protected
     50    */
     51   OptionsPage.registeredPages = {};
     52 
     53   /**
     54    * Pages which are meant to behave like modal dialogs. Maps lower-case overlay
     55    * names to the respective overlay object.
     56    * @protected
     57    */
     58   OptionsPage.registeredOverlayPages = {};
     59 
     60   /**
     61    * True if options page is served from a dialog.
     62    */
     63   OptionsPage.isDialog = false;
     64 
     65   /**
     66    * Gets the default page (to be shown on initial load).
     67    */
     68   OptionsPage.getDefaultPage = function() {
     69     return BrowserOptions.getInstance();
     70   };
     71 
     72   /**
     73    * Shows the default page.
     74    */
     75   OptionsPage.showDefaultPage = function() {
     76     this.navigateToPage(this.getDefaultPage().name);
     77   };
     78 
     79   /**
     80    * "Navigates" to a page, meaning that the page will be shown and the
     81    * appropriate entry is placed in the history.
     82    * @param {string} pageName Page name.
     83    */
     84   OptionsPage.navigateToPage = function(pageName) {
     85     this.showPageByName(pageName, true);
     86   };
     87 
     88   /**
     89    * Shows a registered page. This handles both top-level and overlay pages.
     90    * @param {string} pageName Page name.
     91    * @param {boolean} updateHistory True if we should update the history after
     92    *     showing the page.
     93    * @param {Object=} opt_propertyBag An optional bag of properties including
     94    *     replaceState (if history state should be replaced instead of pushed).
     95    * @private
     96    */
     97   OptionsPage.showPageByName = function(pageName,
     98                                         updateHistory,
     99                                         opt_propertyBag) {
    100     // If |opt_propertyBag| is non-truthy, homogenize to object.
    101     opt_propertyBag = opt_propertyBag || {};
    102 
    103     // If a bubble is currently being shown, hide it.
    104     this.hideBubble();
    105 
    106     // Find the currently visible root-level page.
    107     var rootPage = null;
    108     for (var name in this.registeredPages) {
    109       var page = this.registeredPages[name];
    110       if (page.visible && !page.parentPage) {
    111         rootPage = page;
    112         break;
    113       }
    114     }
    115 
    116     // Find the target page.
    117     var targetPage = this.registeredPages[pageName.toLowerCase()];
    118     if (!targetPage || !targetPage.canShowPage()) {
    119       // If it's not a page, try it as an overlay.
    120       if (!targetPage && this.showOverlay_(pageName, rootPage)) {
    121         if (updateHistory)
    122           this.updateHistoryState_(!!opt_propertyBag.replaceState);
    123         this.updateTitle_();
    124         return;
    125       } else {
    126         targetPage = this.getDefaultPage();
    127       }
    128     }
    129 
    130     pageName = targetPage.name.toLowerCase();
    131     var targetPageWasVisible = targetPage.visible;
    132 
    133     // Determine if the root page is 'sticky', meaning that it
    134     // shouldn't change when showing an overlay. This can happen for special
    135     // pages like Search.
    136     var isRootPageLocked =
    137         rootPage && rootPage.sticky && targetPage.parentPage;
    138 
    139     var allPageNames = Array.prototype.concat.call(
    140         Object.keys(this.registeredPages),
    141         Object.keys(this.registeredOverlayPages));
    142 
    143     // Notify pages if they will be hidden.
    144     for (var i = 0; i < allPageNames.length; ++i) {
    145       var name = allPageNames[i];
    146       var page = this.registeredPages[name] ||
    147                  this.registeredOverlayPages[name];
    148       if (!page.parentPage && isRootPageLocked)
    149         continue;
    150       if (page.willHidePage && name != pageName &&
    151           !page.isAncestorOfPage(targetPage)) {
    152         page.willHidePage();
    153       }
    154     }
    155 
    156     // Update visibilities to show only the hierarchy of the target page.
    157     for (var i = 0; i < allPageNames.length; ++i) {
    158       var name = allPageNames[i];
    159       var page = this.registeredPages[name] ||
    160                  this.registeredOverlayPages[name];
    161       if (!page.parentPage && isRootPageLocked)
    162         continue;
    163       page.visible = name == pageName || page.isAncestorOfPage(targetPage);
    164     }
    165 
    166     // Update the history and current location.
    167     if (updateHistory)
    168       this.updateHistoryState_(!!opt_propertyBag.replaceState);
    169 
    170     // Update focus if any other control was focused on the previous page,
    171     // or the previous page is not known.
    172     if (document.activeElement != document.body &&
    173         (!rootPage || rootPage.pageDiv.contains(document.activeElement))) {
    174       targetPage.focus();
    175     }
    176 
    177     // Notify pages if they were shown.
    178     for (var i = 0; i < allPageNames.length; ++i) {
    179       var name = allPageNames[i];
    180       var page = this.registeredPages[name] ||
    181                  this.registeredOverlayPages[name];
    182       if (!page.parentPage && isRootPageLocked)
    183         continue;
    184       if (!targetPageWasVisible && page.didShowPage &&
    185           (name == pageName || page.isAncestorOfPage(targetPage))) {
    186         page.didShowPage();
    187       }
    188     }
    189 
    190     // Update the document title. Do this after didShowPage was called, in case
    191     // a page decides to change its title.
    192     this.updateTitle_();
    193   };
    194 
    195   /**
    196    * Scrolls the page to the correct position (the top when opening an overlay,
    197    * or the old scroll position a previously hidden overlay becomes visible).
    198    * @private
    199    */
    200   OptionsPage.updateScrollPosition_ = function() {
    201     var container = $('page-container');
    202     var scrollTop = container.oldScrollTop || 0;
    203     container.oldScrollTop = undefined;
    204     window.scroll(scrollLeftForDocument(document), scrollTop);
    205   };
    206 
    207   /**
    208    * Updates the title to title of the current page.
    209    * @private
    210    */
    211   OptionsPage.updateTitle_ = function() {
    212     var page = this.getTopmostVisiblePage();
    213     uber.setTitle(page.title);
    214   };
    215 
    216   /**
    217    * Pushes the current page onto the history stack, replacing the current entry
    218    * if appropriate.
    219    * @param {boolean} replace If true, allow no history events to be created.
    220    * @param {object=} opt_params A bag of optional params, including:
    221    *     {boolean} ignoreHash Whether to include the hash or not.
    222    * @private
    223    */
    224   OptionsPage.updateHistoryState_ = function(replace, opt_params) {
    225     if (OptionsPage.isDialog)
    226       return;
    227 
    228     var page = this.getTopmostVisiblePage();
    229     var path = window.location.pathname + window.location.hash;
    230     if (path)
    231       path = path.slice(1).replace(/\/(?:#|$)/, '');  // Remove trailing slash.
    232 
    233     // If the page is already in history (the user may have clicked the same
    234     // link twice, or this is the initial load), do nothing.
    235     var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash;
    236     var newPath = (page == this.getDefaultPage() ? '' : page.name) + hash;
    237     if (path == newPath)
    238       return;
    239 
    240     var historyFunction = replace ? uber.replaceState : uber.pushState;
    241     historyFunction.call(uber, {}, newPath);
    242   };
    243 
    244   /**
    245    * Shows a registered Overlay page. Does not update history.
    246    * @param {string} overlayName Page name.
    247    * @param {OptionPage} rootPage The currently visible root-level page.
    248    * @return {boolean} whether we showed an overlay.
    249    */
    250   OptionsPage.showOverlay_ = function(overlayName, rootPage) {
    251     var overlay = this.registeredOverlayPages[overlayName.toLowerCase()];
    252     if (!overlay || !overlay.canShowPage())
    253       return false;
    254 
    255     // Save the currently focused element in the page for restoration later.
    256     var currentPage = this.getTopmostVisiblePage();
    257     if (currentPage)
    258       currentPage.lastFocusedElement = document.activeElement;
    259 
    260     if ((!rootPage || !rootPage.sticky) &&
    261         overlay.parentPage &&
    262         !overlay.parentPage.visible) {
    263       this.showPageByName(overlay.parentPage.name, false);
    264     }
    265 
    266     if (!overlay.visible) {
    267       overlay.visible = true;
    268       if (overlay.didShowPage) overlay.didShowPage();
    269     }
    270 
    271     // Change focus to the overlay if any other control was focused by keyboard
    272     // before. Otherwise, no one should have focus.
    273     if (document.activeElement != document.body) {
    274       if (FocusOutlineManager.forDocument(document).visible) {
    275         overlay.focus();
    276       } else if (!overlay.pageDiv.contains(document.activeElement)) {
    277         document.activeElement.blur();
    278       }
    279     }
    280 
    281     if ($('search-field') && $('search-field').value == '') {
    282       var section = overlay.associatedSection;
    283       if (section)
    284         options.BrowserOptions.scrollToSection(section);
    285     }
    286 
    287     return true;
    288   };
    289 
    290   /**
    291    * Returns whether or not an overlay is visible.
    292    * @return {boolean} True if an overlay is visible.
    293    * @private
    294    */
    295   OptionsPage.isOverlayVisible_ = function() {
    296     return this.getVisibleOverlay_() != null;
    297   };
    298 
    299   /**
    300    * Returns the currently visible overlay, or null if no page is visible.
    301    * @return {OptionPage} The visible overlay.
    302    */
    303   OptionsPage.getVisibleOverlay_ = function() {
    304     var topmostPage = null;
    305     for (var name in this.registeredOverlayPages) {
    306       var page = this.registeredOverlayPages[name];
    307       if (page.visible &&
    308           (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) {
    309         topmostPage = page;
    310       }
    311     }
    312     return topmostPage;
    313   };
    314 
    315   /**
    316    * Restores the last focused element on a given page.
    317    */
    318   OptionsPage.restoreLastFocusedElement_ = function() {
    319     var currentPage = this.getTopmostVisiblePage();
    320     if (currentPage.lastFocusedElement)
    321       currentPage.lastFocusedElement.focus();
    322   };
    323 
    324   /**
    325    * Closes the visible overlay. Updates the history state after closing the
    326    * overlay.
    327    */
    328   OptionsPage.closeOverlay = function() {
    329     var overlay = this.getVisibleOverlay_();
    330     if (!overlay)
    331       return;
    332 
    333     overlay.visible = false;
    334 
    335     if (overlay.didClosePage) overlay.didClosePage();
    336     this.updateHistoryState_(false, {ignoreHash: true});
    337     this.updateTitle_();
    338 
    339     this.restoreLastFocusedElement_();
    340   };
    341 
    342   /**
    343    * Closes all overlays and updates the history after each closed overlay.
    344    */
    345   OptionsPage.closeAllOverlays = function() {
    346     while (this.isOverlayVisible_()) {
    347       this.closeOverlay();
    348     }
    349   };
    350 
    351   /**
    352    * Cancels (closes) the overlay, due to the user pressing <Esc>.
    353    */
    354   OptionsPage.cancelOverlay = function() {
    355     // Blur the active element to ensure any changed pref value is saved.
    356     document.activeElement.blur();
    357     var overlay = this.getVisibleOverlay_();
    358     // Let the overlay handle the <Esc> if it wants to.
    359     if (overlay.handleCancel) {
    360       overlay.handleCancel();
    361       this.restoreLastFocusedElement_();
    362     } else {
    363       this.closeOverlay();
    364     }
    365   };
    366 
    367   /**
    368    * Hides the visible overlay. Does not affect the history state.
    369    * @private
    370    */
    371   OptionsPage.hideOverlay_ = function() {
    372     var overlay = this.getVisibleOverlay_();
    373     if (overlay)
    374       overlay.visible = false;
    375   };
    376 
    377   /**
    378    * Returns the pages which are currently visible, ordered by nesting level
    379    * (ascending).
    380    * @return {Array.OptionPage} The pages which are currently visible, ordered
    381    * by nesting level (ascending).
    382    */
    383   OptionsPage.getVisiblePages_ = function() {
    384     var visiblePages = [];
    385     for (var name in this.registeredPages) {
    386       var page = this.registeredPages[name];
    387       if (page.visible)
    388         visiblePages[page.nestingLevel] = page;
    389     }
    390     return visiblePages;
    391   };
    392 
    393   /**
    394    * Returns the topmost visible page (overlays excluded).
    395    * @return {OptionPage} The topmost visible page aside any overlay.
    396    * @private
    397    */
    398   OptionsPage.getTopmostVisibleNonOverlayPage_ = function() {
    399     var topPage = null;
    400     for (var name in this.registeredPages) {
    401       var page = this.registeredPages[name];
    402       if (page.visible &&
    403           (!topPage || page.nestingLevel > topPage.nestingLevel))
    404         topPage = page;
    405     }
    406 
    407     return topPage;
    408   };
    409 
    410   /**
    411    * Returns the topmost visible page, or null if no page is visible.
    412    * @return {OptionPage} The topmost visible page.
    413    */
    414   OptionsPage.getTopmostVisiblePage = function() {
    415     // Check overlays first since they're top-most if visible.
    416     return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_();
    417   };
    418 
    419   /**
    420    * Returns the currently visible bubble, or null if no bubble is visible.
    421    * @return {AutoCloseBubble} The bubble currently being shown.
    422    */
    423   OptionsPage.getVisibleBubble = function() {
    424     var bubble = OptionsPage.bubble_;
    425     return bubble && !bubble.hidden ? bubble : null;
    426   };
    427 
    428   /**
    429    * Shows an informational bubble displaying |content| and pointing at the
    430    * |target| element. If |content| has focusable elements, they join the
    431    * current page's tab order as siblings of |domSibling|.
    432    * @param {HTMLDivElement} content The content of the bubble.
    433    * @param {HTMLElement} target The element at which the bubble points.
    434    * @param {HTMLElement} domSibling The element after which the bubble is added
    435    *                      to the DOM.
    436    * @param {cr.ui.ArrowLocation} location The arrow location.
    437    */
    438   OptionsPage.showBubble = function(content, target, domSibling, location) {
    439     OptionsPage.hideBubble();
    440 
    441     var bubble = new cr.ui.AutoCloseBubble;
    442     bubble.anchorNode = target;
    443     bubble.domSibling = domSibling;
    444     bubble.arrowLocation = location;
    445     bubble.content = content;
    446     bubble.show();
    447     OptionsPage.bubble_ = bubble;
    448   };
    449 
    450   /**
    451    * Hides the currently visible bubble, if any.
    452    */
    453   OptionsPage.hideBubble = function() {
    454     if (OptionsPage.bubble_)
    455       OptionsPage.bubble_.hide();
    456   };
    457 
    458   /**
    459    * Shows the tab contents for the given navigation tab.
    460    * @param {!Element} tab The tab that the user clicked.
    461    */
    462   OptionsPage.showTab = function(tab) {
    463     // Search parents until we find a tab, or the nav bar itself. This allows
    464     // tabs to have child nodes, e.g. labels in separately-styled spans.
    465     while (tab && !tab.classList.contains('subpages-nav-tabs') &&
    466            !tab.classList.contains('tab')) {
    467       tab = tab.parentNode;
    468     }
    469     if (!tab || !tab.classList.contains('tab'))
    470       return;
    471 
    472     // Find tab bar of the tab.
    473     var tabBar = tab;
    474     while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) {
    475       tabBar = tabBar.parentNode;
    476     }
    477     if (!tabBar)
    478       return;
    479 
    480     if (tabBar.activeNavTab != null) {
    481       tabBar.activeNavTab.classList.remove('active-tab');
    482       $(tabBar.activeNavTab.getAttribute('tab-contents')).classList.
    483           remove('active-tab-contents');
    484     }
    485 
    486     tab.classList.add('active-tab');
    487     $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
    488     tabBar.activeNavTab = tab;
    489   };
    490 
    491   /**
    492    * Registers new options page.
    493    * @param {OptionsPage} page Page to register.
    494    */
    495   OptionsPage.register = function(page) {
    496     this.registeredPages[page.name.toLowerCase()] = page;
    497     page.initializePage();
    498   };
    499 
    500   /**
    501    * Find an enclosing section for an element if it exists.
    502    * @param {Element} element Element to search.
    503    * @return {OptionPage} The section element, or null.
    504    * @private
    505    */
    506   OptionsPage.findSectionForNode_ = function(node) {
    507     while (node = node.parentNode) {
    508       if (node.nodeName == 'SECTION')
    509         return node;
    510     }
    511     return null;
    512   };
    513 
    514   /**
    515    * Registers a new Overlay page.
    516    * @param {OptionsPage} overlay Overlay to register.
    517    * @param {OptionsPage} parentPage Associated parent page for this overlay.
    518    * @param {Array} associatedControls Array of control elements associated with
    519    *   this page.
    520    */
    521   OptionsPage.registerOverlay = function(overlay,
    522                                          parentPage,
    523                                          associatedControls) {
    524     this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay;
    525     overlay.parentPage = parentPage;
    526     if (associatedControls) {
    527       overlay.associatedControls = associatedControls;
    528       if (associatedControls.length) {
    529         overlay.associatedSection =
    530             this.findSectionForNode_(associatedControls[0]);
    531       }
    532 
    533       // Sanity check.
    534       for (var i = 0; i < associatedControls.length; ++i) {
    535         assert(associatedControls[i], 'Invalid element passed.');
    536       }
    537     }
    538 
    539     // Reverse the button strip for Windows and CrOS. See the documentation of
    540     // reverseButtonStripIfNecessary_() for an explanation of why this is done.
    541     if (cr.isWindows || cr.isChromeOS)
    542       this.reverseButtonStripIfNecessary_(overlay);
    543 
    544     overlay.tab = undefined;
    545     overlay.isOverlay = true;
    546     overlay.initializePage();
    547   };
    548 
    549   /**
    550    * Reverses the child elements of a button strip if it hasn't already been
    551    * reversed. This is necessary because WebKit does not alter the tab order for
    552    * elements that are visually reversed using -webkit-box-direction: reverse,
    553    * and the button order is reversed for views. See http://webk.it/62664 for
    554    * more information.
    555    * @param {Object} overlay The overlay containing the button strip to reverse.
    556    * @private
    557    */
    558   OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) {
    559     var buttonStrips =
    560         overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])');
    561 
    562     // Reverse all button-strips in the overlay.
    563     for (var j = 0; j < buttonStrips.length; j++) {
    564       var buttonStrip = buttonStrips[j];
    565 
    566       var childNodes = buttonStrip.childNodes;
    567       for (var i = childNodes.length - 1; i >= 0; i--)
    568         buttonStrip.appendChild(childNodes[i]);
    569 
    570       buttonStrip.setAttribute('reversed', '');
    571     }
    572   };
    573 
    574   /**
    575    * Returns the name of the page from the current path.
    576    */
    577   OptionsPage.getPageNameFromPath = function() {
    578     var path = location.pathname;
    579     if (path.length <= 1)
    580       return this.getDefaultPage().name;
    581 
    582     // Skip starting slash and remove trailing slash (if any).
    583     return path.slice(1).replace(/\/$/, '');
    584   };
    585 
    586   /**
    587    * Callback for window.onpopstate to handle back/forward navigations.
    588    * @param {string} pageName The current page name.
    589    * @param {Object} data State data pushed into history.
    590    */
    591   OptionsPage.setState = function(pageName, data) {
    592     var currentOverlay = this.getVisibleOverlay_();
    593     var lowercaseName = pageName.toLowerCase();
    594     var newPage = this.registeredPages[lowercaseName] ||
    595                   this.registeredOverlayPages[lowercaseName] ||
    596                   this.getDefaultPage();
    597     if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) {
    598       currentOverlay.visible = false;
    599       if (currentOverlay.didClosePage) currentOverlay.didClosePage();
    600     }
    601     this.showPageByName(pageName, false);
    602   };
    603 
    604   /**
    605    * Callback for window.onbeforeunload. Used to notify overlays that they will
    606    * be closed.
    607    */
    608   OptionsPage.willClose = function() {
    609     var overlay = this.getVisibleOverlay_();
    610     if (overlay && overlay.didClosePage)
    611       overlay.didClosePage();
    612   };
    613 
    614   /**
    615    * Freezes/unfreezes the scroll position of the root page container.
    616    * @param {boolean} freeze Whether the page should be frozen.
    617    * @private
    618    */
    619   OptionsPage.setRootPageFrozen_ = function(freeze) {
    620     var container = $('page-container');
    621     if (container.classList.contains('frozen') == freeze)
    622       return;
    623 
    624     if (freeze) {
    625       // Lock the width, since auto width computation may change.
    626       container.style.width = window.getComputedStyle(container).width;
    627       container.oldScrollTop = scrollTopForDocument(document);
    628       container.classList.add('frozen');
    629       var verticalPosition =
    630           container.getBoundingClientRect().top - container.oldScrollTop;
    631       container.style.top = verticalPosition + 'px';
    632       this.updateFrozenElementHorizontalPosition_(container);
    633     } else {
    634       container.classList.remove('frozen');
    635       container.style.top = '';
    636       container.style.left = '';
    637       container.style.right = '';
    638       container.style.width = '';
    639     }
    640   };
    641 
    642   /**
    643    * Freezes/unfreezes the scroll position of the root page based on the current
    644    * page stack.
    645    */
    646   OptionsPage.updateRootPageFreezeState = function() {
    647     var topPage = OptionsPage.getTopmostVisiblePage();
    648     if (topPage)
    649       this.setRootPageFrozen_(topPage.isOverlay);
    650   };
    651 
    652   /**
    653    * Initializes the complete options page.  This will cause all C++ handlers to
    654    * be invoked to do final setup.
    655    */
    656   OptionsPage.initialize = function() {
    657     chrome.send('coreOptionsInitialize');
    658     uber.onContentFrameLoaded();
    659     FocusOutlineManager.forDocument(document);
    660     document.addEventListener('scroll', this.handleScroll_.bind(this));
    661 
    662     // Trigger the scroll handler manually to set the initial state.
    663     this.handleScroll_();
    664 
    665     // Shake the dialog if the user clicks outside the dialog bounds.
    666     var containers = [$('overlay-container-1'), $('overlay-container-2')];
    667     for (var i = 0; i < containers.length; i++) {
    668       var overlay = containers[i];
    669       cr.ui.overlay.setupOverlay(overlay);
    670       overlay.addEventListener('cancelOverlay',
    671                                OptionsPage.cancelOverlay.bind(OptionsPage));
    672     }
    673 
    674     cr.ui.overlay.globalInitialization();
    675   };
    676 
    677   /**
    678    * Does a bounds check for the element on the given x, y client coordinates.
    679    * @param {Element} e The DOM element.
    680    * @param {number} x The client X to check.
    681    * @param {number} y The client Y to check.
    682    * @return {boolean} True if the point falls within the element's bounds.
    683    * @private
    684    */
    685   OptionsPage.elementContainsPoint_ = function(e, x, y) {
    686     var clientRect = e.getBoundingClientRect();
    687     return x >= clientRect.left && x <= clientRect.right &&
    688         y >= clientRect.top && y <= clientRect.bottom;
    689   };
    690 
    691   /**
    692    * Called when the page is scrolled; moves elements that are position:fixed
    693    * but should only behave as if they are fixed for vertical scrolling.
    694    * @private
    695    */
    696   OptionsPage.handleScroll_ = function() {
    697     this.updateAllFrozenElementPositions_();
    698   };
    699 
    700   /**
    701    * Updates all frozen pages to match the horizontal scroll position.
    702    * @private
    703    */
    704   OptionsPage.updateAllFrozenElementPositions_ = function() {
    705     var frozenElements = document.querySelectorAll('.frozen');
    706     for (var i = 0; i < frozenElements.length; i++)
    707       this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
    708   };
    709 
    710   /**
    711    * Updates the given frozen element to match the horizontal scroll position.
    712    * @param {HTMLElement} e The frozen element to update.
    713    * @private
    714    */
    715   OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) {
    716     if (isRTL()) {
    717       e.style.right = OptionsPage.horizontalOffset + 'px';
    718     } else {
    719       var scrollLeft = scrollLeftForDocument(document);
    720       e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px';
    721     }
    722   };
    723 
    724   /**
    725    * Change the horizontal offset used to reposition elements while showing an
    726    * overlay from the default.
    727    */
    728   OptionsPage.setHorizontalOffset = function(value) {
    729     OptionsPage.horizontalOffset = value;
    730   };
    731 
    732   OptionsPage.setClearPluginLSODataEnabled = function(enabled) {
    733     if (enabled) {
    734       document.documentElement.setAttribute(
    735           'flashPluginSupportsClearSiteData', '');
    736     } else {
    737       document.documentElement.removeAttribute(
    738           'flashPluginSupportsClearSiteData');
    739     }
    740     if (navigator.plugins['Shockwave Flash'])
    741       document.documentElement.setAttribute('hasFlashPlugin', '');
    742   };
    743 
    744   OptionsPage.setPepperFlashSettingsEnabled = function(enabled) {
    745     if (enabled) {
    746       document.documentElement.setAttribute(
    747           'enablePepperFlashSettings', '');
    748     } else {
    749       document.documentElement.removeAttribute(
    750           'enablePepperFlashSettings');
    751     }
    752   };
    753 
    754   OptionsPage.setIsSettingsApp = function() {
    755     document.documentElement.classList.add('settings-app');
    756   };
    757 
    758   OptionsPage.isSettingsApp = function() {
    759     return document.documentElement.classList.contains('settings-app');
    760   };
    761 
    762   /**
    763    * Whether the page is still loading (i.e. onload hasn't finished running).
    764    * @return {boolean} Whether the page is still loading.
    765    */
    766   OptionsPage.isLoading = function() {
    767     return document.documentElement.classList.contains('loading');
    768   };
    769 
    770   OptionsPage.prototype = {
    771     __proto__: cr.EventTarget.prototype,
    772 
    773     /**
    774      * The parent page of this option page, or null for top-level pages.
    775      * @type {OptionsPage}
    776      */
    777     parentPage: null,
    778 
    779     /**
    780      * The section on the parent page that is associated with this page.
    781      * Can be null.
    782      * @type {Element}
    783      */
    784     associatedSection: null,
    785 
    786     /**
    787      * An array of controls that are associated with this page.  The first
    788      * control should be located on a top-level page.
    789      * @type {OptionsPage}
    790      */
    791     associatedControls: null,
    792 
    793     /**
    794      * Initializes page content.
    795      */
    796     initializePage: function() {},
    797 
    798     /**
    799      * Sets focus on the first focusable element. Override for a custom focus
    800      * strategy.
    801      */
    802     focus: function() {
    803       // Do not change focus if any control on this page is already focused.
    804       if (this.pageDiv.contains(document.activeElement))
    805         return;
    806 
    807       var elements = this.pageDiv.querySelectorAll(
    808           'input, list, select, textarea, button');
    809       for (var i = 0; i < elements.length; i++) {
    810         var element = elements[i];
    811         // Try to focus. If fails, then continue.
    812         element.focus();
    813         if (document.activeElement == element)
    814           return;
    815       }
    816     },
    817 
    818     /**
    819      * Gets the container div for this page if it is an overlay.
    820      * @type {HTMLElement}
    821      */
    822     get container() {
    823       assert(this.isOverlay);
    824       return this.pageDiv.parentNode;
    825     },
    826 
    827     /**
    828      * Gets page visibility state.
    829      * @type {boolean}
    830      */
    831     get visible() {
    832       // If this is an overlay dialog it is no longer considered visible while
    833       // the overlay is fading out. See http://crbug.com/118629.
    834       if (this.isOverlay &&
    835           this.container.classList.contains('transparent')) {
    836         return false;
    837       }
    838       if (this.pageDiv.hidden)
    839         return false;
    840       return this.pageDiv.page == this;
    841     },
    842 
    843     /**
    844      * Sets page visibility.
    845      * @type {boolean}
    846      */
    847     set visible(visible) {
    848       if ((this.visible && visible) || (!this.visible && !visible))
    849         return;
    850 
    851       // If using an overlay, the visibility of the dialog is toggled at the
    852       // same time as the overlay to show the dialog's out transition. This
    853       // is handled in setOverlayVisible.
    854       if (this.isOverlay) {
    855         this.setOverlayVisible_(visible);
    856       } else {
    857         this.pageDiv.page = this;
    858         this.pageDiv.hidden = !visible;
    859         this.onVisibilityChanged_();
    860       }
    861 
    862       cr.dispatchPropertyChange(this, 'visible', visible, !visible);
    863     },
    864 
    865     /**
    866      * Shows or hides an overlay (including any visible dialog).
    867      * @param {boolean} visible Whether the overlay should be visible or not.
    868      * @private
    869      */
    870     setOverlayVisible_: function(visible) {
    871       assert(this.isOverlay);
    872       var pageDiv = this.pageDiv;
    873       var container = this.container;
    874 
    875       if (visible)
    876         uber.invokeMethodOnParent('beginInterceptingEvents');
    877 
    878       if (container.hidden != visible) {
    879         if (visible) {
    880           // If the container is set hidden and then immediately set visible
    881           // again, the fadeCompleted_ callback would cause it to be erroneously
    882           // hidden again. Removing the transparent tag avoids that.
    883           container.classList.remove('transparent');
    884 
    885           // Hide all dialogs in this container since a different one may have
    886           // been previously visible before fading out.
    887           var pages = container.querySelectorAll('.page');
    888           for (var i = 0; i < pages.length; i++)
    889             pages[i].hidden = true;
    890           // Show the new dialog.
    891           pageDiv.hidden = false;
    892           pageDiv.page = this;
    893         }
    894         return;
    895       }
    896 
    897       var self = this;
    898       var loading = OptionsPage.isLoading();
    899       if (!loading) {
    900         // TODO(flackr): Use an event delegate to avoid having to subscribe and
    901         // unsubscribe for webkitTransitionEnd events.
    902         container.addEventListener('webkitTransitionEnd', function f(e) {
    903             var propName = e.propertyName;
    904             if (e.target != e.currentTarget ||
    905                 (propName && propName != 'opacity')) {
    906               return;
    907             }
    908             container.removeEventListener('webkitTransitionEnd', f);
    909             self.fadeCompleted_();
    910         });
    911         // -webkit-transition is 200ms. Let's wait for 400ms.
    912         ensureTransitionEndEvent(container, 400);
    913       }
    914 
    915       if (visible) {
    916         container.hidden = false;
    917         pageDiv.hidden = false;
    918         pageDiv.page = this;
    919         // NOTE: This is a hacky way to force the container to layout which
    920         // will allow us to trigger the webkit transition.
    921         container.scrollTop;
    922 
    923         this.pageDiv.removeAttribute('aria-hidden');
    924         if (this.parentPage) {
    925           this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden',
    926                                                              true);
    927         }
    928         container.classList.remove('transparent');
    929         this.onVisibilityChanged_();
    930       } else {
    931         // Kick change events for text fields.
    932         if (pageDiv.contains(document.activeElement))
    933           document.activeElement.blur();
    934         container.classList.add('transparent');
    935       }
    936 
    937       if (loading)
    938         this.fadeCompleted_();
    939     },
    940 
    941     /**
    942      * Called when a container opacity transition finishes.
    943      * @private
    944      */
    945     fadeCompleted_: function() {
    946       if (this.container.classList.contains('transparent')) {
    947         this.pageDiv.hidden = true;
    948         this.container.hidden = true;
    949 
    950         if (this.parentPage)
    951           this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
    952 
    953         if (this.nestingLevel == 1)
    954           uber.invokeMethodOnParent('stopInterceptingEvents');
    955 
    956         this.onVisibilityChanged_();
    957       }
    958     },
    959 
    960     /**
    961      * Called when a page is shown or hidden to update the root options page
    962      * based on this page's visibility.
    963      * @private
    964      */
    965     onVisibilityChanged_: function() {
    966       OptionsPage.updateRootPageFreezeState();
    967 
    968       if (this.isOverlay && !this.visible)
    969         OptionsPage.updateScrollPosition_();
    970     },
    971 
    972     /**
    973      * The nesting level of this page.
    974      * @type {number} The nesting level of this page (0 for top-level page)
    975      */
    976     get nestingLevel() {
    977       var level = 0;
    978       var parent = this.parentPage;
    979       while (parent) {
    980         level++;
    981         parent = parent.parentPage;
    982       }
    983       return level;
    984     },
    985 
    986     /**
    987      * Whether the page is considered 'sticky', such that it will
    988      * remain a top-level page even if sub-pages change.
    989      * @type {boolean} True if this page is sticky.
    990      */
    991     get sticky() {
    992       return false;
    993     },
    994 
    995     /**
    996      * Checks whether this page is an ancestor of the given page in terms of
    997      * subpage nesting.
    998      * @param {OptionsPage} page The potential descendent of this page.
    999      * @return {boolean} True if |page| is nested under this page.
   1000      */
   1001     isAncestorOfPage: function(page) {
   1002       var parent = page.parentPage;
   1003       while (parent) {
   1004         if (parent == this)
   1005           return true;
   1006         parent = parent.parentPage;
   1007       }
   1008       return false;
   1009     },
   1010 
   1011     /**
   1012      * Whether it should be possible to show the page.
   1013      * @return {boolean} True if the page should be shown.
   1014      */
   1015     canShowPage: function() {
   1016       return true;
   1017     },
   1018   };
   1019 
   1020   // Export
   1021   return {
   1022     OptionsPage: OptionsPage
   1023   };
   1024 });
   1025