• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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