1 // Copyright 2014 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('cr.ui.pageManager', function() {
6 var PageManager = cr.ui.pageManager.PageManager;
9 * Base class for pages that can be shown and hidden by PageManager. Each Page
10 * is like a node in a forest, corresponding to a particular div. At any
11 * point, one root Page is visible, and any visible Page can show a child Page
12 * as an overlay. The host of the root Page(s) should provide a container div
13 * for each nested level to enforce the stack order of overlays.
15 * @param {string} name Page name.
16 * @param {string} title Page title, used for history.
17 * @param {string} pageDivName ID of the div corresponding to the page.
18 * @extends {EventTarget}
20 function Page(name, title, pageDivName) {
23 this.pageDivName = pageDivName;
24 this.pageDiv = $(this.pageDivName);
25 // |pageDiv.page| is set to the page object (this) when the page is visible
26 // to track which page is being shown when multiple pages can share the same
28 this.pageDiv.page = null;
30 this.lastFocusedElement = null;
34 __proto__: cr.EventTarget.prototype,
37 * The parent page of this page, or null for root pages.
43 * The section on the parent page that is associated with this page.
47 associatedSection: null,
50 * An array of controls that are associated with this page. The first
51 * control should be located on a root page.
52 * @type {Array.<Element>}
54 associatedControls: null,
57 * If true, this page should always be considered the top-most page when
64 * Initializes page content.
66 initializePage: function() {},
69 * Sets focus on the first focusable element. Override for a custom focus
73 // Do not change focus if any control on this page is already focused.
74 if (this.pageDiv.contains(document.activeElement))
77 var elements = this.pageDiv.querySelectorAll(
78 'input, list, select, textarea, button');
79 for (var i = 0; i < elements.length; i++) {
80 var element = elements[i];
81 // Try to focus. If fails, then continue.
83 if (document.activeElement == element)
89 * Reverses the child elements of this overlay's button strip if it hasn't
90 * already been reversed. This is necessary because WebKit does not alter
91 * the tab order for elements that are visually reversed using
92 * flex-direction: reverse, and the button order is reversed for views.
93 * See http://webk.it/62664 for more information.
95 reverseButtonStrip: function() {
96 assert(this.isOverlay);
98 this.pageDiv.querySelectorAll('.button-strip:not([reversed])');
100 // Reverse all button-strips in the overlay.
101 for (var j = 0; j < buttonStrips.length; j++) {
102 var buttonStrip = buttonStrips[j];
104 var childNodes = buttonStrip.childNodes;
105 for (var i = childNodes.length - 1; i >= 0; i--)
106 buttonStrip.appendChild(childNodes[i]);
108 buttonStrip.setAttribute('reversed', '');
113 * Whether it should be possible to show the page.
114 * @return {boolean} True if the page should be shown.
116 canShowPage: function() {
121 * Gets the container div for this page if it is an overlay.
122 * @type {HTMLDivElement}
125 assert(this.isOverlay);
126 return this.pageDiv.parentNode;
130 * Gets page visibility state.
134 // If this is an overlay dialog it is no longer considered visible while
135 // the overlay is fading out. See http://crbug.com/118629.
136 if (this.isOverlay &&
137 this.container.classList.contains('transparent')) {
140 if (this.pageDiv.hidden)
142 return this.pageDiv.page == this;
146 * Sets page visibility.
149 set visible(visible) {
150 if ((this.visible && visible) || (!this.visible && !visible))
153 // If using an overlay, the visibility of the dialog is toggled at the
154 // same time as the overlay to show the dialog's out transition. This
155 // is handled in setOverlayVisible.
156 if (this.isOverlay) {
157 this.setOverlayVisible_(visible);
159 this.pageDiv.page = this;
160 this.pageDiv.hidden = !visible;
161 PageManager.onPageVisibilityChanged(this);
164 cr.dispatchPropertyChange(this, 'visible', visible, !visible);
168 * Whether the page is considered 'sticky', such that it will remain a root
169 * page even if sub-pages change.
170 * @type {boolean} True if this page is sticky.
177 * @type {boolean} True if this page should always be considered the
178 * top-most page when visible.
181 return this.alwaysOnTop_;
185 * @type {boolean} True if this page should always be considered the
186 * top-most page when visible. Only overlays can be always on top.
188 set alwaysOnTop(value) {
189 assert(this.isOverlay);
190 this.alwaysOnTop_ = value;
194 * Shows or hides an overlay (including any visible dialog).
195 * @param {boolean} visible Whether the overlay should be visible or not.
198 setOverlayVisible_: function(visible) {
199 assert(this.isOverlay);
200 var pageDiv = this.pageDiv;
201 var container = this.container;
203 if (container.hidden != visible) {
205 // If the container is set hidden and then immediately set visible
206 // again, the fadeCompleted_ callback would cause it to be erroneously
207 // hidden again. Removing the transparent tag avoids that.
208 container.classList.remove('transparent');
210 // Hide all dialogs in this container since a different one may have
211 // been previously visible before fading out.
212 var pages = container.querySelectorAll('.page');
213 for (var i = 0; i < pages.length; i++)
214 pages[i].hidden = true;
215 // Show the new dialog.
216 pageDiv.hidden = false;
223 var loading = PageManager.isLoading();
225 // TODO(flackr): Use an event delegate to avoid having to subscribe and
226 // unsubscribe for webkitTransitionEnd events.
227 container.addEventListener('webkitTransitionEnd', function f(e) {
228 var propName = e.propertyName;
229 if (e.target != e.currentTarget ||
230 (propName && propName != 'opacity')) {
233 container.removeEventListener('webkitTransitionEnd', f);
234 self.fadeCompleted_();
236 // -webkit-transition is 200ms. Let's wait for 400ms.
237 ensureTransitionEndEvent(container, 400);
241 container.hidden = false;
242 pageDiv.hidden = false;
244 // NOTE: This is a hacky way to force the container to layout which
245 // will allow us to trigger the webkit transition.
248 this.pageDiv.removeAttribute('aria-hidden');
249 if (this.parentPage) {
250 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden',
253 container.classList.remove('transparent');
254 PageManager.onPageVisibilityChanged(this);
256 // Kick change events for text fields.
257 if (pageDiv.contains(document.activeElement))
258 document.activeElement.blur();
259 container.classList.add('transparent');
263 this.fadeCompleted_();
267 * Called when a container opacity transition finishes.
270 fadeCompleted_: function() {
271 if (this.container.classList.contains('transparent')) {
272 this.pageDiv.hidden = true;
273 this.container.hidden = true;
276 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
278 PageManager.onPageVisibilityChanged(this);