Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / options / options_page.js
blobbefb7c813981bc869dfda9aa7f179a426fd178f3
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.
5 cr.define('options', function() {
6   /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
8   /////////////////////////////////////////////////////////////////////////////
9   // OptionsPage class:
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   }
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;
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;
46   /**
47    * Main level option pages. Maps lower-case page names to the respective page
48    * object.
49    * @protected
50    */
51   OptionsPage.registeredPages = {};
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 = {};
60   /**
61    * Gets the default page (to be shown on initial load).
62    */
63   OptionsPage.getDefaultPage = function() {
64     return BrowserOptions.getInstance();
65   };
67   /**
68    * Shows the default page.
69    */
70   OptionsPage.showDefaultPage = function() {
71     this.navigateToPage(this.getDefaultPage().name);
72   };
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   };
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 || {};
98     // If a bubble is currently being shown, hide it.
99     this.hideBubble();
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     }
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     }
124     pageName = targetPage.name.toLowerCase();
125     var targetPageWasVisible = targetPage.visible;
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;
133     var allPageNames = Array.prototype.concat.call(
134         Object.keys(this.registeredPages),
135         Object.keys(this.registeredOverlayPages));
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     }
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     }
160     // Update the history and current location.
161     if (updateHistory)
162       this.updateHistoryState_(!!opt_propertyBag.replaceState);
164     // Update tab title.
165     this.setTitle_(targetPage.title);
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     }
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   };
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   };
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   };
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.
224     // Update tab title.
225     this.setTitle_(page.title);
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;
232     var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash;
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});
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   };
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;
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;
265     if ((!rootPage || !rootPage.sticky) &&
266         overlay.parentPage &&
267         !overlay.parentPage.visible) {
268       this.showPageByName(overlay.parentPage.name, false);
269     }
271     if (!overlay.visible) {
272       overlay.visible = true;
273       if (overlay.didShowPage) overlay.didShowPage();
274     }
276     // Update tab title.
277     this.setTitle_(overlay.title);
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     }
289     if ($('search-field').value == '') {
290       var section = overlay.associatedSection;
291       if (section)
292         options.BrowserOptions.scrollToSection(section);
293     }
295     return true;
296   };
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   };
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   };
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   };
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;
341     overlay.visible = false;
343     if (overlay.didClosePage) overlay.didClosePage();
344     this.updateHistoryState_(false, {ignoreHash: true});
346     this.restoreLastFocusedElement_();
347   };
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   };
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   };
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   };
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     }
405     return topPage;
406   };
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   };
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   };
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();
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   };
448   /**
449    * Hides the currently visible bubble, if any.
450    */
451   OptionsPage.hideBubble = function() {
452     if (OptionsPage.bubble_)
453       OptionsPage.bubble_.hide();
454   };
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;
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;
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     }
484     tab.classList.add('active-tab');
485     $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
486     tabBar.activeNavTab = tab;
487   };
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   };
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   };
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       }
531       // Sanity check.
532       for (var i = 0; i < associatedControls.length; ++i) {
533         assert(associatedControls[i], 'Invalid element passed.');
534       }
535     }
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);
542     overlay.tab = undefined;
543     overlay.isOverlay = true;
544     overlay.initializePage();
545   };
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])');
560     // Reverse all button-strips in the overlay.
561     for (var j = 0; j < buttonStrips.length; j++) {
562       var buttonStrip = buttonStrips[j];
564       var childNodes = buttonStrip.childNodes;
565       for (var i = childNodes.length - 1; i >= 0; i--)
566         buttonStrip.appendChild(childNodes[i]);
568       buttonStrip.setAttribute('reversed', '');
569     }
570   };
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   };
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   };
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;
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   };
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   };
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));
649     // Trigger the scroll handler manually to set the initial state.
650     this.handleScroll_();
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     }
661     cr.ui.overlay.globalInitialization();
662   };
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   };
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   };
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   };
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   };
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   };
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   };
731   OptionsPage.setPepperFlashSettingsEnabled = function(enabled) {
732     if (enabled) {
733       document.documentElement.setAttribute(
734           'enablePepperFlashSettings', '');
735     } else {
736       document.documentElement.removeAttribute(
737           'enablePepperFlashSettings');
738     }
739   };
741   OptionsPage.setIsSettingsApp = function() {
742     document.documentElement.classList.add('settings-app');
743   };
745   OptionsPage.isSettingsApp = function() {
746     return document.documentElement.classList.contains('settings-app');
747   };
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   };
757   OptionsPage.prototype = {
758     __proto__: cr.EventTarget.prototype,
760     /**
761      * The parent page of this option page, or null for top-level pages.
762      * @type {OptionsPage}
763      */
764     parentPage: null,
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,
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,
780     /**
781      * Initializes page content.
782      */
783     initializePage: function() {},
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;
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     },
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     },
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     },
830     /**
831      * Sets page visibility.
832      * @type {boolean}
833      */
834     set visible(visible) {
835       if ((this.visible && visible) || (!this.visible && !visible))
836         return;
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       }
849       cr.dispatchPropertyChange(this, 'visible', visible, !visible);
850     },
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;
862       if (visible)
863         uber.invokeMethodOnParent('beginInterceptingEvents');
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');
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       }
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       }
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;
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       }
919       if (loading)
920         this.fadeCompleted_();
921     },
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;
932         if (this.parentPage)
933           this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
935         if (this.nestingLevel == 1)
936           uber.invokeMethodOnParent('stopInterceptingEvents');
938         this.onVisibilityChanged_();
939       }
940     },
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();
950       if (this.isOverlay && !this.visible)
951         OptionsPage.updateScrollPosition_();
952     },
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     },
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     },
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     },
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   };
1002   // Export
1003   return {
1004     OptionsPage: OptionsPage
1005   };