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 * A queue of method invocations on one of the iframes; if the iframe has not
25 * loaded by the time there is a method to invoke, delay the invocation until
30 var queuedInvokes
= {};
33 * Handles page initialization.
36 navFrame
= $('navigation');
37 navFrame
.dataset
.width
= navFrame
.offsetWidth
;
39 // Select a page based on the page-URL.
40 var params
= resolvePageInfo();
41 showPage(params
.id
, HISTORY_STATE_OPTION
.NONE
, params
.path
);
43 window
.addEventListener('message', handleWindowMessage
);
44 window
.setTimeout(function() {
45 document
.documentElement
.classList
.remove('loading');
48 // HACK(dbeam): This makes the assumption that any second part to a path
49 // will result in needing background navigation. We shortcut it to avoid
51 // HACK(csilv): Search URLs aren't overlays, special case them.
52 if (params
.id
== 'settings' && params
.path
&&
53 params
.path
.indexOf('search') != 0) {
54 backgroundNavigation();
57 ensureNonSelectedFrameContainersAreHidden();
61 * Find page information from window.location. If the location doesn't
62 * point to one of our pages, return default parameters.
63 * @return {Object} An object containing the following parameters:
64 * id - The 'id' of the page.
65 * path - A path into the page, including search and hash. Optional.
67 function resolvePageInfo() {
69 var path
= window
.location
.pathname
;
70 if (path
.length
> 1) {
71 // Split the path into id and the remaining path.
73 var index
= path
.indexOf('/');
75 params
.id
= path
.slice(0, index
);
76 params
.path
= path
.slice(index
+ 1);
81 var container
= $(params
.id
);
83 // The id is valid. Add the hash and search parts of the URL to path.
84 params
.path
= (params
.path
|| '') + window
.location
.search
+
87 // The target sub-page does not exist, discard the params we generated.
88 params
.id
= undefined;
89 params
.path
= undefined;
92 // If we don't have a valid page, get a default.
94 params
.id
= getDefaultIframe().id
;
100 * Handler for window.onpopstate.
101 * @param {Event} e The history event.
103 function onPopHistoryState(e
) {
104 // Use the URL to determine which page to route to.
105 var params
= resolvePageInfo();
107 // If the page isn't the current page, load it fresh. Even if the page is
108 // already loaded, it may have state not reflected in the URL, such as the
109 // history page's "Remove selected items" overlay. http://crbug.com/377386
110 if (getRequiredElement(params
.id
) !== getSelectedIframeContainer())
111 showPage(params
.id
, HISTORY_STATE_OPTION
.NONE
, params
.path
);
113 // Either way, send the state down to it.
115 // Note: This assumes that the state and path parameters for every page
116 // under this origin are compatible. All of the downstream pages which
117 // navigate use pushState and replaceState.
118 invokeMethodOnPage(params
.id
, 'popState',
119 {state
: e
.state
, path
: '/' + params
.path
});
123 * @return {Object} The default iframe container.
125 function getDefaultIframe() {
126 return $(loadTimeData
.getString('helpHost'));
130 * @return {Object} The currently selected iframe container.
132 function getSelectedIframeContainer() {
133 return document
.querySelector('.iframe-container.selected');
137 * @return {Object} The currently selected iframe's contentWindow.
139 function getSelectedIframeWindow() {
140 return getSelectedIframeContainer().querySelector('iframe').contentWindow
;
144 * Handles postMessage calls from the iframes of the contained pages.
146 * The pages request functionality from this object by passing an object of
147 * the following form:
149 * { method : "methodToInvoke",
153 * |method| is required, while |params| is optional. Extra parameters required
154 * by a method must be specified by that method's documentation.
156 * @param {Event} e The posted object.
158 function handleWindowMessage(e
) {
159 e
= /** @type{!MessageEvent<!{method: string, params: *}>} */(e
);
160 if (e
.data
.method
=== 'beginInterceptingEvents') {
161 backgroundNavigation();
162 } else if (e
.data
.method
=== 'stopInterceptingEvents') {
163 foregroundNavigation();
164 } else if (e
.data
.method
=== 'ready') {
166 } else if (e
.data
.method
=== 'updateHistory') {
167 updateHistory(e
.origin
, e
.data
.params
.state
, e
.data
.params
.path
,
168 e
.data
.params
.replace
);
169 } else if (e
.data
.method
=== 'setTitle') {
170 setTitle(e
.origin
, e
.data
.params
.title
);
171 } else if (e
.data
.method
=== 'showPage') {
172 showPage(e
.data
.params
.pageId
,
173 HISTORY_STATE_OPTION
.PUSH
,
175 } else if (e
.data
.method
=== 'navigationControlsLoaded') {
176 onNavigationControlsLoaded();
177 } else if (e
.data
.method
=== 'adjustToScroll') {
178 adjustToScroll(/** @type {number} */(e
.data
.params
));
179 } else if (e
.data
.method
=== 'mouseWheel') {
180 forwardMouseWheel(/** @type {Object} */(e
.data
.params
));
181 } else if (e
.data
.method
=== 'mouseDown') {
184 console
.error('Received unexpected message', e
.data
);
189 * Sends the navigation iframe to the background.
191 function backgroundNavigation() {
192 navFrame
.classList
.add('background');
193 navFrame
.firstChild
.tabIndex
= -1;
194 navFrame
.firstChild
.setAttribute('aria-hidden', true);
198 * Retrieves the navigation iframe from the background.
200 function foregroundNavigation() {
201 navFrame
.classList
.remove('background');
202 navFrame
.firstChild
.tabIndex
= 0;
203 navFrame
.firstChild
.removeAttribute('aria-hidden');
207 * Enables or disables animated transitions when changing content while
208 * horizontally scrolled.
209 * @param {boolean} enabled True if enabled, else false to disable.
211 function setContentChanging(enabled
) {
212 navFrame
.classList
[enabled
? 'add' : 'remove']('changing-content');
215 uber
.invokeMethodOnWindow(navFrame
.firstChild
.contentWindow
,
216 'setContentChanging', enabled
);
221 * Get an iframe based on the origin of a received post message.
222 * @param {string} origin The origin of a post message.
223 * @return {!Element} The frame associated to |origin| or null.
225 function getIframeFromOrigin(origin
) {
226 assert(origin
.substr(-1) != '/', 'invalid origin given');
227 var query
= '.iframe-container > iframe[src^="' + origin
+ '/"]';
228 var element
= document
.querySelector(query
);
230 return /** @type {!Element} */(element
);
234 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)).
235 * @param {Object} state The page's state object for the navigation.
236 * @param {string} path The new /path/ to be set after the page name.
237 * @param {number} historyOption The type of history modification to make.
239 function changePathTo(state
, path
, historyOption
) {
240 assert(!path
|| path
.substr(-1) != '/', 'invalid path given');
243 if (historyOption
== HISTORY_STATE_OPTION
.PUSH
)
244 histFunc
= window
.history
.pushState
;
245 else if (historyOption
== HISTORY_STATE_OPTION
.REPLACE
)
246 histFunc
= window
.history
.replaceState
;
248 assert(histFunc
, 'invalid historyOption given ' + historyOption
);
250 var pageId
= getSelectedIframeContainer().id
;
251 var args
= [state
, '', '/' + pageId
+ '/' + (path
|| '')];
252 histFunc
.apply(window
.history
, args
);
256 * Adds or replaces the current history entry based on a navigation from the
258 * @param {string} origin The origin of the source iframe.
259 * @param {Object} state The source iframe's state object.
260 * @param {string} path The new "path" (e.g. "/createProfile").
261 * @param {boolean} replace Whether to replace the current history entry.
263 function updateHistory(origin
, state
, path
, replace
) {
264 assert(!path
|| path
[0] != '/', 'invalid path sent from ' + origin
);
266 replace
? HISTORY_STATE_OPTION
.REPLACE
: HISTORY_STATE_OPTION
.PUSH
;
267 // Only update the currently displayed path if this is the visible frame.
268 var container
= getIframeFromOrigin(origin
).parentNode
;
269 if (container
== getSelectedIframeContainer())
270 changePathTo(state
, path
, historyOption
);
274 * Sets the title of the page.
275 * @param {string} origin The origin of the source iframe.
276 * @param {string} title The title of the page.
278 function setTitle(origin
, title
) {
279 // Cache the title for the client iframe, i.e., the iframe setting the
280 // title. querySelector returns the actual iframe element, so use parentNode
281 // to get back to the container.
282 var container
= getIframeFromOrigin(origin
).parentNode
;
283 container
.dataset
.title
= title
;
285 // Only update the currently displayed title if this is the visible frame.
286 if (container
== getSelectedIframeContainer())
287 document
.title
= title
;
291 * Invokes a method on a subpage. If the subpage has not signaled readiness,
292 * queue the message for when it does.
293 * @param {string} pageId Should match an id of one of the iframe containers.
294 * @param {string} method The name of the method to invoke.
295 * @param {Object=} opt_params Optional property page of parameters to pass to
296 * the invoked method.
298 function invokeMethodOnPage(pageId
, method
, opt_params
) {
299 var frame
= $(pageId
).querySelector('iframe');
300 if (!frame
|| !frame
.dataset
.ready
) {
301 queuedInvokes
[pageId
] = (queuedInvokes
[pageId
] || []);
302 queuedInvokes
[pageId
].push([method
, opt_params
]);
304 uber
.invokeMethodOnWindow(frame
.contentWindow
, method
, opt_params
);
309 * Called in response to a page declaring readiness. Calls any deferred method
310 * invocations from invokeMethodOnPage.
311 * @param {string} origin The origin of the source iframe.
313 function pageReady(origin
) {
314 var frame
= getIframeFromOrigin(origin
);
315 var container
= frame
.parentNode
;
316 frame
.dataset
.ready
= true;
317 var queue
= queuedInvokes
[container
.id
] || [];
318 queuedInvokes
[container
.id
] = undefined;
319 for (var i
= 0; i
< queue
.length
; i
++) {
320 uber
.invokeMethodOnWindow(frame
.contentWindow
, queue
[i
][0], queue
[i
][1]);
325 * Selects and navigates a subpage. This is called from uber-frame.
326 * @param {string} pageId Should match an id of one of the iframe containers.
327 * @param {number} historyOption Indicates whether we should push or replace
329 * @param {string} path A sub-page path.
331 function showPage(pageId
, historyOption
, path
) {
332 var container
= $(pageId
);
334 // Lazy load of iframe contents.
335 var sourceUrl
= container
.dataset
.url
+ (path
|| '');
336 var frame
= container
.querySelector('iframe');
338 frame
= container
.ownerDocument
.createElement('iframe');
340 frame
.setAttribute('role', 'presentation');
341 container
.appendChild(frame
);
342 frame
.src
= sourceUrl
;
344 // There's no particularly good way to know what the current URL of the
345 // content frame is as we don't have access to its contentWindow's
346 // location, so just replace every time until necessary to do otherwise.
347 frame
.contentWindow
.location
.replace(sourceUrl
);
348 frame
.dataset
.ready
= false;
351 // If the last selected container is already showing, ignore the rest.
352 var lastSelected
= document
.querySelector('.iframe-container.selected');
353 if (lastSelected
=== container
)
357 lastSelected
.classList
.remove('selected');
358 // Setting aria-hidden hides the container from assistive technology
359 // immediately. The 'hidden' attribute is set after the transition
360 // finishes - that ensures it's not possible to accidentally focus
361 // an element in an unselected container.
362 lastSelected
.setAttribute('aria-hidden', 'true');
365 // Containers that aren't selected have to be hidden so that their
366 // content isn't focusable.
367 container
.hidden
= false;
368 container
.setAttribute('aria-hidden', 'false');
370 // Trigger a layout after making it visible and before setting
371 // the class to 'selected', so that it animates in.
372 /** @suppress {uselessCode} */
374 container
.classList
.add('selected');
376 setContentChanging(true);
379 var selectedWindow
= getSelectedIframeWindow();
380 uber
.invokeMethodOnWindow(selectedWindow
, 'frameSelected');
381 selectedWindow
.focus();
383 if (historyOption
!= HISTORY_STATE_OPTION
.NONE
)
384 changePathTo({}, path
, historyOption
);
386 if (container
.dataset
.title
)
387 document
.title
= container
.dataset
.title
;
388 assert('favicon' in container
.dataset
);
390 var dataset
= /** @type {{favicon: string}} */(container
.dataset
);
391 $('favicon').href
= 'chrome://theme/' + dataset
.favicon
;
392 $('favicon2x').href
= 'chrome://theme/' + dataset
.favicon
+ '@2x';
394 updateNavigationControls();
397 function onNavigationControlsLoaded() {
398 updateNavigationControls();
402 * Sends a message to uber-frame to update the appearance of the nav controls.
403 * It should be called whenever the selected iframe changes.
405 function updateNavigationControls() {
406 var container
= getSelectedIframeContainer();
407 uber
.invokeMethodOnWindow(navFrame
.firstChild
.contentWindow
,
408 'changeSelection', {pageId
: container
.id
});
412 * Forwarded scroll offset from a content frame's scroll handler.
413 * @param {number} scrollOffset The scroll offset from the content frame.
415 function adjustToScroll(scrollOffset
) {
416 // NOTE: The scroll is reset to 0 and easing turned on every time a user
417 // switches frames. If we receive a non-zero value it has to have come from
418 // a real user scroll, so we disable easing when this happens.
419 if (scrollOffset
!= 0)
420 setContentChanging(false);
423 uber
.invokeMethodOnWindow(navFrame
.firstChild
.contentWindow
,
426 var navWidth
= Math
.max(0, +navFrame
.dataset
.width
+ scrollOffset
);
427 navFrame
.style
.width
= navWidth
+ 'px';
429 navFrame
.style
.webkitTransform
= 'translateX(' + -scrollOffset
+ 'px)';
434 * Forward scroll wheel events to subpages.
435 * @param {Object} params Relevant parameters of wheel event.
437 function forwardMouseWheel(params
) {
438 uber
.invokeMethodOnWindow(getSelectedIframeWindow(), 'mouseWheel', params
);
441 /** Forward mouse down events to subpages. */
442 function forwardMouseDown() {
443 uber
.invokeMethodOnWindow(getSelectedIframeWindow(), 'mouseDown');
447 * Make sure that iframe containers that are not selected are
448 * hidden, so that elements in those frames aren't part of the
449 * focus order. Containers that are unselected later get hidden
450 * when the transition ends. We also set the aria-hidden attribute
451 * because that hides the container from assistive technology
452 * immediately, rather than only after the transition ends.
454 function ensureNonSelectedFrameContainersAreHidden() {
455 var containers
= document
.querySelectorAll('.iframe-container');
456 for (var i
= 0; i
< containers
.length
; i
++) {
457 var container
= containers
[i
];
458 if (!container
.classList
.contains('selected')) {
459 container
.hidden
= true;
460 container
.setAttribute('aria-hidden', 'true');
462 container
.addEventListener('webkitTransitionEnd', function(event
) {
463 if (!event
.target
.classList
.contains('selected'))
464 event
.target
.hidden
= true;
471 onPopHistoryState
: onPopHistoryState
475 window
.addEventListener('popstate', uber
.onPopHistoryState
);
476 document
.addEventListener('DOMContentLoaded', uber
.onLoad
);