uber.js revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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 5cr.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 49 /** 50 * Find page information from window.location. If the location doesn't 51 * point to one of our pages, return default parameters. 52 * @return {Object} An object containing the following parameters: 53 * id - The 'id' of the page. 54 * path - A path into the page, including search and hash. Optional. 55 */ 56 function resolvePageInfo() { 57 var params = {}; 58 var path = window.location.pathname; 59 if (path.length > 1) { 60 // Split the path into id and the remaining path. 61 path = path.slice(1); 62 var index = path.indexOf('/'); 63 if (index != -1) { 64 params.id = path.slice(0, index); 65 params.path = path.slice(index + 1); 66 } else { 67 params.id = path; 68 } 69 70 var container = $(params.id); 71 if (container) { 72 // The id is valid. Add the hash and search parts of the URL to path. 73 params.path = (params.path || '') + window.location.search + 74 window.location.hash; 75 } else { 76 // The target sub-page does not exist, discard the params we generated. 77 params.id = undefined; 78 params.path = undefined; 79 } 80 } 81 // If we don't have a valid page, get a default. 82 if (!params.id) 83 params.id = getDefaultIframe().id; 84 85 return params; 86 } 87 88 /** 89 * Handler for window.onpopstate. 90 * @param {Event} e The history event. 91 */ 92 function onPopHistoryState(e) { 93 if (e.state && e.state.pageId) 94 showPage(e.state.pageId, HISTORY_STATE_OPTION.NONE); 95 } 96 97 /** 98 * @return {Object} The default iframe container. 99 */ 100 function getDefaultIframe() { 101 return $(loadTimeData.getString('helpHost')); 102 } 103 104 /** 105 * @return {Object} The currently selected iframe container. 106 */ 107 function getSelectedIframe() { 108 return document.querySelector('.iframe-container.selected'); 109 } 110 111 /** 112 * Handles postMessage calls from the iframes of the contained pages. 113 * 114 * The pages request functionality from this object by passing an object of 115 * the following form: 116 * 117 * { method : "methodToInvoke", 118 * params : {...} 119 * } 120 * 121 * |method| is required, while |params| is optional. Extra parameters required 122 * by a method must be specified by that method's documentation. 123 * 124 * @param {Event} e The posted object. 125 */ 126 function handleWindowMessage(e) { 127 if (e.data.method === 'beginInterceptingEvents') 128 backgroundNavigation(); 129 else if (e.data.method === 'stopInterceptingEvents') 130 foregroundNavigation(); 131 else if (e.data.method === 'setPath') 132 setPath(e.origin, e.data.params.path); 133 else if (e.data.method === 'setTitle') 134 setTitle(e.origin, e.data.params.title); 135 else if (e.data.method === 'showPage') 136 showPage(e.data.params.pageId, HISTORY_STATE_OPTION.PUSH); 137 else if (e.data.method === 'navigationControlsLoaded') 138 onNavigationControlsLoaded(); 139 else if (e.data.method === 'adjustToScroll') 140 adjustToScroll(e.data.params); 141 else if (e.data.method === 'mouseWheel') 142 forwardMouseWheel(e.data.params); 143 else 144 console.error('Received unexpected message', e.data); 145 } 146 147 /** 148 * Sends the navigation iframe to the background. 149 */ 150 function backgroundNavigation() { 151 navFrame.classList.add('background'); 152 navFrame.firstChild.tabIndex = -1; 153 navFrame.firstChild.setAttribute('aria-hidden', true); 154 } 155 156 /** 157 * Retrieves the navigation iframe from the background. 158 */ 159 function foregroundNavigation() { 160 navFrame.classList.remove('background'); 161 navFrame.firstChild.tabIndex = 0; 162 navFrame.firstChild.removeAttribute('aria-hidden'); 163 } 164 165 /** 166 * Enables or disables animated transitions when changing content while 167 * horizontally scrolled. 168 * @param {boolean} enabled True if enabled, else false to disable. 169 */ 170 function setContentChanging(enabled) { 171 navFrame.classList[enabled ? 'add' : 'remove']('changing-content'); 172 173 if (isRTL()) { 174 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 175 'setContentChanging', 176 enabled); 177 } 178 } 179 180 /** 181 * Get an iframe based on the origin of a received post message. 182 * @param {string} origin The origin of a post message. 183 * @return {!HTMLElement} The frame associated to |origin| or null. 184 */ 185 function getIframeFromOrigin(origin) { 186 assert(origin.substr(-1) != '/', 'invalid origin given'); 187 var query = '.iframe-container > iframe[src^="' + origin + '/"]'; 188 return document.querySelector(query); 189 } 190 191 /** 192 * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). 193 * @param {string} path The new /path/ to be set after the page name. 194 * @param {number} historyOption The type of history modification to make. 195 */ 196 function changePathTo(path, historyOption) { 197 assert(!path || path.substr(-1) != '/', 'invalid path given'); 198 199 var histFunc; 200 if (historyOption == HISTORY_STATE_OPTION.PUSH) 201 histFunc = window.history.pushState; 202 else if (historyOption == HISTORY_STATE_OPTION.REPLACE) 203 histFunc = window.history.replaceState; 204 205 assert(histFunc, 'invalid historyOption given ' + historyOption); 206 207 var pageId = getSelectedIframe().id; 208 var args = [{pageId: pageId}, '', '/' + pageId + '/' + (path || '')]; 209 histFunc.apply(window.history, args); 210 } 211 212 /** 213 * Sets the "path" of the page (actually the path after the first '/' char). 214 * @param {Object} origin The origin of the source iframe. 215 * @param {string} title The new "path". 216 */ 217 function setPath(origin, path) { 218 assert(!path || path[0] != '/', 'invalid path sent from ' + origin); 219 // Only update the currently displayed path if this is the visible frame. 220 if (getIframeFromOrigin(origin).parentNode == getSelectedIframe()) 221 changePathTo(path, HISTORY_STATE_OPTION.REPLACE); 222 } 223 224 /** 225 * Sets the title of the page. 226 * @param {Object} origin The origin of the source iframe. 227 * @param {string} title The title of the page. 228 */ 229 function setTitle(origin, title) { 230 // Cache the title for the client iframe, i.e., the iframe setting the 231 // title. querySelector returns the actual iframe element, so use parentNode 232 // to get back to the container. 233 var container = getIframeFromOrigin(origin).parentNode; 234 container.dataset.title = title; 235 236 // Only update the currently displayed title if this is the visible frame. 237 if (container == getSelectedIframe()) 238 document.title = title; 239 } 240 241 /** 242 * Selects a subpage. This is called from uber-frame. 243 * @param {string} pageId Should matche an id of one of the iframe containers. 244 * @param {integer} historyOption Indicates whether we should push or replace 245 * browser history. 246 * @param {string} path A sub-page path. 247 */ 248 function showPage(pageId, historyOption, path) { 249 var container = $(pageId); 250 var lastSelected = document.querySelector('.iframe-container.selected'); 251 252 // Lazy load of iframe contents. 253 var sourceUrl = container.dataset.url + (path || ''); 254 var frame = container.querySelector('iframe'); 255 if (!frame) { 256 frame = container.ownerDocument.createElement('iframe'); 257 container.appendChild(frame); 258 frame.src = sourceUrl; 259 } else { 260 // There's no particularly good way to know what the current URL of the 261 // content frame is as we don't have access to its contentWindow's 262 // location, so just replace every time until necessary to do otherwise. 263 frame.contentWindow.location.replace(sourceUrl); 264 } 265 266 // If the last selected container is already showing, ignore the rest. 267 if (lastSelected === container) 268 return; 269 270 if (lastSelected) 271 lastSelected.classList.remove('selected'); 272 container.classList.add('selected'); 273 274 setContentChanging(true); 275 adjustToScroll(0); 276 277 var selectedFrame = getSelectedIframe().querySelector('iframe'); 278 uber.invokeMethodOnWindow(selectedFrame.contentWindow, 'frameSelected'); 279 280 if (historyOption != HISTORY_STATE_OPTION.NONE) 281 changePathTo(path, historyOption); 282 283 if (container.dataset.title) 284 document.title = container.dataset.title; 285 $('favicon').href = 'chrome://theme/' + container.dataset.favicon; 286 $('favicon2x').href = 'chrome://theme/' + container.dataset.favicon + '@2x'; 287 288 updateNavigationControls(); 289 } 290 291 function onNavigationControlsLoaded() { 292 updateNavigationControls(); 293 } 294 295 /** 296 * Sends a message to uber-frame to update the appearance of the nav controls. 297 * It should be called whenever the selected iframe changes. 298 */ 299 function updateNavigationControls() { 300 var iframe = getSelectedIframe(); 301 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 302 'changeSelection', {pageId: iframe.id}); 303 } 304 305 /** 306 * Forwarded scroll offset from a content frame's scroll handler. 307 * @param {number} scrollOffset The scroll offset from the content frame. 308 */ 309 function adjustToScroll(scrollOffset) { 310 // NOTE: The scroll is reset to 0 and easing turned on every time a user 311 // switches frames. If we receive a non-zero value it has to have come from 312 // a real user scroll, so we disable easing when this happens. 313 if (scrollOffset != 0) 314 setContentChanging(false); 315 316 if (isRTL()) { 317 uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 318 'adjustToScroll', 319 scrollOffset); 320 var navWidth = Math.max(0, +navFrame.dataset.width + scrollOffset); 321 navFrame.style.width = navWidth + 'px'; 322 } else { 323 navFrame.style.webkitTransform = 'translateX(' + -scrollOffset + 'px)'; 324 } 325 } 326 327 /** 328 * Forward scroll wheel events to subpages. 329 * @param {Object} params Relevant parameters of wheel event. 330 */ 331 function forwardMouseWheel(params) { 332 var iframe = getSelectedIframe().querySelector('iframe'); 333 uber.invokeMethodOnWindow(iframe.contentWindow, 'mouseWheel', params); 334 } 335 336 return { 337 onLoad: onLoad, 338 onPopHistoryState: onPopHistoryState 339 }; 340}); 341 342window.addEventListener('popstate', uber.onPopHistoryState); 343document.addEventListener('DOMContentLoaded', uber.onLoad); 344