Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2011 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 | |
| 5 /** | |
| 6 * @fileoverview Touch-based new tab page | |
| 7 * This is the main code for the new tab page used by touch-enabled Chrome | |
| 8 * browsers. For now this is still a prototype. | |
| 9 */ | |
| 10 | |
| 11 /** | |
| 12 * The Slider object to use for changing app pages. | |
| 13 * @type {Slider|undefined} | |
| 14 */ | |
| 15 var slider; | |
| 16 | |
| 17 /** | |
| 18 * Template to use for creating new 'apps-page' elements | |
| 19 * @type {!Element|undefined} | |
| 20 */ | |
| 21 var appsPageTemplate; | |
| 22 | |
| 23 /** | |
| 24 * Template to use for creating new 'app-container' elements | |
| 25 * @type {!Element|undefined} | |
| 26 */ | |
| 27 var appTemplate; | |
| 28 | |
| 29 /** | |
| 30 * Template to use for creating new 'dot' elements | |
| 31 * @type {!Element|undefined} | |
| 32 */ | |
| 33 var dotTemplate; | |
| 34 | |
| 35 /** | |
| 36 * The 'apps-page-list' element. | |
| 37 * @type {!Element} | |
| 38 */ | |
| 39 var appsPageList = getRequiredElement('apps-page-list'); | |
| 40 | |
| 41 /** | |
| 42 * A list of all 'apps-page' elements. | |
| 43 * @type {!NodeList|undefined} | |
| 44 */ | |
| 45 var appsPages; | |
| 46 | |
| 47 /** | |
| 48 * The 'dots-list' element. | |
| 49 * @type {!Element} | |
| 50 */ | |
| 51 var dotList = getRequiredElement('dot-list'); | |
| 52 | |
| 53 /** | |
| 54 * A list of all 'dots' elements. | |
| 55 * @type {!NodeList|undefined} | |
| 56 */ | |
| 57 var dots; | |
| 58 | |
| 59 /** | |
| 60 * The 'trash' element. Note that technically this is unnecessary, | |
| 61 * JavaScript creates the object for us based on the id. But I don't want | |
| 62 * to rely on the ID being the same, and JSCompiler doesn't know about it. | |
| 63 * @type {!Element} | |
| 64 */ | |
| 65 var trash = getRequiredElement('trash'); | |
| 66 | |
| 67 /** | |
| 68 * The time in milliseconds for most transitions. This should match what's | |
| 69 * in newtab.css. Unfortunately there's no better way to try to time | |
| 70 * something to occur until after a transition has completed. | |
| 71 * @type {number} | |
| 72 * @const | |
| 73 */ | |
| 74 var DEFAULT_TRANSITION_TIME = 500; | |
| 75 | |
| 76 /** | |
| 77 * All the Grabber objects currently in use on the page | |
| 78 * @type {Array.<Grabber>} | |
| 79 */ | |
| 80 var grabbers = []; | |
| 81 | |
| 82 /** | |
| 83 * Holds all event handlers tied to apps (and so subject to removal when the | |
| 84 * app list is refreshed) | |
| 85 * @type {!EventTracker} | |
| 86 */ | |
| 87 var appEvents = new EventTracker(); | |
| 88 | |
| 89 /** | |
| 90 * Invoked at startup once the DOM is available to initialize the app. | |
| 91 */ | |
| 92 function initializeNtp() { | |
| 93 // Prevent touch events from triggering any sort of native scrolling | |
| 94 document.addEventListener('touchmove', function(e) { | |
| 95 e.preventDefault(); | |
| 96 }, true); | |
| 97 | |
| 98 // Get the template elements and remove them from the DOM. Things are | |
| 99 // simpler if we start with 0 pages and 0 apps and don't leave hidden | |
| 100 // template elements behind in the DOM. | |
| 101 appTemplate = getRequiredElement('app-template'); | |
| 102 appTemplate.removeAttribute('id'); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
I don't really see a need to remove the attribute
Rick Byers
2011/03/15 21:47:54
I just noticed that it left empty 'id' attributes
| |
| 103 appTemplate.id = ''; | |
| 104 | |
| 105 appsPages = appsPageList.getElementsByClassName('apps-page'); | |
| 106 assert(appsPages.length == 1, | |
| 107 'Expected exactly one apps-page in the apps-page-list.'); | |
| 108 appsPageTemplate = appsPages[0]; | |
| 109 appsPageList.removeChild(appsPages[0]); | |
| 110 | |
| 111 dots = dotList.getElementsByClassName('dot'); | |
| 112 assert(dots.length == 1, | |
| 113 'Expected exactly one dot in the dots-list.'); | |
| 114 dotTemplate = dots[0]; | |
| 115 dotList.removeChild(dots[0]); | |
| 116 | |
| 117 // Initialize the slider without any cards at the moment | |
| 118 var appsFrame = getRequiredElement('apps-frame'); | |
| 119 slider = new Slider(appsFrame, appsPageList, [], 0, appsFrame.offsetWidth); | |
| 120 slider.initialize(); | |
| 121 | |
| 122 // Ensure the slider is resized appropriately with the window | |
| 123 window.addEventListener('resize', function() { | |
| 124 slider.resize(appsFrame.offsetWidth); | |
| 125 }, false); | |
| 126 | |
| 127 // Handle the page being changed | |
| 128 appsPageList.addEventListener( | |
| 129 Slider.EventType.CARD_CHANGED, | |
| 130 function(e) { | |
| 131 // Update the active dot | |
| 132 var curDot = dotList.getElementsByClassName('selected')[0]; | |
| 133 if (curDot) | |
| 134 curDot.classList.remove('selected'); | |
| 135 var newPageIndex = e.sender.getCurrentCard(); | |
| 136 dots[newPageIndex].classList.add('selected'); | |
| 137 // If an app was being dragged, move it to the end of the new page | |
| 138 if (draggingAppContainer) | |
| 139 appsPages[newPageIndex].appendChild(draggingAppContainer); | |
| 140 }, false); | |
| 141 | |
| 142 // Add a drag handler to the body (for drags that don't land on an existing | |
| 143 // app) | |
| 144 document.body.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter, | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
document.addEventListener also works
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 145 false); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
you can skip optional false
Rick Byers
2011/03/15 21:47:54
Cool. As an aside, is the only way for me to know
arv (Not doing code reviews)
2011/03/15 23:53:43
The WebKit DOM bindings does type coercion. In JS,
| |
| 146 | |
| 147 // Handle dropping an app anywhere other than on the trash | |
| 148 document.body.addEventListener(Grabber.EventType.DROP, appDrop, | |
| 149 false); | |
| 150 | |
| 151 // Add handles to manage the transition into/out-of rearrange mode | |
| 152 // Note that we assume here that we only use a Grabber for moving apps, | |
| 153 // so ANY GRAB event means we're enterring rearrange mode. | |
| 154 appsFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode, | |
| 155 false); | |
| 156 appsFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode, | |
| 157 false); | |
| 158 | |
| 159 // Add handlers for the tash can | |
| 160 trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) { | |
| 161 trash.classList.add('hover'); | |
| 162 e.sender.element.classList.add('trashing'); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
Yeah, this is what I hate with working with non Ch
Rick Byers
2011/03/15 21:47:54
Nope, that would be wrong (counfuses the source an
| |
| 163 e.stopPropagation(); | |
| 164 }, false); | |
| 165 trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) { | |
| 166 e.sender.element.classList.remove('trashing'); | |
| 167 trash.classList.remove('hover'); | |
| 168 }, false); | |
| 169 trash.addEventListener(Grabber.EventType.DROP, appTrash, false); | |
| 170 | |
| 171 // Fill in the apps | |
| 172 chrome.send('getApps'); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
Call this earlier for faster NTP
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 173 } | |
| 174 | |
| 175 /** | |
| 176 * Simple common assertion API | |
| 177 * @param {*} condition The condition to test. Note that this may be used to | |
| 178 * test whether a value is defined or not, and we don't want to force a | |
| 179 * cast to Boolean. | |
| 180 * @param {string=} opt_message A message to use in any error. | |
| 181 */ | |
| 182 function assert(condition, opt_message) { | |
| 183 if (!condition) { | |
| 184 var msg = 'Assertion failed'; | |
| 185 if (opt_message) | |
| 186 msg = msg + ': ' + opt_message; | |
| 187 throw new Error(msg); | |
| 188 } | |
| 189 } | |
| 190 | |
| 191 /** | |
| 192 * Get an element that's known to exist by its ID. We use this instead of just | |
| 193 * calling getElementById and not checking the result because this lets us | |
| 194 * satisfy the JSCompiler type system. | |
| 195 * @param {string} id The identifier name. | |
| 196 * @return {!Element} the Element. | |
| 197 */ | |
| 198 function getRequiredElement(id) | |
| 199 { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
{ after new line
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 200 var element = /** @type !Element */ (document.getElementById(id)); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
Oh noes... type casting is a plague that I loathed
Rick Byers
2011/03/15 21:47:54
You were right that I was being sloppy - I fixed i
| |
| 201 assert(Boolean(element), 'Missing required element: ' + id); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
no need to convert to boolean here
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 202 return element; | |
| 203 } | |
| 204 | |
| 205 /** | |
| 206 * Remove all children of an element which have a given class in | |
| 207 * their classList. | |
| 208 * @param {!Element} element The parent element to examine. | |
| 209 * @param {string} className The class to look for. | |
| 210 */ | |
| 211 function removeChildrenByClassName(element, className) { | |
| 212 for (var child = element.firstElementChild; child;) { | |
| 213 var prev = child; | |
| 214 child = child.nextElementSibling; | |
| 215 if (prev.classList.contains(className)) | |
| 216 element.removeChild(prev); | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 /** | |
| 221 * Callback invoked by chrome with the apps available. | |
| 222 * | |
| 223 * Note that calls to this function can occur at any time, not just in response | |
| 224 * to a getApps request. For example, when a user installs/uninstalls an app on | |
| 225 * another synchronized devices. | |
| 226 * @param {Object} data An object with all the data on available | |
| 227 * applications. | |
| 228 */ | |
| 229 function getAppsCallback(data) | |
| 230 { | |
| 231 // Clean up any existing grabber objects - cancelling any outstanding drag. | |
| 232 // Ideally an async app update wouldn't disrupt an active drag but | |
| 233 // that would require us to re-use existing elements and detect how the apps | |
| 234 // have changed, which would be a lot of work. | |
| 235 // Note that was have to explicitly clean up the grabber objects so they stop | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
typo
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 236 // listening to events and break the DOM<->JS cycles necessary to enable | |
| 237 // collection of all these objects. | |
| 238 grabbers.forEach(function(g) { | |
| 239 // Note that this may raise DRAG_END/RELEASE events to clean up an | |
| 240 // oustanding drag. | |
| 241 g.dispose(); | |
| 242 }); | |
| 243 assert(!draggingAppContainer && !draggingAppOriginalPosition && | |
| 244 !draggingAppOriginalPage); | |
| 245 grabbers = []; | |
| 246 appEvents.removeAll(); | |
| 247 | |
| 248 // Clear any existing apps pages and dots. | |
| 249 // TODO(rbyers): It might be nice to preserve animation of dots after an | |
| 250 // uninstall. Could we re-use the existing page and dot elements? It seems | |
| 251 // unfortunate to have Chrome send us the entire apps list after an uninstall. | |
| 252 removeChildrenByClassName(appsPageList, 'apps-page'); | |
| 253 removeChildrenByClassName(dotList, 'dot'); | |
| 254 | |
| 255 // Get the array of apps and add any special synthesized entries | |
| 256 var apps = data.apps; | |
| 257 apps.push(makeWebstoreApp()); | |
| 258 | |
| 259 // Sort by launch index | |
| 260 apps.sort(function(a, b) { | |
| 261 return a.app_launch_index - b.app_launch_index; | |
| 262 }); | |
| 263 | |
| 264 // Add the apps, creating pages as necessary | |
| 265 for (var i = 0; i < apps.length; i++) { | |
| 266 var app = apps[i]; | |
| 267 var pageIndex = (app.page_index || 0); | |
| 268 while (pageIndex >= appsPages.length) { | |
| 269 var origPageCount = appsPages.length; | |
| 270 createAppPage(); | |
| 271 // Confirm that appsPages is a live object, updated when a new page is | |
| 272 // added (otherwise we'd have an infinite loop) | |
| 273 assert(appsPages.length == origPageCount + 1, 'expected new page'); | |
| 274 } | |
| 275 appendApp(appsPages[pageIndex], app); | |
| 276 } | |
| 277 | |
| 278 // Tell the slider about the pages | |
| 279 updateSliderCards(); | |
| 280 | |
| 281 // Mark the current page | |
| 282 dots[slider.getCurrentCard()].classList.add('selected'); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
slider.currentCard... "use getters for getters" ru
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 283 } | |
| 284 | |
| 285 /** | |
| 286 * Make a synthesized app object representing the chrome web store. It seems | |
| 287 * like this could just as easily come from the back-end, and then would support | |
| 288 * being rearranged, etc. | |
| 289 * @return {Object} The app object as would be sent from the webui back-end. | |
| 290 */ | |
| 291 function makeWebstoreApp() { | |
| 292 return { | |
| 293 id: '', // Empty ID signifies this is a special synthesized app | |
| 294 page_index: 0, | |
| 295 app_launch_index: -1, // always first | |
| 296 name: templateData.web_store_title, | |
| 297 launch_url: templateData.web_store_url, | |
| 298 icon_big: getThemeUrl('IDR_WEBSTORE_ICON') | |
| 299 }; | |
| 300 } | |
| 301 | |
| 302 /** | |
| 303 * Given a theme resource name, construct a URL for it. | |
| 304 * @param {string} resourceName The name of the resource. | |
| 305 * @return {string} A url which can be used to load the resource. | |
| 306 */ | |
| 307 function getThemeUrl(resourceName) { | |
| 308 // Allow standalone_hack.js to hook this mapping (since chrome:// URLs | |
| 309 // won't work for a standalone page) | |
| 310 if (typeof(themeUrlMapper) == 'function') { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
typeof is a unary operator and not a function
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 311 var u = themeUrlMapper(resourceName); | |
| 312 if (u) | |
| 313 return u; | |
| 314 } | |
| 315 return 'chrome://theme/' + resourceName; | |
| 316 } | |
| 317 | |
| 318 /** | |
| 319 * Callback invoked by chrome whenever an app preference changes. | |
| 320 * The normal NTP uses this to keep track of the current launch-type of an app, | |
| 321 * updating the choices in the context menu. We don't have such a menu so don't | |
| 322 * use this at all (but it still needs to be here for chrome to call). | |
| 323 * @param {Object} data An object with all the data on available | |
| 324 * applications. | |
| 325 */ | |
| 326 function appsPrefChangeCallback(data) { | |
| 327 } | |
| 328 | |
| 329 /** | |
| 330 * Invoked whenever the pages in apps-page-list have changed so that | |
| 331 * the Slider knows about the new elements. | |
| 332 */ | |
| 333 function updateSliderCards() | |
| 334 { | |
| 335 var pageNo = slider.getCurrentCard(); | |
| 336 if (pageNo >= appsPages.length) | |
| 337 pageNo = appsPages.length - 1; | |
| 338 var pageArray = []; | |
| 339 for (var i = 0; i < appsPages.length; i++) | |
| 340 pageArray[i] = appsPages[i]; | |
| 341 slider.setCards(pageArray, pageNo); | |
| 342 } | |
| 343 | |
| 344 /** | |
| 345 * Create a new app element and attach it to the end of the specified app page. | |
| 346 * @param {!Element} parent The element where the app should be inserted. | |
| 347 * @param {!Object} app The application object to create an app for. | |
| 348 */ | |
| 349 function appendApp(parent, app) { | |
| 350 // Make a deep copy of the template and clear its ID | |
| 351 var containerElement = /** @type {!Element} */ (appTemplate.cloneNode(true)); | |
| 352 var appElement = containerElement.getElementsByClassName('app')[0]; | |
| 353 assert(appElement, 'Expected app-template to have an app child'); | |
| 354 assert(typeof(app.id) == 'string', | |
| 355 'Expected every app to have an ID or empty string'); | |
| 356 appElement.setAttribute('app-id', app.id); | |
| 357 | |
| 358 // Find the span element (if any) and fill it in with the app name | |
| 359 var span = appElement.getElementsByTagName('span')[0]; | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
appElement.querySelector('span')
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 360 if (span) | |
| 361 span.textContent = app.name; | |
| 362 | |
| 363 // Fill in the image | |
| 364 // We use a mask of the same image so CSS rules can highlight just the image | |
| 365 // when it's touched. | |
| 366 var appImg = appElement.getElementsByTagName('img')[0]; | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
querySelector
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 367 if (appImg) { | |
| 368 appImg.src = app.icon_big; | |
| 369 appImg.style.webkitMaskImage = 'url(' + app.icon_big + ')'; | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
We ran into a URL bug in WebKit for the NTP. I gue
Rick Byers
2011/03/15 21:47:54
Thanks! I'm not sure what the restrictions are on
| |
| 370 // We put a click handler just on the app image - so clicking on the margins | |
| 371 // between apps doesn't do anything | |
| 372 if (app.id) { | |
| 373 appEvents.add(appImg, 'click', appClick, false); | |
| 374 } else { | |
| 375 // Special case of synthesized apps - can't launch directly so just change | |
| 376 // the URL as if we clicked a link. We may want to eventually support | |
| 377 // tracking clicks with ping messages, but really it seems it would be | |
| 378 // better for the back-end to just create virtual apps for such cases. | |
| 379 appEvents.add(appImg, 'click', function(e) { | |
| 380 window.location = app.launch_url; | |
| 381 }, false); | |
| 382 } | |
| 383 } | |
| 384 | |
| 385 // Only real apps with back-end storage (for their launch index, etc.) can be | |
| 386 // rearranged. | |
| 387 if (app.id) { | |
| 388 // Create a grabber to support moving apps around | |
| 389 // Note that we move the app rather than the container. This is so that an | |
| 390 // element remains in the original position so we can detect when an app is | |
| 391 // dropped in its starting location. | |
| 392 var grabber = new Grabber(/** @type {!Element} */ (appElement)); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
sigh
| |
| 393 grabbers.push(grabber); | |
| 394 | |
| 395 // Register to be made aware of when we are dragged | |
| 396 appEvents.add(appElement, Grabber.EventType.DRAG_START, appDragStart, | |
| 397 false); | |
| 398 appEvents.add(appElement, Grabber.EventType.DRAG_END, appDragEnd, | |
| 399 false); | |
| 400 | |
| 401 // Register to be made aware of any app drags on top of our container | |
| 402 appEvents.add(containerElement, Grabber.EventType.DRAG_ENTER, appDragEnter, | |
| 403 false); | |
| 404 } else { | |
| 405 // Prevent any built-in drag-and-drop support from activating for the | |
| 406 // element. | |
| 407 appEvents.add(appElement, 'dragstart', function(e) { | |
| 408 e.preventDefault(); | |
| 409 }, true); | |
| 410 } | |
| 411 | |
| 412 // Insert at the end of the provided page | |
| 413 parent.appendChild(containerElement); | |
| 414 } | |
| 415 | |
| 416 /** | |
| 417 * Creates a new page for apps | |
| 418 * | |
| 419 * @return {!Element} The apps-page element created. | |
| 420 * @param {boolean=} opt_animate If true, add the class 'new' to the created | |
| 421 * dot. | |
| 422 */ | |
| 423 function createAppPage(opt_animate) | |
| 424 { | |
| 425 // Make a shallow copy of the app page template. | |
| 426 var newPage = /** @type !Element */ (appsPageTemplate.cloneNode(false)); | |
| 427 appsPageList.appendChild(newPage); | |
| 428 | |
| 429 // Make a deep copy of the dot template to add a new one. | |
| 430 var dotCount = dots.length; | |
| 431 var newDot = /** @type !Element */ (dotTemplate.cloneNode(true)); | |
| 432 if (opt_animate) | |
| 433 newDot.classList.add('new'); | |
| 434 dotList.appendChild(newDot); | |
| 435 | |
| 436 // Add click handler to the dot to change the page. | |
| 437 // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we don't | |
| 438 // rely on synthesized click events, and the change takes effect before | |
| 439 // releasing). However, click events seems to be synthesized for a region | |
| 440 // outside the border, and a 10px box is too small to require touch events to | |
| 441 // fall inside of. We could get around this by adding a box around the dot for | |
| 442 // accepting the touch events. | |
| 443 var switchPage = function(e) { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
function switchPage(e) {
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 444 slider.setCurrentCard(dotCount, true); | |
| 445 e.stopPropagation(); | |
| 446 } | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
missing semicolon
Rick Byers
2011/03/15 21:47:54
But not when using the syntax you prefer, right?
| |
| 447 appEvents.add(newDot, 'click', switchPage, false); | |
| 448 | |
| 449 // Change pages whenever an app is dragged over a dot. | |
| 450 appEvents.add(newDot, Grabber.EventType.DRAG_ENTER, switchPage, false); | |
| 451 | |
| 452 return newPage; | |
| 453 } | |
| 454 | |
| 455 /** | |
| 456 * Invoked when an app is clicked | |
| 457 * @param {Event} e The click event. | |
| 458 */ | |
| 459 function appClick(e) { | |
| 460 var target = /** @type {!Element} */ (e.currentTarget); | |
| 461 var app = getParentByClassName(target, 'app'); | |
| 462 assert(app, 'appClick should have been on a descendant of an app'); | |
| 463 | |
| 464 var appId = app.getAttribute('app-id'); | |
| 465 assert(appId, 'unexpected app without appId'); | |
| 466 | |
| 467 // Tell chrome to launch the app. | |
| 468 var NTP_APPS_MAXIMIZED = 0; | |
| 469 chrome.send('launchApp', [appId, NTP_APPS_MAXIMIZED]); | |
| 470 | |
| 471 // Don't propagate the click (in case we have a link or something) | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
This comment explains why we preventDefault. It do
Rick Byers
2011/03/15 21:47:54
What is your preferred style for when to call stop
| |
| 472 e.stopPropagation(); | |
| 473 e.preventDefault(); | |
| 474 } | |
| 475 | |
| 476 /** | |
| 477 * Search an elements ancestor chain for the nearest element that is a member of | |
| 478 * the specified class. | |
| 479 * @param {!Element} element The element to start searching from. | |
| 480 * @param {string} className The name of the class to locate. | |
| 481 * @return {Element} The first ancestor of the specified class or null. | |
| 482 */ | |
| 483 function getParentByClassName(element, className) | |
| 484 { | |
| 485 for (var e = element; | |
| 486 e && e.nodeType == Node.ELEMENT_NODE; | |
| 487 e = e.parentNode) { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
e.parentElement allows you to skip the nodeTyoe ch
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 488 if (e.classList.contains(className)) | |
| 489 return /** @type {Element} */ (e); | |
| 490 } | |
| 491 return null; | |
| 492 } | |
| 493 | |
| 494 /** | |
| 495 * The container where the app currently being dragged came from. | |
| 496 * @type {Element} | |
| 497 */ | |
| 498 var draggingAppContainer = null; | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
Do you really need to init these to null? Most of
Rick Byers
2011/03/15 21:47:54
Nope, I originally used undefined and switched to
| |
| 499 | |
| 500 /** | |
| 501 * The apps-page that the app currently being dragged camed from. | |
| 502 * @type {Element} | |
| 503 */ | |
| 504 var draggingAppOriginalPage = null; | |
| 505 | |
| 506 /** | |
| 507 * The element that was originally after the app currently being dragged (or | |
| 508 * null if it was the last on the page). | |
| 509 * @type {Element} | |
| 510 */ | |
| 511 var draggingAppOriginalPosition = null; | |
| 512 | |
| 513 /** | |
| 514 * Invoked when app dragging begins. | |
| 515 * @param {Event} e The event from the Grabber indicating the drag. | |
| 516 */ | |
| 517 function appDragStart(e) { | |
| 518 // Pull the element out to the appsFrame using fixed positioning. This ensures | |
| 519 // that the app is not affected (remains under the finger) if the slider | |
| 520 // changes cards and is translated. An alternate approach would be to use | |
| 521 // fixed positioning for the slider (so that changes to its position don't | |
| 522 // affect children that aren't positioned relative to it), but we don't yet | |
| 523 // have GPU acceleration for this. Note that we use the appsFrame | |
| 524 var element = e.sender.element; | |
| 525 | |
| 526 var pos = element.getBoundingClientRect(); | |
| 527 element.style.webkitTransform = ''; | |
| 528 | |
| 529 element.style.position = 'fixed'; | |
| 530 // Don't want to zoom around the middle since the left/top co-ordinates | |
| 531 // are post-transform values. | |
| 532 element.style.webkitTransformOrigin = 'left top'; | |
| 533 element.style.left = pos.left + 'px'; | |
| 534 element.style.top = pos.top + 'px'; | |
| 535 | |
| 536 // Keep track of what app is being dragged and where it came from | |
| 537 assert(draggingAppContainer == null, | |
| 538 'got DRAG_START without DRAG_END'); | |
| 539 draggingAppContainer = element.parentNode; | |
| 540 assert(draggingAppContainer.classList.contains('app-container')); | |
| 541 draggingAppOriginalPosition = draggingAppContainer.nextSibling; | |
| 542 draggingAppOriginalPage = draggingAppContainer.parentNode; | |
| 543 | |
| 544 // Move the app out of the container | |
| 545 // Note that appendChild also removes the element from its current parent. | |
| 546 getRequiredElement('apps-frame').appendChild(element); | |
| 547 } | |
| 548 | |
| 549 /** | |
| 550 * Invoked when app dragging terminates (either successfully or not) | |
| 551 * @param {CustomEvent} e The event from the Grabber. | |
| 552 */ | |
| 553 function appDragEnd(e) { | |
| 554 // Stop floating the app | |
| 555 var appBeingDragged = e.sender.element; | |
| 556 assert(appBeingDragged.classList.contains('app')); | |
| 557 appBeingDragged.style.position = ''; | |
| 558 appBeingDragged.style.webkitTransformOrigin = ''; | |
| 559 appBeingDragged.style.left = ''; | |
| 560 appBeingDragged.style.top = ''; | |
| 561 | |
| 562 // If we have an active drag (i.e. it wasn't aborted by an app update) | |
| 563 if (draggingAppContainer) { | |
| 564 // Put the app back into it's container | |
| 565 if (appBeingDragged.parentNode != draggingAppContainer) | |
| 566 draggingAppContainer.appendChild(appBeingDragged); | |
| 567 | |
| 568 // If we care about the container's original position | |
| 569 if (draggingAppOriginalPage) | |
| 570 { | |
| 571 // Then put the container back where it came from | |
| 572 if (draggingAppOriginalPosition) { | |
| 573 draggingAppOriginalPage.insertBefore(draggingAppContainer, | |
| 574 draggingAppOriginalPosition); | |
| 575 } else { | |
| 576 draggingAppOriginalPage.appendChild(draggingAppContainer); | |
| 577 } | |
| 578 } | |
| 579 } | |
| 580 | |
| 581 draggingAppContainer = null; | |
| 582 draggingAppOriginalPage = null; | |
| 583 draggingAppOriginalPosition = null; | |
| 584 } | |
| 585 | |
| 586 /** | |
| 587 * Invoked when an app is dragged over another app. Updates the DOM to affect | |
| 588 * the rearrangement (but doesn't commit the change until the app is dropped). | |
| 589 * @param {Event} e The event from the Grabber indicating the drag. | |
| 590 */ | |
| 591 function appDragEnter(e) | |
| 592 { | |
| 593 assert(draggingAppContainer, 'expected stored container'); | |
| 594 var sourceContainer = /** @type {!Element} */ (draggingAppContainer); | |
| 595 | |
| 596 e.stopPropagation(); | |
| 597 | |
| 598 var curPage = appsPages[slider.getCurrentCard()]; | |
| 599 var followingContainer = null; | |
| 600 | |
| 601 // If we dragged over a specific app, determine which one to insert before | |
| 602 if (e.currentTarget != document.body) { | |
| 603 | |
| 604 // Start by assuming we'll insert the app before the one dragged over | |
| 605 followingContainer = e.currentTarget; | |
| 606 assert(followingContainer.classList.contains('app-container'), | |
| 607 'expected drag over container'); | |
| 608 assert(followingContainer.parentNode == curPage); | |
| 609 if (followingContainer == draggingAppContainer) | |
| 610 return; | |
| 611 | |
| 612 // But if it's after the current container position then we'll need to | |
| 613 // move ahead by one to account for the container being removed. | |
| 614 if (curPage == draggingAppContainer.parentNode) { | |
| 615 for (var c = draggingAppContainer; c; c = c.nextSibling) { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
nextElementSibling maybe to skip non element nodes
Rick Byers
2011/03/15 21:47:54
Done.
| |
| 616 if (c == followingContainer) { | |
| 617 followingContainer = followingContainer.nextSibling; | |
| 618 break; | |
| 619 } | |
| 620 } | |
| 621 } | |
| 622 } | |
| 623 | |
| 624 // Move the container to the appropriate place on the page | |
| 625 if (followingContainer) { | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
This if is not needed. insertBefore(node, null) is
Rick Byers
2011/03/15 21:47:54
Nice
| |
| 626 curPage.insertBefore(draggingAppContainer, followingContainer); | |
| 627 } else { | |
| 628 curPage.appendChild(draggingAppContainer); | |
| 629 } | |
| 630 } | |
| 631 | |
| 632 /** | |
| 633 * Invoked when an app is dropped on the trash | |
| 634 * @param {Event} e The event from the Grabber indicating the drop. | |
| 635 */ | |
| 636 function appTrash(e) { | |
| 637 var appElement = e.sender.element; | |
| 638 assert(appElement.classList.contains('app')); | |
| 639 var appId = appElement.getAttribute('app-id'); | |
| 640 assert(appId); | |
| 641 | |
| 642 // Mark this drop as handled | |
| 643 e.stopPropagation(); | |
|
arv (Not doing code reviews)
2011/03/11 20:08:47
I don't understand what propagation has to do with
Rick Byers
2011/03/15 21:47:54
Does the expanded comment help? I want to treat a
| |
| 644 | |
| 645 // Tell chrome to uninstall the app (prompting the user) | |
| 646 chrome.send('uninstallApp', [appId]); | |
| 647 | |
| 648 // Return the trash can to normal (we don't get a DRAG_LEAVE) | |
| 649 getRequiredElement('trash').classList.remove('hover'); | |
| 650 appElement.classList.remove('trashing'); | |
| 651 } | |
| 652 | |
| 653 /** | |
| 654 * Called when an app is dropped anywhere other than the trash can. Commits any | |
| 655 * movement that has occurred. | |
| 656 * @param {Event} e The event from the Grabber indicating the drop. | |
| 657 */ | |
| 658 function appDrop(e) { | |
| 659 if (!draggingAppContainer) | |
| 660 // Drag was aborted (eg. due to an app update) - do nothing | |
| 661 return; | |
| 662 | |
| 663 // If the app is dropped back into it's original position then do nothing | |
| 664 assert(draggingAppOriginalPage); | |
| 665 if (draggingAppContainer.parentNode == draggingAppOriginalPage && | |
| 666 draggingAppContainer.nextSibling == draggingAppOriginalPosition) | |
| 667 return; | |
| 668 | |
| 669 // Determine which app was being dragged | |
| 670 var appElement = e.sender.element; | |
| 671 assert(appElement.classList.contains('app')); | |
| 672 var appId = appElement.getAttribute('app-id'); | |
| 673 assert(appId); | |
| 674 | |
| 675 // Update the page index for the app if it's changed. This doesn't trigger a | |
| 676 // call to getAppsCallback so we want to do it before reorderApps | |
| 677 var pageIndex = slider.getCurrentCard(); | |
| 678 assert(pageIndex >= 0 && pageIndex < appsPages.length, | |
| 679 'page number out of range'); | |
| 680 if (appsPages[pageIndex] != draggingAppOriginalPage) | |
| 681 chrome.send('setPageIndex', [appId, pageIndex]); | |
| 682 | |
| 683 // Put the app being dragged back into it's container | |
| 684 draggingAppContainer.appendChild(appElement); | |
| 685 | |
| 686 // Create a list of all appIds in the order now present in the DOM | |
| 687 var appIds = []; | |
| 688 for (var page = 0; page < appsPages.length; page++) { | |
| 689 var appsOnPage = appsPages[page].getElementsByClassName('app'); | |
| 690 for (var i = 0; i < appsOnPage.length; i++) { | |
| 691 var id = appsOnPage[i].getAttribute('app-id'); | |
| 692 if (id) | |
| 693 appIds.push(id); | |
| 694 } | |
| 695 } | |
| 696 | |
| 697 // We are going to commit this repositioning - clear the original position | |
| 698 draggingAppOriginalPage = null; | |
| 699 draggingAppOriginalPosition = null; | |
| 700 | |
| 701 // Tell chrome to update its database to persist this new order of apps This | |
| 702 // will cause getAppsCallback to be invoked and the apps to be redrawn. | |
| 703 chrome.send('reorderApps', [appId, appIds]); | |
| 704 appMoved = true; | |
| 705 } | |
| 706 | |
| 707 /** | |
| 708 * Set to true if we're currently in rearrange mode and an app has | |
| 709 * been successfully dropped to a new location. This indicates that | |
| 710 * a getAppsCallback call is pending and we can rely on the DOM being | |
| 711 * updated by that. | |
| 712 * @type {boolean} | |
| 713 */ | |
| 714 var appMoved = false; | |
| 715 | |
| 716 /** | |
| 717 * Invoked whenever some app is grabbed | |
| 718 * @param {Event} e The Grabber Grab event. | |
| 719 */ | |
| 720 function enterRearrangeMode(e) | |
| 721 { | |
| 722 // Stop the slider from sliding for this touch | |
| 723 slider.cancelTouch(); | |
| 724 | |
| 725 // Add an extra blank page in case the user wants to create a new page | |
| 726 createAppPage(true); | |
| 727 var pageAdded = appsPages.length - 1; | |
| 728 window.setTimeout(function() { | |
| 729 dots[pageAdded].classList.remove('new'); | |
| 730 }, 0); | |
| 731 | |
| 732 updateSliderCards(); | |
| 733 | |
| 734 // Cause the dot-list to grow | |
| 735 getRequiredElement('footer').classList.add('rearrange-mode'); | |
| 736 | |
| 737 assert(!appMoved, 'appMoved should not be set yet'); | |
| 738 } | |
| 739 | |
| 740 /** | |
| 741 * Invoked whenever some app is released | |
| 742 * @param {Event} e The Grabber RELEASE event. | |
| 743 */ | |
| 744 function leaveRearrangeMode(e) | |
| 745 { | |
| 746 // Return the dot-list to normal | |
| 747 getRequiredElement('footer').classList.remove('rearrange-mode'); | |
| 748 | |
| 749 // If we didn't successfully re-arrange an app, then we won't be | |
| 750 // refreshing the app view in getAppCallback and need to explicitly remove the | |
| 751 // extra empty page we added. We don't want to do this in the normal case | |
| 752 // because if we did actually drop an app there, we want to retain that page | |
| 753 // as our current page number. | |
| 754 if (!appMoved) { | |
| 755 assert(appsPages[appsPages.length - 1]. | |
| 756 getElementsByClassName('app-container').length == 0, | |
| 757 'Last app page should be empty'); | |
| 758 removePage(appsPages.length - 1); | |
| 759 } | |
| 760 appMoved = false; | |
| 761 } | |
| 762 | |
| 763 /** | |
| 764 * Remove the page with the specified index and update the slider. | |
| 765 * @param {number} pageNo The index of the page to remove. | |
| 766 */ | |
| 767 function removePage(pageNo) | |
| 768 { | |
| 769 var page = appsPages[pageNo]; | |
| 770 | |
| 771 // Remove the page from the DOM | |
| 772 page.parentNode.removeChild(page); | |
| 773 | |
| 774 // Remove the corresponding dot | |
| 775 // Need to give it a chance to animate though | |
| 776 var dot = dots[pageNo]; | |
| 777 dot.classList.add('new'); | |
| 778 window.setTimeout(function() { | |
| 779 // If we've re-created the apps (eg. because an app was uninstalled) then | |
| 780 // we will have removed the old dots from the document already, so skip. | |
| 781 if (dot.parentNode) | |
| 782 dot.parentNode.removeChild(dot); | |
| 783 }, DEFAULT_TRANSITION_TIME); | |
| 784 | |
| 785 updateSliderCards(); | |
| 786 } | |
| 787 | |
| 788 // There doesn't seem to be any need to wait for DOMContentLoaded | |
| 789 initializeNtp(); | |
| 790 | |
| OLD | NEW |