OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 cr.define('uber', function() { | 5 cr.define('uber', function() { |
6 /** | 6 /** |
7 * Options for how web history should be handled. | 7 * Options for how web history should be handled. |
8 */ | 8 */ |
9 var HISTORY_STATE_OPTION = { | 9 var HISTORY_STATE_OPTION = { |
10 PUSH: 1, // Push a new history state. | 10 PUSH: 1, // Push a new history state. |
11 REPLACE: 2, // Replace the current history state. | 11 REPLACE: 2, // Replace the current history state. |
12 NONE: 3, // Ignore this history state change. | 12 NONE: 3, // Ignore this history state change. |
13 }; | 13 }; |
14 | 14 |
15 /** | 15 /** |
16 * We cache a reference to the #navigation frame here so we don't need to grab | 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. | 17 * it from the DOM on each scroll. |
18 * @type {Node} | 18 * @type {Node} |
19 * @private | 19 * @private |
20 */ | 20 */ |
21 var navFrame; | 21 var navFrame; |
22 | 22 |
23 /** | 23 /** |
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 | |
26 * it is ready. | |
27 * @type {Object} | |
28 * @private | |
29 */ | |
30 var queuedInvokes = {}; | |
davidben
2014/05/19 22:38:33
This may not be strictly necessary? The only page
| |
31 | |
32 /** | |
24 * Handles page initialization. | 33 * Handles page initialization. |
25 */ | 34 */ |
26 function onLoad(e) { | 35 function onLoad(e) { |
27 navFrame = $('navigation'); | 36 navFrame = $('navigation'); |
28 navFrame.dataset.width = navFrame.offsetWidth; | 37 navFrame.dataset.width = navFrame.offsetWidth; |
29 | 38 |
30 // Select a page based on the page-URL. | 39 // Select a page based on the page-URL. |
31 var params = resolvePageInfo(); | 40 var params = resolvePageInfo(); |
32 showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path); | 41 showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path); |
33 | 42 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
85 params.id = getDefaultIframe().id; | 94 params.id = getDefaultIframe().id; |
86 | 95 |
87 return params; | 96 return params; |
88 } | 97 } |
89 | 98 |
90 /** | 99 /** |
91 * Handler for window.onpopstate. | 100 * Handler for window.onpopstate. |
92 * @param {Event} e The history event. | 101 * @param {Event} e The history event. |
93 */ | 102 */ |
94 function onPopHistoryState(e) { | 103 function onPopHistoryState(e) { |
95 if (e.state && e.state.pageId) | 104 if (e.state && e.state.pageId) { |
96 showPage(e.state.pageId, HISTORY_STATE_OPTION.NONE); | 105 var params = resolvePageInfo(); |
106 assert(params.id === e.state.pageId); | |
107 | |
108 // If the page doesn't exist, create it. Otherwise, swap it in. | |
109 var frame = $(params.id).querySelector('iframe'); | |
110 if (!frame) { | |
111 showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path); | |
112 } else { | |
113 selectPage(params.id); | |
114 } | |
Dan Beam
2014/05/19 23:06:49
nit: no curlies
davidben
2014/05/19 23:32:28
Done.
| |
115 | |
116 // Either way, send the state down to it. | |
117 // | |
118 // Note: This assumes that the state and path parameters for every page | |
119 // under this origin are compatible. All of the downstream pages which | |
120 // navigate use pushState and replaceState. | |
121 invokeMethodOnPage(e.state.pageId, 'popState', | |
122 {state: e.state.pageState, path: params.path}); | |
123 } | |
97 } | 124 } |
98 | 125 |
99 /** | 126 /** |
100 * @return {Object} The default iframe container. | 127 * @return {Object} The default iframe container. |
101 */ | 128 */ |
102 function getDefaultIframe() { | 129 function getDefaultIframe() { |
103 return $(loadTimeData.getString('helpHost')); | 130 return $(loadTimeData.getString('helpHost')); |
104 } | 131 } |
105 | 132 |
106 /** | 133 /** |
(...skipping 16 matching lines...) Expand all Loading... | |
123 * |method| is required, while |params| is optional. Extra parameters required | 150 * |method| is required, while |params| is optional. Extra parameters required |
124 * by a method must be specified by that method's documentation. | 151 * by a method must be specified by that method's documentation. |
125 * | 152 * |
126 * @param {Event} e The posted object. | 153 * @param {Event} e The posted object. |
127 */ | 154 */ |
128 function handleWindowMessage(e) { | 155 function handleWindowMessage(e) { |
129 if (e.data.method === 'beginInterceptingEvents') { | 156 if (e.data.method === 'beginInterceptingEvents') { |
130 backgroundNavigation(); | 157 backgroundNavigation(); |
131 } else if (e.data.method === 'stopInterceptingEvents') { | 158 } else if (e.data.method === 'stopInterceptingEvents') { |
132 foregroundNavigation(); | 159 foregroundNavigation(); |
133 } else if (e.data.method === 'setPath') { | 160 } else if (e.data.method === 'ready') { |
134 setPath(e.origin, e.data.params.path); | 161 pageReady(e.origin); |
162 } else if (e.data.method === 'updateHistory') { | |
163 updateHistory(e.origin, e.data.params.state, e.data.params.path, | |
164 e.data.params.replace); | |
135 } else if (e.data.method === 'setTitle') { | 165 } else if (e.data.method === 'setTitle') { |
136 setTitle(e.origin, e.data.params.title); | 166 setTitle(e.origin, e.data.params.title); |
137 } else if (e.data.method === 'showPage') { | 167 } else if (e.data.method === 'showPage') { |
138 showPage(e.data.params.pageId, | 168 showPage(e.data.params.pageId, |
139 HISTORY_STATE_OPTION.PUSH, | 169 HISTORY_STATE_OPTION.PUSH, |
140 e.data.params.path); | 170 e.data.params.path); |
141 } else if (e.data.method === 'navigationControlsLoaded') { | 171 } else if (e.data.method === 'navigationControlsLoaded') { |
142 onNavigationControlsLoaded(); | 172 onNavigationControlsLoaded(); |
143 } else if (e.data.method === 'adjustToScroll') { | 173 } else if (e.data.method === 'adjustToScroll') { |
144 adjustToScroll(e.data.params); | 174 adjustToScroll(e.data.params); |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
188 * @return {!HTMLElement} The frame associated to |origin| or null. | 218 * @return {!HTMLElement} The frame associated to |origin| or null. |
189 */ | 219 */ |
190 function getIframeFromOrigin(origin) { | 220 function getIframeFromOrigin(origin) { |
191 assert(origin.substr(-1) != '/', 'invalid origin given'); | 221 assert(origin.substr(-1) != '/', 'invalid origin given'); |
192 var query = '.iframe-container > iframe[src^="' + origin + '/"]'; | 222 var query = '.iframe-container > iframe[src^="' + origin + '/"]'; |
193 return document.querySelector(query); | 223 return document.querySelector(query); |
194 } | 224 } |
195 | 225 |
196 /** | 226 /** |
197 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). | 227 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). |
228 * @param {Object} state The page's state object for the navigation. | |
198 * @param {string} path The new /path/ to be set after the page name. | 229 * @param {string} path The new /path/ to be set after the page name. |
199 * @param {number} historyOption The type of history modification to make. | 230 * @param {number} historyOption The type of history modification to make. |
200 */ | 231 */ |
201 function changePathTo(path, historyOption) { | 232 function changePathTo(state, path, historyOption) { |
202 assert(!path || path.substr(-1) != '/', 'invalid path given'); | 233 assert(!path || path.substr(-1) != '/', 'invalid path given'); |
203 | 234 |
204 var histFunc; | 235 var histFunc; |
205 if (historyOption == HISTORY_STATE_OPTION.PUSH) | 236 if (historyOption == HISTORY_STATE_OPTION.PUSH) |
206 histFunc = window.history.pushState; | 237 histFunc = window.history.pushState; |
207 else if (historyOption == HISTORY_STATE_OPTION.REPLACE) | 238 else if (historyOption == HISTORY_STATE_OPTION.REPLACE) |
208 histFunc = window.history.replaceState; | 239 histFunc = window.history.replaceState; |
209 | 240 |
210 assert(histFunc, 'invalid historyOption given ' + historyOption); | 241 assert(histFunc, 'invalid historyOption given ' + historyOption); |
211 | 242 |
212 var pageId = getSelectedIframe().id; | 243 var pageId = getSelectedIframe().id; |
213 var args = [{pageId: pageId}, '', '/' + pageId + '/' + (path || '')]; | 244 var args = [{pageId: pageId, pageState: state}, |
245 '', | |
246 '/' + pageId + '/' + (path || '')]; | |
214 histFunc.apply(window.history, args); | 247 histFunc.apply(window.history, args); |
215 } | 248 } |
216 | 249 |
217 /** | 250 /** |
218 * Sets the "path" of the page (actually the path after the first '/' char). | 251 * Adds or replaces the current history entry based on a navigation from the |
219 * @param {Object} origin The origin of the source iframe. | 252 * source iframe. |
220 * @param {string} title The new "path". | 253 * @param {string} origin The origin of the source iframe. |
254 * @param {Object} state The source iframe's state object. | |
255 * @param {string} path The new "path". | |
Dan Beam
2014/05/19 23:06:49
@param {string} path The new "path" (e.g. "/create
davidben
2014/05/19 23:32:28
Done.
| |
256 * @param {boolean} replace Whether to replace the current history entry. | |
221 */ | 257 */ |
222 function setPath(origin, path) { | 258 function updateHistory(origin, state, path, replace) { |
223 assert(!path || path[0] != '/', 'invalid path sent from ' + origin); | 259 assert(!path || path[0] != '/', 'invalid path sent from ' + origin); |
260 var historyOption = | |
261 replace ? HISTORY_STATE_OPTION.REPLACE : HISTORY_STATE_OPTION.PUSH; | |
Dan Beam
2014/05/19 23:06:49
nit: 4 \s indent
davidben
2014/05/19 23:32:28
Done.
| |
224 // Only update the currently displayed path if this is the visible frame. | 262 // Only update the currently displayed path if this is the visible frame. |
225 if (getIframeFromOrigin(origin).parentNode == getSelectedIframe()) | 263 if (getIframeFromOrigin(origin).parentNode == getSelectedIframe()) { |
226 changePathTo(path, HISTORY_STATE_OPTION.REPLACE); | 264 changePathTo(state, path, historyOption); |
265 } | |
Dan Beam
2014/05/19 23:06:49
nit: no curlies
davidben
2014/05/19 23:32:28
Done.
| |
227 } | 266 } |
228 | 267 |
229 /** | 268 /** |
230 * Sets the title of the page. | 269 * Sets the title of the page. |
231 * @param {Object} origin The origin of the source iframe. | 270 * @param {string} origin The origin of the source iframe. |
232 * @param {string} title The title of the page. | 271 * @param {string} title The title of the page. |
233 */ | 272 */ |
234 function setTitle(origin, title) { | 273 function setTitle(origin, title) { |
235 // Cache the title for the client iframe, i.e., the iframe setting the | 274 // Cache the title for the client iframe, i.e., the iframe setting the |
236 // title. querySelector returns the actual iframe element, so use parentNode | 275 // title. querySelector returns the actual iframe element, so use parentNode |
237 // to get back to the container. | 276 // to get back to the container. |
238 var container = getIframeFromOrigin(origin).parentNode; | 277 var container = getIframeFromOrigin(origin).parentNode; |
239 container.dataset.title = title; | 278 container.dataset.title = title; |
240 | 279 |
241 // Only update the currently displayed title if this is the visible frame. | 280 // Only update the currently displayed title if this is the visible frame. |
242 if (container == getSelectedIframe()) | 281 if (container == getSelectedIframe()) |
243 document.title = title; | 282 document.title = title; |
244 } | 283 } |
245 | 284 |
246 /** | 285 /** |
247 * Selects a subpage. This is called from uber-frame. | 286 * Invokes a method on a subpage. If the subpage has not signaled readiness, |
287 * queue the message for when it does. | |
288 * @param {string} pageId Should match an id of one of the iframe containers. | |
289 * @param {string} method The name of the method to invoke. | |
290 * @param {Object=} opt_params Optional property page of parameters to pass to | |
291 * the invoked method. | |
292 */ | |
293 function invokeMethodOnPage(pageId, method, opt_params) { | |
294 var frame = $(pageId).querySelector('iframe'); | |
295 if (!frame || !frame.dataset.ready) { | |
296 queuedInvokes[pageId] = (queuedInvokes[pageId] || []); | |
297 queuedInvokes[pageId].push([method, opt_params]); | |
298 } else { | |
299 uber.invokeMethodOnWindow(frame.contentWindow, method, opt_params); | |
300 } | |
301 } | |
302 | |
303 /** | |
304 * Called in response to a page declaring readiness. Calls any deferred method | |
305 * invocations from invokeMethodOnPage. | |
306 * @param {string} origin The origin of the source iframe. | |
307 */ | |
308 function pageReady(origin) { | |
309 var frame = getIframeFromOrigin(origin); | |
310 var container = frame.parentNode; | |
311 frame.dataset.ready = true; | |
312 var queue = queuedInvokes[container.id] || []; | |
313 delete queuedInvokes[container.id]; | |
Dan Beam
2014/05/19 23:06:49
arguably this should be
queuedInvokes[container
davidben
2014/05/19 23:32:28
Done.
| |
314 for (var i = 0; i < queue.length; i++) { | |
315 uber.invokeMethodOnWindow(frame.contentWindow, queue[i][0], queue[i][1]); | |
316 } | |
317 } | |
318 | |
319 /** | |
320 * Selects and navigates a subpage. This is called from uber-frame. | |
248 * @param {string} pageId Should match an id of one of the iframe containers. | 321 * @param {string} pageId Should match an id of one of the iframe containers. |
249 * @param {integer} historyOption Indicates whether we should push or replace | 322 * @param {integer} historyOption Indicates whether we should push or replace |
250 * browser history. | 323 * browser history. |
251 * @param {string} path A sub-page path. | 324 * @param {string} path A sub-page path. |
252 */ | 325 */ |
253 function showPage(pageId, historyOption, path) { | 326 function showPage(pageId, historyOption, path) { |
254 var container = $(pageId); | 327 var container = $(pageId); |
255 var lastSelected = document.querySelector('.iframe-container.selected'); | |
256 | 328 |
257 // Lazy load of iframe contents. | 329 // Lazy load of iframe contents. |
258 var sourceUrl = container.dataset.url + (path || ''); | 330 var sourceUrl = container.dataset.url + (path || ''); |
259 var frame = container.querySelector('iframe'); | 331 var frame = container.querySelector('iframe'); |
260 if (!frame) { | 332 if (!frame) { |
261 frame = container.ownerDocument.createElement('iframe'); | 333 frame = container.ownerDocument.createElement('iframe'); |
262 frame.name = pageId; | 334 frame.name = pageId; |
263 container.appendChild(frame); | 335 container.appendChild(frame); |
264 frame.src = sourceUrl; | 336 frame.src = sourceUrl; |
265 } else { | 337 } else { |
266 // There's no particularly good way to know what the current URL of the | 338 // There's no particularly good way to know what the current URL of the |
267 // content frame is as we don't have access to its contentWindow's | 339 // content frame is as we don't have access to its contentWindow's |
268 // location, so just replace every time until necessary to do otherwise. | 340 // location, so just replace every time until necessary to do otherwise. |
269 frame.contentWindow.location.replace(sourceUrl); | 341 frame.contentWindow.location.replace(sourceUrl); |
342 frame.dataset.ready = false; | |
270 } | 343 } |
271 | 344 |
345 selectPage(pageId); | |
346 | |
347 if (historyOption != HISTORY_STATE_OPTION.NONE) | |
348 changePathTo({}, path, historyOption); | |
349 } | |
350 | |
351 /** | |
352 * Switches to a subpage. The subpage must already exist. | |
353 * @param {string} pageId Should match an id of one of the iframe containers. | |
354 */ | |
355 function selectPage(pageId) { | |
356 var container = $(pageId); | |
Dan Beam
2014/05/19 23:06:49
nit: $ -> getRequiredElement
davidben
2014/05/19 23:32:28
Done.
Dan Beam
2014/05/19 23:49:10
I think you did this on L325 instead of L354
davidben
2014/05/20 00:12:55
Oops, so I did. Done.
| |
357 var lastSelected = document.querySelector('.iframe-container.selected'); | |
358 | |
272 // If the last selected container is already showing, ignore the rest. | 359 // If the last selected container is already showing, ignore the rest. |
273 if (lastSelected === container) | 360 if (lastSelected === container) |
274 return; | 361 return; |
275 | 362 |
276 if (lastSelected) { | 363 if (lastSelected) { |
277 lastSelected.classList.remove('selected'); | 364 lastSelected.classList.remove('selected'); |
278 // Setting aria-hidden hides the container from assistive technology | 365 // Setting aria-hidden hides the container from assistive technology |
279 // immediately. The 'hidden' attribute is set after the transition | 366 // immediately. The 'hidden' attribute is set after the transition |
280 // finishes - that ensures it's not possible to accidentally focus | 367 // finishes - that ensures it's not possible to accidentally focus |
281 // an element in an unselected container. | 368 // an element in an unselected container. |
282 lastSelected.setAttribute('aria-hidden', 'true'); | 369 lastSelected.setAttribute('aria-hidden', 'true'); |
283 } | 370 } |
284 | 371 |
285 // Containers that aren't selected have to be hidden so that their | 372 // Containers that aren't selected have to be hidden so that their |
286 // content isn't focusable. | 373 // content isn't focusable. |
287 container.hidden = false; | 374 container.hidden = false; |
288 container.setAttribute('aria-hidden', 'false'); | 375 container.setAttribute('aria-hidden', 'false'); |
289 | 376 |
290 // Trigger a layout after making it visible and before setting | 377 // Trigger a layout after making it visible and before setting |
291 // the class to 'selected', so that it animates in. | 378 // the class to 'selected', so that it animates in. |
292 container.offsetTop; | 379 container.offsetTop; |
293 container.classList.add('selected'); | 380 container.classList.add('selected'); |
294 | 381 |
295 setContentChanging(true); | 382 setContentChanging(true); |
296 adjustToScroll(0); | 383 adjustToScroll(0); |
297 | 384 |
298 var selectedFrame = getSelectedIframe().querySelector('iframe'); | 385 var selectedFrame = getSelectedIframe().querySelector('iframe'); |
299 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected'); | 386 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected'); |
300 | 387 |
301 if (historyOption != HISTORY_STATE_OPTION.NONE) | |
302 changePathTo(path, historyOption); | |
303 | |
304 if (container.dataset.title) | 388 if (container.dataset.title) |
305 document.title = container.dataset.title; | 389 document.title = container.dataset.title; |
306 $('favicon').href = 'chrome://theme/' + container.dataset.favicon; | 390 $('favicon').href = 'chrome://theme/' + container.dataset.favicon; |
307 $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x'; | 391 $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x'; |
308 | 392 |
309 updateNavigationControls(); | 393 updateNavigationControls(); |
310 } | 394 } |
311 | 395 |
312 function onNavigationControlsLoaded() { | 396 function onNavigationControlsLoaded() { |
313 updateNavigationControls(); | 397 updateNavigationControls(); |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
378 } | 462 } |
379 | 463 |
380 return { | 464 return { |
381 onLoad: onLoad, | 465 onLoad: onLoad, |
382 onPopHistoryState: onPopHistoryState | 466 onPopHistoryState: onPopHistoryState |
383 }; | 467 }; |
384 }); | 468 }); |
385 | 469 |
386 window.addEventListener('popstate', uber.onPopHistoryState); | 470 window.addEventListener('popstate', uber.onPopHistoryState); |
387 document.addEventListener('DOMContentLoaded', uber.onLoad); | 471 document.addEventListener('DOMContentLoaded', uber.onLoad); |
OLD | NEW |