Home | History | Annotate | Download | only in ntp4
      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 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 cr.define('ntp4', function() {
     14   'use strict';
     15 
     16   /**
     17    * The CardSlider object to use for changing app pages.
     18    * @type {CardSlider|undefined}
     19    */
     20   var cardSlider;
     21 
     22   /**
     23    * Template to use for creating new 'dot' elements
     24    * @type {!Element|undefined}
     25    */
     26   var dotTemplate;
     27 
     28   /**
     29    * The 'page-list' element.
     30    * @type {!Element|undefined}
     31    */
     32   var pageList;
     33 
     34   /**
     35    * A list of all 'tile-page' elements.
     36    * @type {!NodeList|undefined}
     37    */
     38   var tilePages;
     39 
     40   /**
     41    * The Most Visited page.
     42    * @type {!Element|undefined}
     43    */
     44   var mostVisitedPage;
     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|undefined}
     55    */
     56   var dotList;
     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|undefined}
     69    */
     70   var trash;
     71 
     72   /**
     73    * The time in milliseconds for most transitions.  This should match what's
     74    * in new_tab.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    * Invoked at startup once the DOM is available to initialize the app.
     89    */
     90   function initialize() {
     91     // Load the current theme colors.
     92     themeChanged(false);
     93 
     94     dotList = getRequiredElement('dot-list');
     95     pageList = getRequiredElement('page-list');
     96     trash = getRequiredElement('trash');
     97     trash.hidden = true;
     98 
     99     // Request data on the apps so we can fill them in.
    100     // Note that this is kicked off asynchronously.  'getAppsCallback' will be
    101     // invoked at some point after this function returns.
    102     chrome.send('getApps');
    103 
    104     // Prevent touch events from triggering any sort of native scrolling
    105     document.addEventListener('touchmove', function(e) {
    106       e.preventDefault();
    107     }, true);
    108 
    109     // Get the template elements and remove them from the DOM.  Things are
    110     // simpler if we start with 0 pages and 0 apps and don't leave hidden
    111     // template elements behind in the DOM.
    112     dots = dotList.getElementsByClassName('dot');
    113     assert(dots.length == 1,
    114            'Expected exactly one dot in the dots-list.');
    115     dotTemplate = dots[0];
    116     dotList.removeChild(dots[0]);
    117 
    118     tilePages = pageList.getElementsByClassName('tile-page');
    119     appsPages = pageList.getElementsByClassName('apps-page');
    120 
    121     // Initialize the cardSlider without any cards at the moment
    122     var sliderFrame = getRequiredElement('card-slider-frame');
    123     cardSlider = new CardSlider(sliderFrame, pageList, [], 0,
    124                                 sliderFrame.offsetWidth);
    125     cardSlider.initialize();
    126 
    127     // Ensure the slider is resized appropriately with the window
    128     window.addEventListener('resize', function() {
    129       cardSlider.resize(sliderFrame.offsetWidth);
    130     });
    131 
    132     // Handle the page being changed
    133     pageList.addEventListener(
    134         CardSlider.EventType.CARD_CHANGED,
    135         function(e) {
    136           // Update the active dot
    137           var curDot = dotList.getElementsByClassName('selected')[0];
    138           if (curDot)
    139             curDot.classList.remove('selected');
    140           var newPageIndex = e.cardSlider.currentCard;
    141           dots[newPageIndex].classList.add('selected');
    142           // If an app was being dragged, move it to the end of the new page
    143           if (draggingAppContainer)
    144             appsPages[newPageIndex].appendChild(draggingAppContainer);
    145         });
    146 
    147     // Add a drag handler to the body (for drags that don't land on an existing
    148     // app)
    149     document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter);
    150 
    151     // Handle dropping an app anywhere other than on the trash
    152     document.addEventListener(Grabber.EventType.DROP, appDrop);
    153 
    154     // Add handles to manage the transition into/out-of rearrange mode
    155     // Note that we assume here that we only use a Grabber for moving apps,
    156     // so ANY GRAB event means we're enterring rearrange mode.
    157     sliderFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode);
    158     sliderFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode);
    159 
    160     // Add handlers for the tash can
    161     trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) {
    162       trash.classList.add('hover');
    163       e.grabbedElement.classList.add('trashing');
    164       e.stopPropagation();
    165     });
    166     trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) {
    167       e.grabbedElement.classList.remove('trashing');
    168       trash.classList.remove('hover');
    169     });
    170     trash.addEventListener(Grabber.EventType.DROP, appTrash);
    171 
    172     cr.ui.decorate($('recently-closed-menu-button'), ntp4.RecentMenuButton);
    173     chrome.send('getRecentlyClosedTabs');
    174 
    175     mostVisitedPage = new ntp4.MostVisitedPage('Most Visited');
    176     appendTilePage(mostVisitedPage);
    177     chrome.send('getMostVisited');
    178   }
    179 
    180   /**
    181    * Simple common assertion API
    182    * @param {*} condition The condition to test.  Note that this may be used to
    183    *     test whether a value is defined or not, and we don't want to force a
    184    *     cast to Boolean.
    185    * @param {string=} opt_message A message to use in any error.
    186    */
    187   function assert(condition, opt_message) {
    188     'use strict';
    189     if (!condition) {
    190       var msg = 'Assertion failed';
    191       if (opt_message)
    192         msg = msg + ': ' + opt_message;
    193       throw new Error(msg);
    194     }
    195   }
    196 
    197   /**
    198    * Get an element that's known to exist by its ID. We use this instead of just
    199    * calling getElementById and not checking the result because this lets us
    200    * satisfy the JSCompiler type system.
    201    * @param {string} id The identifier name.
    202    * @return {!Element} the Element.
    203    */
    204   function getRequiredElement(id) {
    205     var element = document.getElementById(id);
    206     assert(element, 'Missing required element: ' + id);
    207     return element;
    208   }
    209 
    210   /**
    211    * Callback invoked by chrome with the apps available.
    212    *
    213    * Note that calls to this function can occur at any time, not just in
    214    * response to a getApps request. For example, when a user installs/uninstalls
    215    * an app on another synchronized devices.
    216    * @param {Object} data An object with all the data on available
    217    *        applications.
    218    */
    219   function getAppsCallback(data) {
    220     // Clean up any existing grabber objects - cancelling any outstanding drag.
    221     // Ideally an async app update wouldn't disrupt an active drag but
    222     // that would require us to re-use existing elements and detect how the apps
    223     // have changed, which would be a lot of work.
    224     // Note that we have to explicitly clean up the grabber objects so they stop
    225     // listening to events and break the DOM<->JS cycles necessary to enable
    226     // collection of all these objects.
    227     grabbers.forEach(function(g) {
    228       // Note that this may raise DRAG_END/RELEASE events to clean up an
    229       // oustanding drag.
    230       g.dispose();
    231     });
    232     assert(!draggingAppContainer && !draggingAppOriginalPosition &&
    233            !draggingAppOriginalPage);
    234     grabbers = [];
    235 
    236     // Clear any existing apps pages and dots.
    237     // TODO(rbyers): It might be nice to preserve animation of dots after an
    238     // uninstall. Could we re-use the existing page and dot elements?  It seems
    239     // unfortunate to have Chrome send us the entire apps list after an
    240     // uninstall.
    241     for (var i = 0; i < appsPages.length; i++) {
    242       var page = appsPages[i];
    243       var dot = page.navigationDot;
    244 
    245       page.tearDown();
    246       page.parentNode.removeChild(page);
    247       dot.parentNode.removeChild(dot);
    248     }
    249 
    250     // Get the array of apps and add any special synthesized entries
    251     var apps = data.apps;
    252 
    253     // Sort by launch index
    254     apps.sort(function(a, b) {
    255       return a.app_launch_index - b.app_launch_index;
    256     });
    257 
    258     // Add the apps, creating pages as necessary
    259     for (var i = 0; i < apps.length; i++) {
    260       var app = apps[i];
    261       var pageIndex = (app.page_index || 0);
    262       while (pageIndex >= appsPages.length) {
    263         var origPageCount = appsPages.length;
    264         appendTilePage(new ntp4.AppsPage('Apps'));
    265         // Confirm that appsPages is a live object, updated when a new page is
    266         // added (otherwise we'd have an infinite loop)
    267         assert(appsPages.length == origPageCount + 1, 'expected new page');
    268       }
    269 
    270       appsPages[pageIndex].appendApp(app);
    271     }
    272 
    273     // Add a couple blank apps pages for testing. TODO(estade): remove this.
    274     appendTilePage(new ntp4.AppsPage('Foo'));
    275     appendTilePage(new ntp4.AppsPage('Bar'));
    276 
    277     // Tell the slider about the pages
    278     updateSliderCards();
    279 
    280     // Mark the current page
    281     dots[cardSlider.currentCard].classList.add('selected');
    282   }
    283 
    284   /**
    285    * Make a synthesized app object representing the chrome web store.  It seems
    286    * like this could just as easily come from the back-end, and then would
    287    * support being rearranged, etc.
    288    * @return {Object} The app object as would be sent from the webui back-end.
    289    */
    290   function makeWebstoreApp() {
    291     return {
    292       id: '',   // Empty ID signifies this is a special synthesized app
    293       page_index: 0,
    294       app_launch_index: -1,   // always first
    295       name: templateData.web_store_title,
    296       launch_url: templateData.web_store_url,
    297       icon_big: getThemeUrl('IDR_WEBSTORE_ICON')
    298     };
    299   }
    300 
    301   /**
    302    * Given a theme resource name, construct a URL for it.
    303    * @param {string} resourceName The name of the resource.
    304    * @return {string} A url which can be used to load the resource.
    305    */
    306   function getThemeUrl(resourceName) {
    307     return 'chrome://theme/' + resourceName;
    308   }
    309 
    310   /**
    311    * Callback invoked by chrome whenever an app preference changes.
    312    * The normal NTP uses this to keep track of the current launch-type of an
    313    * app, updating the choices in the context menu.  We don't have such a menu
    314    * so don't use this at all (but it still needs to be here for chrome to
    315    * call).
    316    * @param {Object} data An object with all the data on available
    317    *        applications.
    318    */
    319   function appsPrefChangeCallback(data) {
    320   }
    321 
    322   /**
    323    * Invoked whenever the pages in apps-page-list have changed so that
    324    * the Slider knows about the new elements.
    325    */
    326   function updateSliderCards() {
    327     var pageNo = cardSlider.currentCard;
    328     if (pageNo >= tilePages.length)
    329       pageNo = tilePages.length - 1;
    330     var pageArray = [];
    331     for (var i = 0; i < tilePages.length; i++)
    332       pageArray[i] = tilePages[i];
    333     cardSlider.setCards(pageArray, pageNo);
    334   }
    335 
    336   /**
    337    * Appends a tile page (for apps or most visited).
    338    *
    339    * @param {TilePage} page The page element.
    340    * @param {boolean=} opt_animate If true, add the class 'new' to the created
    341    *        dot.
    342    */
    343   function appendTilePage(page, opt_animate) {
    344     pageList.appendChild(page);
    345 
    346     // Make a deep copy of the dot template to add a new one.
    347     var dotCount = dots.length;
    348     var newDot = dotTemplate.cloneNode(true);
    349     newDot.querySelector('span').textContent = page.pageName;
    350     if (opt_animate)
    351       newDot.classList.add('new');
    352     dotList.appendChild(newDot);
    353     page.navigationDot = newDot;
    354 
    355     // Add click handler to the dot to change the page.
    356     // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we
    357     // don't rely on synthesized click events, and the change takes effect
    358     // before releasing). However, click events seems to be synthesized for a
    359     // region outside the border, and a 10px box is too small to require touch
    360     // events to fall inside of. We could get around this by adding a box around
    361     // the dot for accepting the touch events.
    362     function switchPage(e) {
    363       cardSlider.selectCard(dotCount, true);
    364       e.stopPropagation();
    365     }
    366     newDot.addEventListener('click', switchPage);
    367 
    368     // Change pages whenever an app is dragged over a dot.
    369     newDot.addEventListener(Grabber.EventType.DRAG_ENTER, switchPage);
    370   }
    371   /**
    372    * Search an elements ancestor chain for the nearest element that is a member
    373    * of the specified class.
    374    * @param {!Element} element The element to start searching from.
    375    * @param {string} className The name of the class to locate.
    376    * @return {Element} The first ancestor of the specified class or null.
    377    */
    378   function getParentByClassName(element, className) {
    379     for (var e = element; e; e = e.parentElement) {
    380       if (e.classList.contains(className))
    381         return e;
    382     }
    383     return null;
    384   }
    385 
    386   /**
    387    * The container where the app currently being dragged came from.
    388    * @type {!Element|undefined}
    389    */
    390   var draggingAppContainer;
    391 
    392   /**
    393    * The apps-page that the app currently being dragged camed from.
    394    * @type {!Element|undefined}
    395    */
    396   var draggingAppOriginalPage;
    397 
    398   /**
    399    * The element that was originally after the app currently being dragged (or
    400    * null if it was the last on the page).
    401    * @type {!Element|undefined}
    402    */
    403   var draggingAppOriginalPosition;
    404 
    405   /**
    406    * Invoked when app dragging begins.
    407    * @param {Grabber.Event} e The event from the Grabber indicating the drag.
    408    */
    409   function appDragStart(e) {
    410     // Pull the element out to the sliderFrame using fixed positioning. This
    411     // ensures that the app is not affected (remains under the finger) if the
    412     // slider changes cards and is translated.  An alternate approach would be
    413     // to use fixed positioning for the slider (so that changes to its position
    414     // don't affect children that aren't positioned relative to it), but we
    415     // don't yet have GPU acceleration for this.
    416     var element = e.grabbedElement;
    417 
    418     var pos = element.getBoundingClientRect();
    419     element.style.webkitTransform = '';
    420 
    421     element.style.position = 'fixed';
    422     // Don't want to zoom around the middle since the left/top co-ordinates
    423     // are post-transform values.
    424     element.style.webkitTransformOrigin = 'left top';
    425     element.style.left = pos.left + 'px';
    426     element.style.top = pos.top + 'px';
    427 
    428     // Keep track of what app is being dragged and where it came from
    429     assert(!draggingAppContainer, 'got DRAG_START without DRAG_END');
    430     draggingAppContainer = element.parentNode;
    431     assert(draggingAppContainer.classList.contains('app-container'));
    432     draggingAppOriginalPosition = draggingAppContainer.nextSibling;
    433     draggingAppOriginalPage = draggingAppContainer.parentNode;
    434 
    435     // Move the app out of the container
    436     // Note that appendChild also removes the element from its current parent.
    437     sliderFrame.appendChild(element);
    438   }
    439 
    440   /**
    441    * Invoked when app dragging terminates (either successfully or not)
    442    * @param {Grabber.Event} e The event from the Grabber.
    443    */
    444   function appDragEnd(e) {
    445     // Stop floating the app
    446     var appBeingDragged = e.grabbedElement;
    447     assert(appBeingDragged.classList.contains('app'));
    448     appBeingDragged.style.position = '';
    449     appBeingDragged.style.webkitTransformOrigin = '';
    450     appBeingDragged.style.left = '';
    451     appBeingDragged.style.top = '';
    452 
    453     // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE
    454     // for it - eg. if we drop on it, or the drag is cancelled)
    455     trash.classList.remove('hover');
    456     appBeingDragged.classList.remove('trashing');
    457 
    458     // If we have an active drag (i.e. it wasn't aborted by an app update)
    459     if (draggingAppContainer) {
    460       // Put the app back into it's container
    461       if (appBeingDragged.parentNode != draggingAppContainer)
    462         draggingAppContainer.appendChild(appBeingDragged);
    463 
    464       // If we care about the container's original position
    465       if (draggingAppOriginalPage)
    466       {
    467         // Then put the container back where it came from
    468         if (draggingAppOriginalPosition) {
    469           draggingAppOriginalPage.insertBefore(draggingAppContainer,
    470                                                draggingAppOriginalPosition);
    471         } else {
    472           draggingAppOriginalPage.appendChild(draggingAppContainer);
    473         }
    474       }
    475     }
    476 
    477     draggingAppContainer = undefined;
    478     draggingAppOriginalPage = undefined;
    479     draggingAppOriginalPosition = undefined;
    480   }
    481 
    482   /**
    483    * Invoked when an app is dragged over another app.  Updates the DOM to affect
    484    * the rearrangement (but doesn't commit the change until the app is dropped).
    485    * @param {Grabber.Event} e The event from the Grabber indicating the drag.
    486    */
    487   function appDragEnter(e)
    488   {
    489     assert(draggingAppContainer, 'expected stored container');
    490     var sourceContainer = draggingAppContainer;
    491 
    492     // Ensure enter events delivered to an app-container don't also get
    493     // delivered to the document.
    494     e.stopPropagation();
    495 
    496     var curPage = appsPages[cardSlider.currentCard];
    497     var followingContainer = null;
    498 
    499     // If we dragged over a specific app, determine which one to insert before
    500     if (e.currentTarget != document) {
    501 
    502       // Start by assuming we'll insert the app before the one dragged over
    503       followingContainer = e.currentTarget;
    504       assert(followingContainer.classList.contains('app-container'),
    505              'expected drag over container');
    506       assert(followingContainer.parentNode == curPage);
    507       if (followingContainer == draggingAppContainer)
    508         return;
    509 
    510       // But if it's after the current container position then we'll need to
    511       // move ahead by one to account for the container being removed.
    512       if (curPage == draggingAppContainer.parentNode) {
    513         for (var c = draggingAppContainer; c; c = c.nextElementSibling) {
    514           if (c == followingContainer) {
    515             followingContainer = followingContainer.nextElementSibling;
    516             break;
    517           }
    518         }
    519       }
    520     }
    521 
    522     // Move the container to the appropriate place on the page
    523     curPage.insertBefore(draggingAppContainer, followingContainer);
    524   }
    525 
    526   /**
    527    * Invoked when an app is dropped on the trash
    528    * @param {Grabber.Event} e The event from the Grabber indicating the drop.
    529    */
    530   function appTrash(e) {
    531     var appElement = e.grabbedElement;
    532     assert(appElement.classList.contains('app'));
    533     var appId = appElement.getAttribute('app-id');
    534     assert(appId);
    535 
    536     // Mark this drop as handled so that the catch-all drop handler
    537     // on the document doesn't see this event.
    538     e.stopPropagation();
    539 
    540     // Tell chrome to uninstall the app (prompting the user)
    541     chrome.send('uninstallApp', [appId]);
    542   }
    543 
    544   /**
    545    * Called when an app is dropped anywhere other than the trash can.  Commits
    546    * any movement that has occurred.
    547    * @param {Grabber.Event} e The event from the Grabber indicating the drop.
    548    */
    549   function appDrop(e) {
    550     if (!draggingAppContainer)
    551       // Drag was aborted (eg. due to an app update) - do nothing
    552       return;
    553 
    554     // If the app is dropped back into it's original position then do nothing
    555     assert(draggingAppOriginalPage);
    556     if (draggingAppContainer.parentNode == draggingAppOriginalPage &&
    557         draggingAppContainer.nextSibling == draggingAppOriginalPosition)
    558       return;
    559 
    560     // Determine which app was being dragged
    561     var appElement = e.grabbedElement;
    562     assert(appElement.classList.contains('app'));
    563     var appId = appElement.getAttribute('app-id');
    564     assert(appId);
    565 
    566     // Update the page index for the app if it's changed.  This doesn't trigger
    567     // a call to getAppsCallback so we want to do it before reorderApps
    568     var pageIndex = cardSlider.currentCard;
    569     assert(pageIndex >= 0 && pageIndex < appsPages.length,
    570            'page number out of range');
    571     if (appsPages[pageIndex] != draggingAppOriginalPage)
    572       chrome.send('setPageIndex', [appId, pageIndex]);
    573 
    574     // Put the app being dragged back into it's container
    575     draggingAppContainer.appendChild(appElement);
    576 
    577     // Create a list of all appIds in the order now present in the DOM
    578     var appIds = [];
    579     for (var page = 0; page < appsPages.length; page++) {
    580       var appsOnPage = appsPages[page].getElementsByClassName('app');
    581       for (var i = 0; i < appsOnPage.length; i++) {
    582         var id = appsOnPage[i].getAttribute('app-id');
    583         if (id)
    584           appIds.push(id);
    585       }
    586     }
    587 
    588     // We are going to commit this repositioning - clear the original position
    589     draggingAppOriginalPage = undefined;
    590     draggingAppOriginalPosition = undefined;
    591 
    592     // Tell chrome to update its database to persist this new order of apps This
    593     // will cause getAppsCallback to be invoked and the apps to be redrawn.
    594     chrome.send('reorderApps', [appId, appIds]);
    595     appMoved = true;
    596   }
    597 
    598   /**
    599    * Set to true if we're currently in rearrange mode and an app has
    600    * been successfully dropped to a new location.  This indicates that
    601    * a getAppsCallback call is pending and we can rely on the DOM being
    602    * updated by that.
    603    * @type {boolean}
    604    */
    605   var appMoved = false;
    606 
    607   /**
    608    * Invoked whenever some app is grabbed
    609    * @param {Grabber.Event} e The Grabber Grab event.
    610    */
    611   function enterRearrangeMode(e)
    612   {
    613     // Stop the slider from sliding for this touch
    614     cardSlider.cancelTouch();
    615 
    616     // Add an extra blank page in case the user wants to create a new page
    617     appendTilePage(new ntp4.AppsPage(''), true);
    618     var pageAdded = appsPages.length - 1;
    619     window.setTimeout(function() {
    620       dots[pageAdded].classList.remove('new');
    621     }, 0);
    622 
    623     updateSliderCards();
    624 
    625     // Cause the dot-list to grow
    626     getRequiredElement('footer').classList.add('rearrange-mode');
    627 
    628     assert(!appMoved, 'appMoved should not be set yet');
    629   }
    630 
    631   /**
    632    * Invoked whenever some app is released
    633    * @param {Grabber.Event} e The Grabber RELEASE event.
    634    */
    635   function leaveRearrangeMode(e)
    636   {
    637     // Return the dot-list to normal
    638     getRequiredElement('footer').classList.remove('rearrange-mode');
    639 
    640     // If we didn't successfully re-arrange an app, then we won't be
    641     // refreshing the app view in getAppCallback and need to explicitly remove
    642     // the extra empty page we added.  We don't want to do this in the normal
    643     // case because if we did actually drop an app there, we want to retain that
    644     // page as our current page number.
    645     if (!appMoved) {
    646       assert(appsPages[appsPages.length - 1].
    647              getElementsByClassName('app-container').length == 0,
    648              'Last app page should be empty');
    649       removePage(appsPages.length - 1);
    650     }
    651     appMoved = false;
    652   }
    653 
    654   /**
    655    * Remove the page with the specified index and update the slider.
    656    * @param {number} pageNo The index of the page to remove.
    657    */
    658   function removePage(pageNo) {
    659     pageList.removeChild(tilePages[pageNo]);
    660 
    661     // Remove the corresponding dot
    662     // Need to give it a chance to animate though
    663     var dot = dots[pageNo];
    664     dot.classList.add('new');
    665     window.setTimeout(function() {
    666       // If we've re-created the apps (eg. because an app was uninstalled) then
    667       // we will have removed the old dots from the document already, so skip.
    668       if (dot.parentNode)
    669         dot.parentNode.removeChild(dot);
    670     }, DEFAULT_TRANSITION_TIME);
    671 
    672     updateSliderCards();
    673   }
    674 
    675   // TODO(estade): remove |hasAttribution|.
    676   // TODO(estade): rename newtab.css to new_tab_theme.css
    677   function themeChanged(hasAttribution) {
    678     $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now();
    679   }
    680 
    681   function setRecentlyClosedTabs(dataItems) {
    682     $('recently-closed-menu-button').dataItems = dataItems;
    683   }
    684 
    685   function setMostVisitedPages(data, firstRun, hasBlacklistedUrls) {
    686     mostVisitedPage.data = data;
    687   }
    688 
    689   // Return an object with all the exports
    690   return {
    691     assert: assert,
    692     appsPrefChangeCallback: appsPrefChangeCallback,
    693     getAppsCallback: getAppsCallback,
    694     initialize: initialize,
    695     themeChanged: themeChanged,
    696     setRecentlyClosedTabs: setRecentlyClosedTabs,
    697     setMostVisitedPages: setMostVisitedPages,
    698   };
    699 });
    700 
    701 // publish ntp globals
    702 // TODO(estade): update the content handlers to use ntp namespace instead of
    703 // making these global.
    704 var assert = ntp4.assert;
    705 var getAppsCallback = ntp4.getAppsCallback;
    706 var appsPrefChangeCallback = ntp4.appsPrefChangeCallback;
    707 var themeChanged = ntp4.themeChanged;
    708 var recentlyClosedTabs = ntp4.setRecentlyClosedTabs;
    709 var mostVisitedPages = ntp4.setMostVisitedPages;
    710 
    711 document.addEventListener('DOMContentLoaded', ntp4.initialize);
    712