| 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 = {}; |
| 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 |
| 115 // Either way, send the state down to it. |
| 116 // |
| 117 // Note: This assumes that the state and path parameters for every page |
| 118 // under this origin are compatible. All of the downstream pages which |
| 119 // navigate use pushState and replaceState. |
| 120 invokeMethodOnPage(e.state.pageId, 'popState', |
| 121 {state: e.state.pageState, path: params.path}); |
| 122 } |
| 97 } | 123 } |
| 98 | 124 |
| 99 /** | 125 /** |
| 100 * @return {Object} The default iframe container. | 126 * @return {Object} The default iframe container. |
| 101 */ | 127 */ |
| 102 function getDefaultIframe() { | 128 function getDefaultIframe() { |
| 103 return $(loadTimeData.getString('helpHost')); | 129 return $(loadTimeData.getString('helpHost')); |
| 104 } | 130 } |
| 105 | 131 |
| 106 /** | 132 /** |
| (...skipping 16 matching lines...) Expand all Loading... |
| 123 * |method| is required, while |params| is optional. Extra parameters required | 149 * |method| is required, while |params| is optional. Extra parameters required |
| 124 * by a method must be specified by that method's documentation. | 150 * by a method must be specified by that method's documentation. |
| 125 * | 151 * |
| 126 * @param {Event} e The posted object. | 152 * @param {Event} e The posted object. |
| 127 */ | 153 */ |
| 128 function handleWindowMessage(e) { | 154 function handleWindowMessage(e) { |
| 129 if (e.data.method === 'beginInterceptingEvents') { | 155 if (e.data.method === 'beginInterceptingEvents') { |
| 130 backgroundNavigation(); | 156 backgroundNavigation(); |
| 131 } else if (e.data.method === 'stopInterceptingEvents') { | 157 } else if (e.data.method === 'stopInterceptingEvents') { |
| 132 foregroundNavigation(); | 158 foregroundNavigation(); |
| 133 } else if (e.data.method === 'setPath') { | 159 } else if (e.data.method === 'ready') { |
| 134 setPath(e.origin, e.data.params.path); | 160 pageReady(e.origin); |
| 161 } else if (e.data.method === 'updateHistory') { |
| 162 updateHistory(e.origin, e.data.params.state, e.data.params.path, |
| 163 e.data.params.replace); |
| 135 } else if (e.data.method === 'setTitle') { | 164 } else if (e.data.method === 'setTitle') { |
| 136 setTitle(e.origin, e.data.params.title); | 165 setTitle(e.origin, e.data.params.title); |
| 137 } else if (e.data.method === 'showPage') { | 166 } else if (e.data.method === 'showPage') { |
| 138 showPage(e.data.params.pageId, | 167 showPage(e.data.params.pageId, |
| 139 HISTORY_STATE_OPTION.PUSH, | 168 HISTORY_STATE_OPTION.PUSH, |
| 140 e.data.params.path); | 169 e.data.params.path); |
| 141 } else if (e.data.method === 'navigationControlsLoaded') { | 170 } else if (e.data.method === 'navigationControlsLoaded') { |
| 142 onNavigationControlsLoaded(); | 171 onNavigationControlsLoaded(); |
| 143 } else if (e.data.method === 'adjustToScroll') { | 172 } else if (e.data.method === 'adjustToScroll') { |
| 144 adjustToScroll(e.data.params); | 173 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. | 217 * @return {!HTMLElement} The frame associated to |origin| or null. |
| 189 */ | 218 */ |
| 190 function getIframeFromOrigin(origin) { | 219 function getIframeFromOrigin(origin) { |
| 191 assert(origin.substr(-1) != '/', 'invalid origin given'); | 220 assert(origin.substr(-1) != '/', 'invalid origin given'); |
| 192 var query = '.iframe-container > iframe[src^="' + origin + '/"]'; | 221 var query = '.iframe-container > iframe[src^="' + origin + '/"]'; |
| 193 return document.querySelector(query); | 222 return document.querySelector(query); |
| 194 } | 223 } |
| 195 | 224 |
| 196 /** | 225 /** |
| 197 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). | 226 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). |
| 227 * @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. | 228 * @param {string} path The new /path/ to be set after the page name. |
| 199 * @param {number} historyOption The type of history modification to make. | 229 * @param {number} historyOption The type of history modification to make. |
| 200 */ | 230 */ |
| 201 function changePathTo(path, historyOption) { | 231 function changePathTo(state, path, historyOption) { |
| 202 assert(!path || path.substr(-1) != '/', 'invalid path given'); | 232 assert(!path || path.substr(-1) != '/', 'invalid path given'); |
| 203 | 233 |
| 204 var histFunc; | 234 var histFunc; |
| 205 if (historyOption == HISTORY_STATE_OPTION.PUSH) | 235 if (historyOption == HISTORY_STATE_OPTION.PUSH) |
| 206 histFunc = window.history.pushState; | 236 histFunc = window.history.pushState; |
| 207 else if (historyOption == HISTORY_STATE_OPTION.REPLACE) | 237 else if (historyOption == HISTORY_STATE_OPTION.REPLACE) |
| 208 histFunc = window.history.replaceState; | 238 histFunc = window.history.replaceState; |
| 209 | 239 |
| 210 assert(histFunc, 'invalid historyOption given ' + historyOption); | 240 assert(histFunc, 'invalid historyOption given ' + historyOption); |
| 211 | 241 |
| 212 var pageId = getSelectedIframe().id; | 242 var pageId = getSelectedIframe().id; |
| 213 var args = [{pageId: pageId}, '', '/' + pageId + '/' + (path || '')]; | 243 var args = [{pageId: pageId, pageState: state}, |
| 244 '', |
| 245 '/' + pageId + '/' + (path || '')]; |
| 214 histFunc.apply(window.history, args); | 246 histFunc.apply(window.history, args); |
| 215 } | 247 } |
| 216 | 248 |
| 217 /** | 249 /** |
| 218 * Sets the "path" of the page (actually the path after the first '/' char). | 250 * Adds or replaces the current history entry based on a navigation from the |
| 219 * @param {Object} origin The origin of the source iframe. | 251 * source iframe. |
| 220 * @param {string} title The new "path". | 252 * @param {string} origin The origin of the source iframe. |
| 253 * @param {Object} state The source iframe's state object. |
| 254 * @param {string} path The new "path" (e.g. "/createProfile"). |
| 255 * @param {boolean} replace Whether to replace the current history entry. |
| 221 */ | 256 */ |
| 222 function setPath(origin, path) { | 257 function updateHistory(origin, state, path, replace) { |
| 223 assert(!path || path[0] != '/', 'invalid path sent from ' + origin); | 258 assert(!path || path[0] != '/', 'invalid path sent from ' + origin); |
| 259 var historyOption = |
| 260 replace ? HISTORY_STATE_OPTION.REPLACE : HISTORY_STATE_OPTION.PUSH; |
| 224 // Only update the currently displayed path if this is the visible frame. | 261 // Only update the currently displayed path if this is the visible frame. |
| 225 if (getIframeFromOrigin(origin).parentNode == getSelectedIframe()) | 262 if (getIframeFromOrigin(origin).parentNode == getSelectedIframe()) |
| 226 changePathTo(path, HISTORY_STATE_OPTION.REPLACE); | 263 changePathTo(state, path, historyOption); |
| 227 } | 264 } |
| 228 | 265 |
| 229 /** | 266 /** |
| 230 * Sets the title of the page. | 267 * Sets the title of the page. |
| 231 * @param {Object} origin The origin of the source iframe. | 268 * @param {string} origin The origin of the source iframe. |
| 232 * @param {string} title The title of the page. | 269 * @param {string} title The title of the page. |
| 233 */ | 270 */ |
| 234 function setTitle(origin, title) { | 271 function setTitle(origin, title) { |
| 235 // Cache the title for the client iframe, i.e., the iframe setting the | 272 // Cache the title for the client iframe, i.e., the iframe setting the |
| 236 // title. querySelector returns the actual iframe element, so use parentNode | 273 // title. querySelector returns the actual iframe element, so use parentNode |
| 237 // to get back to the container. | 274 // to get back to the container. |
| 238 var container = getIframeFromOrigin(origin).parentNode; | 275 var container = getIframeFromOrigin(origin).parentNode; |
| 239 container.dataset.title = title; | 276 container.dataset.title = title; |
| 240 | 277 |
| 241 // Only update the currently displayed title if this is the visible frame. | 278 // Only update the currently displayed title if this is the visible frame. |
| 242 if (container == getSelectedIframe()) | 279 if (container == getSelectedIframe()) |
| 243 document.title = title; | 280 document.title = title; |
| 244 } | 281 } |
| 245 | 282 |
| 246 /** | 283 /** |
| 247 * Selects a subpage. This is called from uber-frame. | 284 * Invokes a method on a subpage. If the subpage has not signaled readiness, |
| 285 * queue the message for when it does. |
| 286 * @param {string} pageId Should match an id of one of the iframe containers. |
| 287 * @param {string} method The name of the method to invoke. |
| 288 * @param {Object=} opt_params Optional property page of parameters to pass to |
| 289 * the invoked method. |
| 290 */ |
| 291 function invokeMethodOnPage(pageId, method, opt_params) { |
| 292 var frame = $(pageId).querySelector('iframe'); |
| 293 if (!frame || !frame.dataset.ready) { |
| 294 queuedInvokes[pageId] = (queuedInvokes[pageId] || []); |
| 295 queuedInvokes[pageId].push([method, opt_params]); |
| 296 } else { |
| 297 uber.invokeMethodOnWindow(frame.contentWindow, method, opt_params); |
| 298 } |
| 299 } |
| 300 |
| 301 /** |
| 302 * Called in response to a page declaring readiness. Calls any deferred method |
| 303 * invocations from invokeMethodOnPage. |
| 304 * @param {string} origin The origin of the source iframe. |
| 305 */ |
| 306 function pageReady(origin) { |
| 307 var frame = getIframeFromOrigin(origin); |
| 308 var container = frame.parentNode; |
| 309 frame.dataset.ready = true; |
| 310 var queue = queuedInvokes[container.id] || []; |
| 311 queuedInvokes[container.id] = undefined; |
| 312 for (var i = 0; i < queue.length; i++) { |
| 313 uber.invokeMethodOnWindow(frame.contentWindow, queue[i][0], queue[i][1]); |
| 314 } |
| 315 } |
| 316 |
| 317 /** |
| 318 * 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. | 319 * @param {string} pageId Should match an id of one of the iframe containers. |
| 249 * @param {integer} historyOption Indicates whether we should push or replace | 320 * @param {integer} historyOption Indicates whether we should push or replace |
| 250 * browser history. | 321 * browser history. |
| 251 * @param {string} path A sub-page path. | 322 * @param {string} path A sub-page path. |
| 252 */ | 323 */ |
| 253 function showPage(pageId, historyOption, path) { | 324 function showPage(pageId, historyOption, path) { |
| 254 var container = $(pageId); | 325 var container = getRequiredElement(pageId); |
| 255 var lastSelected = document.querySelector('.iframe-container.selected'); | |
| 256 | 326 |
| 257 // Lazy load of iframe contents. | 327 // Lazy load of iframe contents. |
| 258 var sourceUrl = container.dataset.url + (path || ''); | 328 var sourceUrl = container.dataset.url + (path || ''); |
| 259 var frame = container.querySelector('iframe'); | 329 var frame = container.querySelector('iframe'); |
| 260 if (!frame) { | 330 if (!frame) { |
| 261 frame = container.ownerDocument.createElement('iframe'); | 331 frame = container.ownerDocument.createElement('iframe'); |
| 262 frame.name = pageId; | 332 frame.name = pageId; |
| 263 container.appendChild(frame); | 333 container.appendChild(frame); |
| 264 frame.src = sourceUrl; | 334 frame.src = sourceUrl; |
| 265 } else { | 335 } else { |
| 266 // There's no particularly good way to know what the current URL of the | 336 // 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 | 337 // 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. | 338 // location, so just replace every time until necessary to do otherwise. |
| 269 frame.contentWindow.location.replace(sourceUrl); | 339 frame.contentWindow.location.replace(sourceUrl); |
| 340 frame.dataset.ready = false; |
| 270 } | 341 } |
| 271 | 342 |
| 343 selectPage(pageId); |
| 344 |
| 345 if (historyOption != HISTORY_STATE_OPTION.NONE) |
| 346 changePathTo({}, path, historyOption); |
| 347 } |
| 348 |
| 349 /** |
| 350 * Switches to a subpage. The subpage must already exist. |
| 351 * @param {string} pageId Should match an id of one of the iframe containers. |
| 352 */ |
| 353 function selectPage(pageId) { |
| 354 var container = $(pageId); |
| 355 var lastSelected = document.querySelector('.iframe-container.selected'); |
| 356 |
| 272 // If the last selected container is already showing, ignore the rest. | 357 // If the last selected container is already showing, ignore the rest. |
| 273 if (lastSelected === container) | 358 if (lastSelected === container) |
| 274 return; | 359 return; |
| 275 | 360 |
| 276 if (lastSelected) { | 361 if (lastSelected) { |
| 277 lastSelected.classList.remove('selected'); | 362 lastSelected.classList.remove('selected'); |
| 278 // Setting aria-hidden hides the container from assistive technology | 363 // Setting aria-hidden hides the container from assistive technology |
| 279 // immediately. The 'hidden' attribute is set after the transition | 364 // immediately. The 'hidden' attribute is set after the transition |
| 280 // finishes - that ensures it's not possible to accidentally focus | 365 // finishes - that ensures it's not possible to accidentally focus |
| 281 // an element in an unselected container. | 366 // an element in an unselected container. |
| 282 lastSelected.setAttribute('aria-hidden', 'true'); | 367 lastSelected.setAttribute('aria-hidden', 'true'); |
| 283 } | 368 } |
| 284 | 369 |
| 285 // Containers that aren't selected have to be hidden so that their | 370 // Containers that aren't selected have to be hidden so that their |
| 286 // content isn't focusable. | 371 // content isn't focusable. |
| 287 container.hidden = false; | 372 container.hidden = false; |
| 288 container.setAttribute('aria-hidden', 'false'); | 373 container.setAttribute('aria-hidden', 'false'); |
| 289 | 374 |
| 290 // Trigger a layout after making it visible and before setting | 375 // Trigger a layout after making it visible and before setting |
| 291 // the class to 'selected', so that it animates in. | 376 // the class to 'selected', so that it animates in. |
| 292 container.offsetTop; | 377 container.offsetTop; |
| 293 container.classList.add('selected'); | 378 container.classList.add('selected'); |
| 294 | 379 |
| 295 setContentChanging(true); | 380 setContentChanging(true); |
| 296 adjustToScroll(0); | 381 adjustToScroll(0); |
| 297 | 382 |
| 298 var selectedFrame = getSelectedIframe().querySelector('iframe'); | 383 var selectedFrame = getSelectedIframe().querySelector('iframe'); |
| 299 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected'); | 384 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected'); |
| 300 | 385 |
| 301 if (historyOption != HISTORY_STATE_OPTION.NONE) | |
| 302 changePathTo(path, historyOption); | |
| 303 | |
| 304 if (container.dataset.title) | 386 if (container.dataset.title) |
| 305 document.title = container.dataset.title; | 387 document.title = container.dataset.title; |
| 306 $('favicon').href = 'chrome://theme/' + container.dataset.favicon; | 388 $('favicon').href = 'chrome://theme/' + container.dataset.favicon; |
| 307 $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x'; | 389 $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x'; |
| 308 | 390 |
| 309 updateNavigationControls(); | 391 updateNavigationControls(); |
| 310 } | 392 } |
| 311 | 393 |
| 312 function onNavigationControlsLoaded() { | 394 function onNavigationControlsLoaded() { |
| 313 updateNavigationControls(); | 395 updateNavigationControls(); |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 378 } | 460 } |
| 379 | 461 |
| 380 return { | 462 return { |
| 381 onLoad: onLoad, | 463 onLoad: onLoad, |
| 382 onPopHistoryState: onPopHistoryState | 464 onPopHistoryState: onPopHistoryState |
| 383 }; | 465 }; |
| 384 }); | 466 }); |
| 385 | 467 |
| 386 window.addEventListener('popstate', uber.onPopHistoryState); | 468 window.addEventListener('popstate', uber.onPopHistoryState); |
| 387 document.addEventListener('DOMContentLoaded', uber.onLoad); | 469 document.addEventListener('DOMContentLoaded', uber.onLoad); |
| OLD | NEW |