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