Javascript  |  806行  |  27.75 KB

// 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 Slider object to use for changing app pages.
   * @type {Slider|undefined}
   */
  var slider;

  /**
   * 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 newtab.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 slider without any cards at the moment
    var appsFrame = getRequiredElement('apps-frame');
    slider = new Slider(appsFrame, appsPageList, [], 0, appsFrame.offsetWidth);
    slider.initialize();

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

    // Handle the page being changed
    appsPageList.addEventListener(
        Slider.EventType.CARD_CHANGED,
        function(e) {
          // Update the active dot
          var curDot = dotList.getElementsByClassName('selected')[0];
          if (curDot)
            curDot.classList.remove('selected');
          var newPageIndex = e.slider.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[slider.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) {
    // Allow standalone_hack.js to hook this mapping (since chrome:// URLs
    // won't work for a standalone page)
    if (typeof themeUrlMapper == 'function') {
      var u = themeUrlMapper(resourceName);
      if (u)
        return u;
    }
    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 = slider.currentCard;
    if (pageNo >= appsPages.length)
      pageNo = appsPages.length - 1;
    var pageArray = [];
    for (var i = 0; i < appsPages.length; i++)
      pageArray[i] = appsPages[i];
    slider.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) {
      slider.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[slider.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 = slider.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
    slider.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();