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('uber', function() {
7 * Options for how web history should be handled.
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.
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.
24 * Handles page initialization.
27 navFrame = $('navigation');
28 navFrame.dataset.width = navFrame.offsetWidth;
30 // Select a page based on the page-URL.
31 var params = resolvePageInfo();
32 showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path);
34 window.addEventListener('message', handleWindowMessage);
35 window.setTimeout(function() {
36 document.documentElement.classList.remove('loading');
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
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();
48 ensureNonSelectedFrameContainersAreHidden();
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.
58 function resolvePageInfo() {
60 var path = window.location.pathname;
61 if (path.length > 1) {
62 // Split the path into id and the remaining path.
64 var index = path.indexOf('/');
66 params.id = path.slice(0, index);
67 params.path = path.slice(index + 1);
72 var container = $(params.id);
74 // The id is valid. Add the hash and search parts of the URL to path.
75 params.path = (params.path || '') + window.location.search +
78 // The target sub-page does not exist, discard the params we generated.
79 params.id = undefined;
80 params.path = undefined;
83 // If we don't have a valid page, get a default.
85 params.id = getDefaultIframe().id;
91 * Handler for window.onpopstate.
92 * @param {Event} e The history event.
94 function onPopHistoryState(e) {
95 if (e.state && e.state.pageId)
96 showPage(e.state.pageId, HISTORY_STATE_OPTION.NONE);
100 * @return {Object} The default iframe container.
102 function getDefaultIframe() {
103 return $(loadTimeData.getString('helpHost'));
107 * @return {Object} The currently selected iframe container.
109 function getSelectedIframe() {
110 return document.querySelector('.iframe-container.selected');
114 * Handles postMessage calls from the iframes of the contained pages.
116 * The pages request functionality from this object by passing an object of
117 * the following form:
119 * { method : "methodToInvoke",
123 * |method| is required, while |params| is optional. Extra parameters required
124 * by a method must be specified by that method's documentation.
126 * @param {Event} e The posted object.
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);
146 console.error('Received unexpected message', e.data);
150 * Sends the navigation iframe to the background.
152 function backgroundNavigation() {
153 navFrame.classList.add('background');
154 navFrame.firstChild.tabIndex = -1;
155 navFrame.firstChild.setAttribute('aria-hidden', true);
159 * Retrieves the navigation iframe from the background.
161 function foregroundNavigation() {
162 navFrame.classList.remove('background');
163 navFrame.firstChild.tabIndex = 0;
164 navFrame.firstChild.removeAttribute('aria-hidden');
168 * Enables or disables animated transitions when changing content while
169 * horizontally scrolled.
170 * @param {boolean} enabled True if enabled, else false to disable.
172 function setContentChanging(enabled) {
173 navFrame.classList[enabled ? 'add' : 'remove']('changing-content');
176 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
177 'setContentChanging',
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.
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);
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.
198 function changePathTo(path, historyOption) {
199 assert(!path || path.substr(-1) != '/', 'invalid path given');
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;
207 assert(histFunc, 'invalid historyOption given ' + historyOption);
209 var pageId = getSelectedIframe().id;
210 var args = [{pageId: pageId}, '', '/' + pageId + '/' + (path || '')];
211 histFunc.apply(window.history, args);
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".
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);
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.
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;
238 // Only update the currently displayed title if this is the visible frame.
239 if (container == getSelectedIframe())
240 document.title = title;
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
248 * @param {string} path A sub-page path.
250 function showPage(pageId, historyOption, path) {
251 var container = $(pageId);
252 var lastSelected = document.querySelector('.iframe-container.selected');
254 // Lazy load of iframe contents.
255 var sourceUrl = container.dataset.url + (path || '');
256 var frame = container.querySelector('iframe');
258 frame = container.ownerDocument.createElement('iframe');
259 container.appendChild(frame);
260 frame.src = sourceUrl;
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);
268 // If the last selected container is already showing, ignore the rest.
269 if (lastSelected === container)
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');
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');
286 // Trigger a layout after making it visible and before setting
287 // the class to 'selected', so that it animates in.
289 container.classList.add('selected');
291 setContentChanging(true);
294 var selectedFrame = getSelectedIframe().querySelector('iframe');
295 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected');
297 if (historyOption != HISTORY_STATE_OPTION.NONE)
298 changePathTo(path, historyOption);
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';
305 updateNavigationControls();
308 function onNavigationControlsLoaded() {
309 updateNavigationControls();
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.
316 function updateNavigationControls() {
317 var iframe = getSelectedIframe();
318 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
319 'changeSelection', {pageId: iframe.id});
323 * Forwarded scroll offset from a content frame's scroll handler.
324 * @param {number} scrollOffset The scroll offset from the content frame.
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);
334 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow,
337 var navWidth = Math.max(0, +navFrame.dataset.width + scrollOffset);
338 navFrame.style.width = navWidth + 'px';
340 navFrame.style.webkitTransform = 'translateX(' + -scrollOffset + 'px)';
345 * Forward scroll wheel events to subpages.
346 * @param {Object} params Relevant parameters of wheel event.
348 function forwardMouseWheel(params) {
349 var iframe = getSelectedIframe().querySelector('iframe');
350 uber.invokeMethodOnWindow(iframe.contentWindow, 'mouseWheel', params);
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.
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');
369 container.addEventListener('webkitTransitionEnd', function(event) {
370 if (!event.target.classList.contains('selected'))
371 event.target.hidden = true;
378 onPopHistoryState: onPopHistoryState
382 window.addEventListener('popstate', uber.onPopHistoryState);
383 document.addEventListener('DOMContentLoaded', uber.onLoad);