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