• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5cr.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