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