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 13var 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 799var assert = ntp.assert; 800var getAppsCallback = ntp.getAppsCallback; 801var appsPrefChangeCallback = ntp.appsPrefChangeCallback; 802 803// Initialize immediately once globals are published (there doesn't seem to be 804// any need to wait for DOMContentLoaded) 805ntp.initialize(); 806