summaryrefslogtreecommitdiffstats
path: root/chrome/browser/resources/ntp4/new_tab.js
blob: bac18ae42aaf155390a304c1a086f1d34604f7a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Touch-based new tab page
 * This is the main code for the new tab page used by touch-enabled Chrome
 * browsers.  For now this is still a prototype.
 */

// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome
var ntp = (function() {
  'use strict';

  /**
   * The CardSlider object to use for changing app pages.
   * @type {CardSlider|undefined}
   */
  var cardSlider;

  /**
   * Template to use for creating new 'apps-page' elements
   * @type {!Element|undefined}
   */
  var appsPageTemplate;

  /**
   * Template to use for creating new 'app-container' elements
   * @type {!Element|undefined}
   */
  var appTemplate;

  /**
   * Template to use for creating new 'dot' elements
   * @type {!Element|undefined}
   */
  var dotTemplate;

  /**
   * The 'apps-page-list' element.
   * @type {!Element}
   */
  var appsPageList = getRequiredElement('apps-page-list');

  /**
   * A list of all 'apps-page' elements.
   * @type {!NodeList|undefined}
   */
  var appsPages;

  /**
   * The 'dots-list' element.
   * @type {!Element}
   */
  var dotList = getRequiredElement('dot-list');

  /**
   * A list of all 'dots' elements.
   * @type {!NodeList|undefined}
   */
  var dots;

  /**
   * The 'trash' element.  Note that technically this is unnecessary,
   * JavaScript creates the object for us based on the id.  But I don't want
   * to rely on the ID being the same, and JSCompiler doesn't know about it.
   * @type {!Element}
   */
  var trash = getRequiredElement('trash');

  /**
   * The time in milliseconds for most transitions.  This should match what's
   * in new_tab.css.  Unfortunately there's no better way to try to time
   * something to occur until after a transition has completed.
   * @type {number}
   * @const
   */
  var DEFAULT_TRANSITION_TIME = 500;

  /**
   * All the Grabber objects currently in use on the page
   * @type {Array.<Grabber>}
   */
  var grabbers = [];

  /**
   * Holds all event handlers tied to apps (and so subject to removal when the
   * app list is refreshed)
   * @type {!EventTracker}
   */
  var appEvents = new EventTracker();

  /**
   * Invoked at startup once the DOM is available to initialize the app.
   */
  function initializeNtp() {
    // Request data on the apps so we can fill them in.
    // Note that this is kicked off asynchronously.  'getAppsCallback' will be
    // invoked at some point after this function returns.
    chrome.send('getApps');

    // Prevent touch events from triggering any sort of native scrolling
    document.addEventListener('touchmove', function(e) {
      e.preventDefault();
    }, true);

    // Get the template elements and remove them from the DOM.  Things are
    // simpler if we start with 0 pages and 0 apps and don't leave hidden
    // template elements behind in the DOM.
    appTemplate = getRequiredElement('app-template');
    appTemplate.id = null;

    appsPages = appsPageList.getElementsByClassName('apps-page');
    assert(appsPages.length == 1,
           'Expected exactly one apps-page in the apps-page-list.');
    appsPageTemplate = appsPages[0];
    appsPageList.removeChild(appsPages[0]);

    dots = dotList.getElementsByClassName('dot');
    assert(dots.length == 1,
           'Expected exactly one dot in the dots-list.');
    dotTemplate = dots[0];
    dotList.removeChild(dots[0]);

    // Initialize the cardSlider without any cards at the moment
    var appsFrame = getRequiredElement('apps-frame');
    cardSlider = new CardSlider(appsFrame, appsPageList, [], 0,
                                appsFrame.offsetWidth);
    cardSlider.initialize();

    // Ensure the slider is resized appropriately with the window
    window.addEventListener('resize', function() {
      cardSlider.resize(appsFrame.offsetWidth);
    });

    // Handle the page being changed
    appsPageList.addEventListener(
        CardSlider.EventType.CARD_CHANGED,
        function(e) {
          // Update the active dot
          var curDot = dotList.getElementsByClassName('selected')[0];
          if (curDot)
            curDot.classList.remove('selected');
          var newPageIndex = e.cardSlider.currentCard;
          dots[newPageIndex].classList.add('selected');
          // If an app was being dragged, move it to the end of the new page
          if (draggingAppContainer)
            appsPages[newPageIndex].appendChild(draggingAppContainer);
        });

    // Add a drag handler to the body (for drags that don't land on an existing
    // app)
    document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter);

    // Handle dropping an app anywhere other than on the trash
    document.addEventListener(Grabber.EventType.DROP, appDrop);

    // Add handles to manage the transition into/out-of rearrange mode
    // Note that we assume here that we only use a Grabber for moving apps,
    // so ANY GRAB event means we're enterring rearrange mode.
    appsFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode);
    appsFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode);

    // Add handlers for the tash can
    trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) {
      trash.classList.add('hover');
      e.grabbedElement.classList.add('trashing');
      e.stopPropagation();
    });
    trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) {
      e.grabbedElement.classList.remove('trashing');
      trash.classList.remove('hover');
    });
    trash.addEventListener(Grabber.EventType.DROP, appTrash);
  }

  /**
   * Simple common assertion API
   * @param {*} condition The condition to test.  Note that this may be used to
   *     test whether a value is defined or not, and we don't want to force a
   *     cast to Boolean.
   * @param {string=} opt_message A message to use in any error.
   */
  function assert(condition, opt_message) {
    'use strict';
    if (!condition) {
      var msg = 'Assertion failed';
      if (opt_message)
        msg = msg + ': ' + opt_message;
      throw new Error(msg);
    }
  }

  /**
   * Get an element that's known to exist by its ID. We use this instead of just
   * calling getElementById and not checking the result because this lets us
   * satisfy the JSCompiler type system.
   * @param {string} id The identifier name.
   * @return {!Element} the Element.
   */
  function getRequiredElement(id) {
    var element = document.getElementById(id);
    assert(element, 'Missing required element: ' + id);
    return element;
  }

  /**
   * Remove all children of an element which have a given class in
   * their classList.
   * @param {!Element} element The parent element to examine.
   * @param {string} className The class to look for.
   */
  function removeChildrenByClassName(element, className) {
    for (var child = element.firstElementChild; child;) {
      var prev = child;
      child = child.nextElementSibling;
      if (prev.classList.contains(className))
        element.removeChild(prev);
    }
  }

  /**
   * Callback invoked by chrome with the apps available.
   *
   * Note that calls to this function can occur at any time, not just in
   * response to a getApps request. For example, when a user installs/uninstalls
   * an app on another synchronized devices.
   * @param {Object} data An object with all the data on available
   *        applications.
   */
  function getAppsCallback(data)
  {
    // Clean up any existing grabber objects - cancelling any outstanding drag.
    // Ideally an async app update wouldn't disrupt an active drag but
    // that would require us to re-use existing elements and detect how the apps
    // have changed, which would be a lot of work.
    // Note that we have to explicitly clean up the grabber objects so they stop
    // listening to events and break the DOM<->JS cycles necessary to enable
    // collection of all these objects.
    grabbers.forEach(function(g) {
      // Note that this may raise DRAG_END/RELEASE events to clean up an
      // oustanding drag.
      g.dispose();
    });
    assert(!draggingAppContainer && !draggingAppOriginalPosition &&
           !draggingAppOriginalPage);
    grabbers = [];
    appEvents.removeAll();

    // Clear any existing apps pages and dots.
    // TODO(rbyers): It might be nice to preserve animation of dots after an
    // uninstall. Could we re-use the existing page and dot elements?  It seems
    // unfortunate to have Chrome send us the entire apps list after an
    // uninstall.
    removeChildrenByClassName(appsPageList, 'apps-page');
    removeChildrenByClassName(dotList, 'dot');

    // Get the array of apps and add any special synthesized entries
    var apps = data.apps;
    apps.push(makeWebstoreApp());

    // Sort by launch index
    apps.sort(function(a, b) {
      return a.app_launch_index - b.app_launch_index;
    });

    // Add the apps, creating pages as necessary
    for (var i = 0; i < apps.length; i++) {
      var app = apps[i];
      var pageIndex = (app.page_index || 0);
      while (pageIndex >= appsPages.length) {
        var origPageCount = appsPages.length;
        createAppPage();
        // Confirm that appsPages is a live object, updated when a new page is
        // added (otherwise we'd have an infinite loop)
        assert(appsPages.length == origPageCount + 1, 'expected new page');
      }
      appendApp(appsPages[pageIndex], app);
    }

    // Tell the slider about the pages
    updateSliderCards();

    // Mark the current page
    dots[cardSlider.currentCard].classList.add('selected');
  }

  /**
   * Make a synthesized app object representing the chrome web store.  It seems
   * like this could just as easily come from the back-end, and then would
   * support being rearranged, etc.
   * @return {Object} The app object as would be sent from the webui back-end.
   */
  function makeWebstoreApp() {
    return {
      id: '',   // Empty ID signifies this is a special synthesized app
      page_index: 0,
      app_launch_index: -1,   // always first
      name: templateData.web_store_title,
      launch_url: templateData.web_store_url,
      icon_big: getThemeUrl('IDR_WEBSTORE_ICON')
    };
  }

  /**
   * Given a theme resource name, construct a URL for it.
   * @param {string} resourceName The name of the resource.
   * @return {string} A url which can be used to load the resource.
   */
  function getThemeUrl(resourceName) {
    return 'chrome://theme/' + resourceName;
  }

  /**
   * Callback invoked by chrome whenever an app preference changes.
   * The normal NTP uses this to keep track of the current launch-type of an
   * app, updating the choices in the context menu.  We don't have such a menu
   * so don't use this at all (but it still needs to be here for chrome to
   * call).
   * @param {Object} data An object with all the data on available
   *        applications.
   */
  function appsPrefChangeCallback(data) {
  }

  /**
   * Invoked whenever the pages in apps-page-list have changed so that
   * the Slider knows about the new elements.
   */
  function updateSliderCards() {
    var pageNo = cardSlider.currentCard;
    if (pageNo >= appsPages.length)
      pageNo = appsPages.length - 1;
    var pageArray = [];
    for (var i = 0; i < appsPages.length; i++)
      pageArray[i] = appsPages[i];
    cardSlider.setCards(pageArray, pageNo);
  }

  /**
   * Create a new app element and attach it to the end of the specified app
   * page.
   * @param {!Element} parent The element where the app should be inserted.
   * @param {!Object} app The application object to create an app for.
   */
  function appendApp(parent, app) {
    // Make a deep copy of the template and clear its ID
    var containerElement = appTemplate.cloneNode(true);
    var appElement = containerElement.getElementsByClassName('app')[0];
    assert(appElement, 'Expected app-template to have an app child');
    assert(typeof(app.id) == 'string',
           'Expected every app to have an ID or empty string');
    appElement.setAttribute('app-id', app.id);

    // Find the span element (if any) and fill it in with the app name
    var span = appElement.querySelector('span');
    if (span)
      span.textContent = app.name;

    // Fill in the image
    // We use a mask of the same image so CSS rules can highlight just the image
    // when it's touched.
    var appImg = appElement.querySelector('img');
    if (appImg) {
      appImg.src = app.icon_big;
      appImg.style.webkitMaskImage = url(app.icon_big);
      // We put a click handler just on the app image - so clicking on the
      // margins between apps doesn't do anything
      if (app.id) {
        appEvents.add(appImg, 'click', appClick, false);
      } else {
        // Special case of synthesized apps - can't launch directly so just
        // change the URL as if we clicked a link.  We may want to eventually
        // support tracking clicks with ping messages, but really it seems it
        // would be better for the back-end to just create virtual apps for such
        // cases.
        appEvents.add(appImg, 'click', function(e) {
          window.location = app.launch_url;
        }, false);
      }
    }

    // Only real apps with back-end storage (for their launch index, etc.) can
    // be rearranged.
    if (app.id) {
      // Create a grabber to support moving apps around
      // Note that we move the app rather than the container. This is so that an
      // element remains in the original position so we can detect when an app
      // is dropped in its starting location.
      var grabber = new Grabber(appElement);
      grabbers.push(grabber);

      // Register to be made aware of when we are dragged
      appEvents.add(appElement, Grabber.EventType.DRAG_START, appDragStart,
                    false);
      appEvents.add(appElement, Grabber.EventType.DRAG_END, appDragEnd,
                    false);

      // Register to be made aware of any app drags on top of our container
      appEvents.add(containerElement, Grabber.EventType.DRAG_ENTER,
          appDragEnter, false);
    } else {
      // Prevent any built-in drag-and-drop support from activating for the
      // element.
      appEvents.add(appElement, 'dragstart', function(e) {
        e.preventDefault();
      }, true);
    }

    // Insert at the end of the provided page
    parent.appendChild(containerElement);
  }

  /**
   * Creates a new page for apps
   *
   * @return {!Element} The apps-page element created.
   * @param {boolean=} opt_animate If true, add the class 'new' to the created
   *        dot.
   */
  function createAppPage(opt_animate)
  {
    // Make a shallow copy of the app page template.
    var newPage = appsPageTemplate.cloneNode(false);
    appsPageList.appendChild(newPage);

    // Make a deep copy of the dot template to add a new one.
    var dotCount = dots.length;
    var newDot = dotTemplate.cloneNode(true);
    if (opt_animate)
      newDot.classList.add('new');
    dotList.appendChild(newDot);

    // Add click handler to the dot to change the page.
    // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we
    // don't rely on synthesized click events, and the change takes effect
    // before releasing). However, click events seems to be synthesized for a
    // region outside the border, and a 10px box is too small to require touch
    // events to fall inside of. We could get around this by adding a box around
    // the dot for accepting the touch events.
    function switchPage(e) {
      cardSlider.selectCard(dotCount, true);
      e.stopPropagation();
    }
    appEvents.add(newDot, 'click', switchPage, false);

    // Change pages whenever an app is dragged over a dot.
    appEvents.add(newDot, Grabber.EventType.DRAG_ENTER, switchPage, false);

    return newPage;
  }

  /**
   * Invoked when an app is clicked
   * @param {Event} e The click event.
   */
  function appClick(e) {
    var target = e.currentTarget;
    var app = getParentByClassName(target, 'app');
    assert(app, 'appClick should have been on a descendant of an app');

    var appId = app.getAttribute('app-id');
    assert(appId, 'unexpected app without appId');

    // Tell chrome to launch the app.
    var NTP_APPS_MAXIMIZED = 0;
    chrome.send('launchApp', [appId, NTP_APPS_MAXIMIZED]);

    // Don't allow the click to trigger a link or anything
    e.preventDefault();
  }

  /**
   * Search an elements ancestor chain for the nearest element that is a member
   * of the specified class.
   * @param {!Element} element The element to start searching from.
   * @param {string} className The name of the class to locate.
   * @return {Element} The first ancestor of the specified class or null.
   */
  function getParentByClassName(element, className)
  {
    for (var e = element; e; e = e.parentElement) {
      if (e.classList.contains(className))
        return e;
    }
    return null;
  }

  /**
   * The container where the app currently being dragged came from.
   * @type {!Element|undefined}
   */
  var draggingAppContainer;

  /**
   * The apps-page that the app currently being dragged camed from.
   * @type {!Element|undefined}
   */
  var draggingAppOriginalPage;

  /**
   * The element that was originally after the app currently being dragged (or
   * null if it was the last on the page).
   * @type {!Element|undefined}
   */
  var draggingAppOriginalPosition;

  /**
   * Invoked when app dragging begins.
   * @param {Grabber.Event} e The event from the Grabber indicating the drag.
   */
  function appDragStart(e) {
    // Pull the element out to the appsFrame using fixed positioning. This
    // ensures that the app is not affected (remains under the finger) if the
    // slider changes cards and is translated.  An alternate approach would be
    // to use fixed positioning for the slider (so that changes to its position
    // don't affect children that aren't positioned relative to it), but we
    // don't yet have GPU acceleration for this.  Note that we use the appsFrame
    var element = e.grabbedElement;

    var pos = element.getBoundingClientRect();
    element.style.webkitTransform = '';

    element.style.position = 'fixed';
    // Don't want to zoom around the middle since the left/top co-ordinates
    // are post-transform values.
    element.style.webkitTransformOrigin = 'left top';
    element.style.left = pos.left + 'px';
    element.style.top = pos.top + 'px';

    // Keep track of what app is being dragged and where it came from
    assert(!draggingAppContainer, 'got DRAG_START without DRAG_END');
    draggingAppContainer = element.parentNode;
    assert(draggingAppContainer.classList.contains('app-container'));
    draggingAppOriginalPosition = draggingAppContainer.nextSibling;
    draggingAppOriginalPage = draggingAppContainer.parentNode;

    // Move the app out of the container
    // Note that appendChild also removes the element from its current parent.
    getRequiredElement('apps-frame').appendChild(element);
  }

  /**
   * Invoked when app dragging terminates (either successfully or not)
   * @param {Grabber.Event} e The event from the Grabber.
   */
  function appDragEnd(e) {
    // Stop floating the app
    var appBeingDragged = e.grabbedElement;
    assert(appBeingDragged.classList.contains('app'));
    appBeingDragged.style.position = '';
    appBeingDragged.style.webkitTransformOrigin = '';
    appBeingDragged.style.left = '';
    appBeingDragged.style.top = '';

    // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE
    // for it - eg. if we drop on it, or the drag is cancelled)
    trash.classList.remove('hover');
    appBeingDragged.classList.remove('trashing');

    // If we have an active drag (i.e. it wasn't aborted by an app update)
    if (draggingAppContainer) {
      // Put the app back into it's container
      if (appBeingDragged.parentNode != draggingAppContainer)
        draggingAppContainer.appendChild(appBeingDragged);

      // If we care about the container's original position
      if (draggingAppOriginalPage)
      {
        // Then put the container back where it came from
        if (draggingAppOriginalPosition) {
          draggingAppOriginalPage.insertBefore(draggingAppContainer,
                                               draggingAppOriginalPosition);
        } else {
          draggingAppOriginalPage.appendChild(draggingAppContainer);
        }
      }
    }

    draggingAppContainer = undefined;
    draggingAppOriginalPage = undefined;
    draggingAppOriginalPosition = undefined;
  }

  /**
   * Invoked when an app is dragged over another app.  Updates the DOM to affect
   * the rearrangement (but doesn't commit the change until the app is dropped).
   * @param {Grabber.Event} e The event from the Grabber indicating the drag.
   */
  function appDragEnter(e)
  {
    assert(draggingAppContainer, 'expected stored container');
    var sourceContainer = draggingAppContainer;

    // Ensure enter events delivered to an app-container don't also get
    // delivered to the document.
    e.stopPropagation();

    var curPage = appsPages[cardSlider.currentCard];
    var followingContainer = null;

    // If we dragged over a specific app, determine which one to insert before
    if (e.currentTarget != document) {

      // Start by assuming we'll insert the app before the one dragged over
      followingContainer = e.currentTarget;
      assert(followingContainer.classList.contains('app-container'),
             'expected drag over container');
      assert(followingContainer.parentNode == curPage);
      if (followingContainer == draggingAppContainer)
        return;

      // But if it's after the current container position then we'll need to
      // move ahead by one to account for the container being removed.
      if (curPage == draggingAppContainer.parentNode) {
        for (var c = draggingAppContainer; c; c = c.nextElementSibling) {
          if (c == followingContainer) {
            followingContainer = followingContainer.nextElementSibling;
            break;
          }
        }
      }
    }

    // Move the container to the appropriate place on the page
    curPage.insertBefore(draggingAppContainer, followingContainer);
  }

  /**
   * Invoked when an app is dropped on the trash
   * @param {Grabber.Event} e The event from the Grabber indicating the drop.
   */
  function appTrash(e) {
    var appElement = e.grabbedElement;
    assert(appElement.classList.contains('app'));
    var appId = appElement.getAttribute('app-id');
    assert(appId);

    // Mark this drop as handled so that the catch-all drop handler
    // on the document doesn't see this event.
    e.stopPropagation();

    // Tell chrome to uninstall the app (prompting the user)
    chrome.send('uninstallApp', [appId]);
  }

  /**
   * Called when an app is dropped anywhere other than the trash can.  Commits
   * any movement that has occurred.
   * @param {Grabber.Event} e The event from the Grabber indicating the drop.
   */
  function appDrop(e) {
    if (!draggingAppContainer)
      // Drag was aborted (eg. due to an app update) - do nothing
      return;

    // If the app is dropped back into it's original position then do nothing
    assert(draggingAppOriginalPage);
    if (draggingAppContainer.parentNode == draggingAppOriginalPage &&
        draggingAppContainer.nextSibling == draggingAppOriginalPosition)
      return;

    // Determine which app was being dragged
    var appElement = e.grabbedElement;
    assert(appElement.classList.contains('app'));
    var appId = appElement.getAttribute('app-id');
    assert(appId);

    // Update the page index for the app if it's changed.  This doesn't trigger
    // a call to getAppsCallback so we want to do it before reorderApps
    var pageIndex = cardSlider.currentCard;
    assert(pageIndex >= 0 && pageIndex < appsPages.length,
           'page number out of range');
    if (appsPages[pageIndex] != draggingAppOriginalPage)
      chrome.send('setPageIndex', [appId, pageIndex]);

    // Put the app being dragged back into it's container
    draggingAppContainer.appendChild(appElement);

    // Create a list of all appIds in the order now present in the DOM
    var appIds = [];
    for (var page = 0; page < appsPages.length; page++) {
      var appsOnPage = appsPages[page].getElementsByClassName('app');
      for (var i = 0; i < appsOnPage.length; i++) {
        var id = appsOnPage[i].getAttribute('app-id');
        if (id)
          appIds.push(id);
      }
    }

    // We are going to commit this repositioning - clear the original position
    draggingAppOriginalPage = undefined;
    draggingAppOriginalPosition = undefined;

    // Tell chrome to update its database to persist this new order of apps This
    // will cause getAppsCallback to be invoked and the apps to be redrawn.
    chrome.send('reorderApps', [appId, appIds]);
    appMoved = true;
  }

  /**
   * Set to true if we're currently in rearrange mode and an app has
   * been successfully dropped to a new location.  This indicates that
   * a getAppsCallback call is pending and we can rely on the DOM being
   * updated by that.
   * @type {boolean}
   */
  var appMoved = false;

  /**
   * Invoked whenever some app is grabbed
   * @param {Grabber.Event} e The Grabber Grab event.
   */
  function enterRearrangeMode(e)
  {
    // Stop the slider from sliding for this touch
    cardSlider.cancelTouch();

    // Add an extra blank page in case the user wants to create a new page
    createAppPage(true);
    var pageAdded = appsPages.length - 1;
    window.setTimeout(function() {
      dots[pageAdded].classList.remove('new');
    }, 0);

    updateSliderCards();

    // Cause the dot-list to grow
    getRequiredElement('footer').classList.add('rearrange-mode');

    assert(!appMoved, 'appMoved should not be set yet');
  }

  /**
   * Invoked whenever some app is released
   * @param {Grabber.Event} e The Grabber RELEASE event.
   */
  function leaveRearrangeMode(e)
  {
    // Return the dot-list to normal
    getRequiredElement('footer').classList.remove('rearrange-mode');

    // If we didn't successfully re-arrange an app, then we won't be
    // refreshing the app view in getAppCallback and need to explicitly remove
    // the extra empty page we added.  We don't want to do this in the normal
    // case because if we did actually drop an app there, we want to retain that
    // page as our current page number.
    if (!appMoved) {
      assert(appsPages[appsPages.length - 1].
             getElementsByClassName('app-container').length == 0,
             'Last app page should be empty');
      removePage(appsPages.length - 1);
    }
    appMoved = false;
  }

  /**
   * Remove the page with the specified index and update the slider.
   * @param {number} pageNo The index of the page to remove.
   */
  function removePage(pageNo)
  {
    var page = appsPages[pageNo];

    // Remove the page from the DOM
    page.parentNode.removeChild(page);

    // Remove the corresponding dot
    // Need to give it a chance to animate though
    var dot = dots[pageNo];
    dot.classList.add('new');
    window.setTimeout(function() {
      // If we've re-created the apps (eg. because an app was uninstalled) then
      // we will have removed the old dots from the document already, so skip.
      if (dot.parentNode)
        dot.parentNode.removeChild(dot);
    }, DEFAULT_TRANSITION_TIME);

    updateSliderCards();
  }

  // Return an object with all the exports
  return {
    assert: assert,
    appsPrefChangeCallback: appsPrefChangeCallback,
    getAppsCallback: getAppsCallback,
    initialize: initializeNtp
  };
})();

// publish ntp globals
var assert = ntp.assert;
var getAppsCallback = ntp.getAppsCallback;
var appsPrefChangeCallback = ntp.appsPrefChangeCallback;

// Initialize immediately once globals are published (there doesn't seem to be
// any need to wait for DOMContentLoaded)
ntp.initialize();