- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / uber / uber.js
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
5 cr.define('uber', function() {
6   /**
7    * Options for how web history should be handled.
8    */
9   var HISTORY_STATE_OPTION = {
10     PUSH: 1,    // Push a new history state.
11     REPLACE: 2, // Replace the current history state.
12     NONE: 3,    // Ignore this history state change.
13   };
14
15   /**
16    * We cache a reference to the #navigation frame here so we don't need to grab
17    * it from the DOM on each scroll.
18    * @type {Node}
19    * @private
20    */
21   var navFrame;
22
23   /**
24    * Handles page initialization.
25    */
26   function onLoad(e) {
27     navFrame = $('navigation');
28     navFrame.dataset.width = navFrame.offsetWidth;
29
30     // Select a page based on the page-URL.
31     var params = resolvePageInfo();
32     showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path);
33
34     window.addEventListener('message', handleWindowMessage);
35     window.setTimeout(function() {
36       document.documentElement.classList.remove('loading');
37     }, 0);
38
39     // HACK(dbeam): This makes the assumption that any second part to a path
40     // will result in needing background navigation. We shortcut it to avoid
41     // flicker on load.
42     // HACK(csilv): Search URLs aren't overlays, special case them.
43     if (params.id == 'settings' && params.path &&
44         params.path.indexOf('search') != 0) {
45       backgroundNavigation();
46     }
47
48     ensureNonSelectedFrameContainersAreHidden();
49   }
50
51   /**
52    * Find page information from window.location. If the location doesn't
53    * point to one of our pages, return default parameters.
54    * @return {Object} An object containing the following parameters:
55    *     id - The 'id' of the page.
56    *     path - A path into the page, including search and hash. Optional.
57    */
58   function resolvePageInfo() {
59     var params = {};
60     var path = window.location.pathname;
61     if (path.length > 1) {
62       // Split the path into id and the remaining path.
63       path = path.slice(1);
64       var index = path.indexOf('/');
65       if (index != -1) {
66         params.id = path.slice(0, index);
67         params.path = path.slice(index + 1);
68       } else {
69         params.id = path;
70       }
71
72       var container = $(params.id);
73       if (container) {
74         // The id is valid. Add the hash and search parts of the URL to path.
75         params.path = (params.path || '') + window.location.search +
76             window.location.hash;
77       } else {
78         // The target sub-page does not exist, discard the params we generated.
79         params.id = undefined;
80         params.path = undefined;
81       }
82     }
83     // If we don't have a valid page, get a default.
84     if (!params.id)
85       params.id = getDefaultIframe().id;
86
87     return params;
88   }
89
90   /**
91    * Handler for window.onpopstate.
92    * @param {Event} e The history event.
93    */
94   function onPopHistoryState(e) {
95     if (e.state && e.state.pageId)
96       showPage(e.state.pageId, HISTORY_STATE_OPTION.NONE);
97   }
98
99   /**
100    * @return {Object} The default iframe container.
101    */
102   function getDefaultIframe() {
103     return $(loadTimeData.getString('helpHost'));
104   }
105
106   /**
107    * @return {Object} The currently selected iframe container.
108    */
109   function getSelectedIframe() {
110     return document.querySelector('.iframe-container.selected');
111   }
112
113   /**
114    * Handles postMessage calls from the iframes of the contained pages.
115    *
116    * The pages request functionality from this object by passing an object of
117    * the following form:
118    *
119    *  { method : "methodToInvoke",
120    *    params : {...}
121    *  }
122    *
123    * |method| is required, while |params| is optional. Extra parameters required
124    * by a method must be specified by that method's documentation.
125    *
126    * @param {Event} e The posted object.
127    */
128   function handleWindowMessage(e) {
129     if (e.data.method === 'beginInterceptingEvents')
130       backgroundNavigation();
131     else if (e.data.method === 'stopInterceptingEvents')
132       foregroundNavigation();
133     else if (e.data.method === 'setPath')
134       setPath(e.origin, e.data.params.path);
135     else if (e.data.method === 'setTitle')
136       setTitle(e.origin, e.data.params.title);
137     else if (e.data.method === 'showPage')
138       showPage(e.data.params.pageId, HISTORY_STATE_OPTION.PUSH);
139     else if (e.data.method === 'navigationControlsLoaded')
140       onNavigationControlsLoaded();
141     else if (e.data.method === 'adjustToScroll')
142       adjustToScroll(e.data.params);
143     else if (e.data.method === 'mouseWheel')
144       forwardMouseWheel(e.data.params);
145     else
146       console.error('Received unexpected message', e.data);
147   }
148
149   /**
150    * Sends the navigation iframe to the background.
151    */
152   function backgroundNavigation() {
153     navFrame.classList.add('background');
154     navFrame.firstChild.tabIndex = -1;
155     navFrame.firstChild.setAttribute('aria-hidden', true);
156   }
157
158   /**
159    * Retrieves the navigation iframe from the background.
160    */
161   function foregroundNavigation() {
162     navFrame.classList.remove('background');
163     navFrame.firstChild.tabIndex = 0;
164     navFrame.firstChild.removeAttribute('aria-hidden');
165   }
166
167   /**
168    * Enables or disables animated transitions when changing content while
169    * horizontally scrolled.
170    * @param {boolean} enabled True if enabled, else false to disable.
171    */
172   function setContentChanging(enabled) {
173     navFrame.classList[enabled ? 'add' : 'remove']('changing-content');
174
175     if (isRTL()) {
176       uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
177                                 'setContentChanging',
178                                 enabled);
179     }
180   }
181
182   /**
183    * Get an iframe based on the origin of a received post message.
184    * @param {string} origin The origin of a post message.
185    * @return {!HTMLElement} The frame associated to |origin| or null.
186    */
187   function getIframeFromOrigin(origin) {
188     assert(origin.substr(-1) != '/', 'invalid origin given');
189     var query = '.iframe-container > iframe[src^="' + origin + '/"]';
190     return document.querySelector(query);
191   }
192
193   /**
194    * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)).
195    * @param {string} path The new /path/ to be set after the page name.
196    * @param {number} historyOption The type of history modification to make.
197    */
198   function changePathTo(path, historyOption) {
199     assert(!path || path.substr(-1) != '/', 'invalid path given');
200
201     var histFunc;
202     if (historyOption == HISTORY_STATE_OPTION.PUSH)
203       histFunc = window.history.pushState;
204     else if (historyOption == HISTORY_STATE_OPTION.REPLACE)
205       histFunc = window.history.replaceState;
206
207     assert(histFunc, 'invalid historyOption given ' + historyOption);
208
209     var pageId = getSelectedIframe().id;
210     var args = [{pageId: pageId}, '', '/' + pageId + '/' + (path || '')];
211     histFunc.apply(window.history, args);
212   }
213
214   /**
215    * Sets the "path" of the page (actually the path after the first '/' char).
216    * @param {Object} origin The origin of the source iframe.
217    * @param {string} title The new "path".
218    */
219   function setPath(origin, path) {
220     assert(!path || path[0] != '/', 'invalid path sent from ' + origin);
221     // Only update the currently displayed path if this is the visible frame.
222     if (getIframeFromOrigin(origin).parentNode == getSelectedIframe())
223       changePathTo(path, HISTORY_STATE_OPTION.REPLACE);
224   }
225
226   /**
227    * Sets the title of the page.
228    * @param {Object} origin The origin of the source iframe.
229    * @param {string} title The title of the page.
230    */
231   function setTitle(origin, title) {
232     // Cache the title for the client iframe, i.e., the iframe setting the
233     // title. querySelector returns the actual iframe element, so use parentNode
234     // to get back to the container.
235     var container = getIframeFromOrigin(origin).parentNode;
236     container.dataset.title = title;
237
238     // Only update the currently displayed title if this is the visible frame.
239     if (container == getSelectedIframe())
240       document.title = title;
241   }
242
243   /**
244    * Selects a subpage. This is called from uber-frame.
245    * @param {string} pageId Should matche an id of one of the iframe containers.
246    * @param {integer} historyOption Indicates whether we should push or replace
247    *     browser history.
248    * @param {string} path A sub-page path.
249    */
250   function showPage(pageId, historyOption, path) {
251     var container = $(pageId);
252     var lastSelected = document.querySelector('.iframe-container.selected');
253
254     // Lazy load of iframe contents.
255     var sourceUrl = container.dataset.url + (path || '');
256     var frame = container.querySelector('iframe');
257     if (!frame) {
258       frame = container.ownerDocument.createElement('iframe');
259       container.appendChild(frame);
260       frame.src = sourceUrl;
261     } else {
262       // There's no particularly good way to know what the current URL of the
263       // content frame is as we don't have access to its contentWindow's
264       // location, so just replace every time until necessary to do otherwise.
265       frame.contentWindow.location.replace(sourceUrl);
266     }
267
268     // If the last selected container is already showing, ignore the rest.
269     if (lastSelected === container)
270       return;
271
272     if (lastSelected) {
273       lastSelected.classList.remove('selected');
274       // Setting aria-hidden hides the container from assistive technology
275       // immediately. The 'hidden' attribute is set after the transition
276       // finishes - that ensures it's not possible to accidentally focus
277       // an element in an unselected container.
278       lastSelected.setAttribute('aria-hidden', 'true');
279     }
280
281     // Containers that aren't selected have to be hidden so that their
282     // content isn't focusable.
283     container.hidden = false;
284     container.setAttribute('aria-hidden', 'false');
285
286     // Trigger a layout after making it visible and before setting
287     // the class to 'selected', so that it animates in.
288     container.offsetTop;
289     container.classList.add('selected');
290
291     setContentChanging(true);
292     adjustToScroll(0);
293
294     var selectedFrame = getSelectedIframe().querySelector('iframe');
295     uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected');
296
297     if (historyOption != HISTORY_STATE_OPTION.NONE)
298       changePathTo(path, historyOption);
299
300     if (container.dataset.title)
301       document.title = container.dataset.title;
302     $('favicon').href = 'chrome://theme/' + container.dataset.favicon;
303     $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x';
304
305     updateNavigationControls();
306   }
307
308   function onNavigationControlsLoaded() {
309     updateNavigationControls();
310   }
311
312   /**
313    * Sends a message to uber-frame to update the appearance of the nav controls.
314    * It should be called whenever the selected iframe changes.
315    */
316   function updateNavigationControls() {
317     var iframe = getSelectedIframe();
318     uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
319                               'changeSelection', {pageId: iframe.id});
320   }
321
322   /**
323    * Forwarded scroll offset from a content frame's scroll handler.
324    * @param {number} scrollOffset The scroll offset from the content frame.
325    */
326   function adjustToScroll(scrollOffset) {
327     // NOTE: The scroll is reset to 0 and easing turned on every time a user
328     // switches frames. If we receive a non-zero value it has to have come from
329     // a real user scroll, so we disable easing when this happens.
330     if (scrollOffset != 0)
331       setContentChanging(false);
332
333     if (isRTL()) {
334       uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
335                                 'adjustToScroll',
336                                 scrollOffset);
337       var navWidth = Math.max(0, +navFrame.dataset.width + scrollOffset);
338       navFrame.style.width = navWidth + 'px';
339     } else {
340       navFrame.style.webkitTransform = 'translateX(' + -scrollOffset + 'px)';
341     }
342   }
343
344   /**
345    * Forward scroll wheel events to subpages.
346    * @param {Object} params Relevant parameters of wheel event.
347    */
348   function forwardMouseWheel(params) {
349     var iframe = getSelectedIframe().querySelector('iframe');
350     uber.invokeMethodOnWindow(iframe.contentWindow, 'mouseWheel', params);
351   }
352
353   /**
354    * Make sure that iframe containers that are not selected are
355    * hidden, so that elements in those frames aren't part of the
356    * focus order. Containers that are unselected later get hidden
357    * when the transition ends. We also set the aria-hidden attribute
358    * because that hides the container from assistive technology
359    * immediately, rather than only after the transition ends.
360    */
361   function ensureNonSelectedFrameContainersAreHidden() {
362     var containers = document.querySelectorAll('.iframe-container');
363     for (var i = 0; i < containers.length; i++) {
364       var container = containers[i];
365       if (!container.classList.contains('selected')) {
366         container.hidden = true;
367         container.setAttribute('aria-hidden', 'true');
368       }
369       container.addEventListener('webkitTransitionEnd', function(event) {
370         if (!event.target.classList.contains('selected'))
371           event.target.hidden = true;
372       });
373     }
374   }
375
376   return {
377     onLoad: onLoad,
378     onPopHistoryState: onPopHistoryState
379   };
380 });
381
382 window.addEventListener('popstate', uber.onPopHistoryState);
383 document.addEventListener('DOMContentLoaded', uber.onLoad);