Home | History | Annotate | Download | only in touch_ntp
      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();
    806