Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / page_manager / page.js
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.
4
5 cr.define('cr.ui.pageManager', function() {
6   var PageManager = cr.ui.pageManager.PageManager;
7
8   /**
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.
14    * @constructor
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}
19    */
20   function Page(name, title, pageDivName) {
21     this.name = name;
22     this.title = title;
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
27     // underlying div.
28     this.pageDiv.page = null;
29     this.tab = null;
30     this.lastFocusedElement = null;
31   }
32
33   Page.prototype = {
34     __proto__: cr.EventTarget.prototype,
35
36     /**
37      * The parent page of this page, or null for root pages.
38      * @type {Page}
39      */
40     parentPage: null,
41
42     /**
43      * The section on the parent page that is associated with this page.
44      * Can be null.
45      * @type {Element}
46      */
47     associatedSection: null,
48
49     /**
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>}
53      */
54     associatedControls: null,
55
56     /**
57      * If true, this page should always be considered the top-most page when
58      * visible.
59      * @type {boolean}
60      */
61     alwaysOnTop_: false,
62
63     /**
64      * Initializes page content.
65      */
66     initializePage: function() {},
67
68     /**
69      * Sets focus on the first focusable element. Override for a custom focus
70      * strategy.
71      */
72     focus: function() {
73       // Do not change focus if any control on this page is already focused.
74       if (this.pageDiv.contains(document.activeElement))
75         return;
76
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.
82         element.focus();
83         if (document.activeElement == element)
84           return;
85       }
86     },
87
88     /**
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.
94      */
95     reverseButtonStrip: function() {
96       assert(this.isOverlay);
97       var buttonStrips =
98           this.pageDiv.querySelectorAll('.button-strip:not([reversed])');
99
100       // Reverse all button-strips in the overlay.
101       for (var j = 0; j < buttonStrips.length; j++) {
102         var buttonStrip = buttonStrips[j];
103
104         var childNodes = buttonStrip.childNodes;
105         for (var i = childNodes.length - 1; i >= 0; i--)
106           buttonStrip.appendChild(childNodes[i]);
107
108         buttonStrip.setAttribute('reversed', '');
109       }
110     },
111
112     /**
113      * Whether it should be possible to show the page.
114      * @return {boolean} True if the page should be shown.
115      */
116     canShowPage: function() {
117       return true;
118     },
119
120     /**
121      * Gets the container div for this page if it is an overlay.
122      * @type {HTMLDivElement}
123      */
124     get container() {
125       assert(this.isOverlay);
126       return this.pageDiv.parentNode;
127     },
128
129     /**
130      * Gets page visibility state.
131      * @type {boolean}
132      */
133     get visible() {
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')) {
138         return false;
139       }
140       if (this.pageDiv.hidden)
141         return false;
142       return this.pageDiv.page == this;
143     },
144
145     /**
146      * Sets page visibility.
147      * @type {boolean}
148      */
149     set visible(visible) {
150       if ((this.visible && visible) || (!this.visible && !visible))
151         return;
152
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);
158       } else {
159         this.pageDiv.page = this;
160         this.pageDiv.hidden = !visible;
161         PageManager.onPageVisibilityChanged(this);
162       }
163
164       cr.dispatchPropertyChange(this, 'visible', visible, !visible);
165     },
166
167     /**
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.
171      */
172     get sticky() {
173       return false;
174     },
175
176     /**
177      * @type {boolean} True if this page should always be considered the
178      *     top-most page when visible.
179      */
180     get alwaysOnTop() {
181       return this.alwaysOnTop_;
182     },
183
184     /**
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.
187      */
188     set alwaysOnTop(value) {
189       assert(this.isOverlay);
190       this.alwaysOnTop_ = value;
191     },
192
193     /**
194      * Shows or hides an overlay (including any visible dialog).
195      * @param {boolean} visible Whether the overlay should be visible or not.
196      * @private
197      */
198     setOverlayVisible_: function(visible) {
199       assert(this.isOverlay);
200       var pageDiv = this.pageDiv;
201       var container = this.container;
202
203       if (container.hidden != visible) {
204         if (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');
209
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;
217           pageDiv.page = this;
218         }
219         return;
220       }
221
222       var self = this;
223       var loading = PageManager.isLoading();
224       if (!loading) {
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')) {
231               return;
232             }
233             container.removeEventListener('webkitTransitionEnd', f);
234             self.fadeCompleted_();
235         });
236         // -webkit-transition is 200ms. Let's wait for 400ms.
237         ensureTransitionEndEvent(container, 400);
238       }
239
240       if (visible) {
241         container.hidden = false;
242         pageDiv.hidden = false;
243         pageDiv.page = this;
244         // NOTE: This is a hacky way to force the container to layout which
245         // will allow us to trigger the webkit transition.
246         container.scrollTop;
247
248         this.pageDiv.removeAttribute('aria-hidden');
249         if (this.parentPage) {
250           this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden',
251                                                              true);
252         }
253         container.classList.remove('transparent');
254         PageManager.onPageVisibilityChanged(this);
255       } else {
256         // Kick change events for text fields.
257         if (pageDiv.contains(document.activeElement))
258           document.activeElement.blur();
259         container.classList.add('transparent');
260       }
261
262       if (loading)
263         this.fadeCompleted_();
264     },
265
266     /**
267      * Called when a container opacity transition finishes.
268      * @private
269      */
270     fadeCompleted_: function() {
271       if (this.container.classList.contains('transparent')) {
272         this.pageDiv.hidden = true;
273         this.container.hidden = true;
274
275         if (this.parentPage)
276           this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
277
278         PageManager.onPageVisibilityChanged(this);
279       }
280     },
281   };
282
283   // Export
284   return {
285     Page: Page
286   };
287 });