Home | History | Annotate | Download | only in ntp4
      1 // Copyright (c) 2012 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('ntp', function() {
     14   'use strict';
     15 
     16   /**
     17    * NewTabView instance.
     18    * @type {!Object|undefined}
     19    */
     20   var newTabView;
     21 
     22   /**
     23    * The 'notification-container' element.
     24    * @type {!Element|undefined}
     25    */
     26   var notificationContainer;
     27 
     28   /**
     29    * If non-null, an info bubble for showing messages to the user. It points at
     30    * the Most Visited label, and is used to draw more attention to the
     31    * navigation dot UI.
     32    * @type {!Element|undefined}
     33    */
     34   var promoBubble;
     35 
     36   /**
     37    * If non-null, an bubble confirming that the user has signed into sync. It
     38    * points at the login status at the top of the page.
     39    * @type {!Element|undefined}
     40    */
     41   var loginBubble;
     42 
     43   /**
     44    * true if |loginBubble| should be shown.
     45    * @type {boolean}
     46    */
     47   var shouldShowLoginBubble = false;
     48 
     49   /**
     50    * The 'other-sessions-menu-button' element.
     51    * @type {!Element|undefined}
     52    */
     53   var otherSessionsButton;
     54 
     55   /**
     56    * The time when all sections are ready.
     57    * @type {number|undefined}
     58    * @private
     59    */
     60   var startTime;
     61 
     62   /**
     63    * The time in milliseconds for most transitions.  This should match what's
     64    * in new_tab.css.  Unfortunately there's no better way to try to time
     65    * something to occur until after a transition has completed.
     66    * @type {number}
     67    * @const
     68    */
     69   var DEFAULT_TRANSITION_TIME = 500;
     70 
     71   /**
     72    * See description for these values in ntp_stats.h.
     73    * @enum {number}
     74    */
     75   var NtpFollowAction = {
     76     CLICKED_TILE: 11,
     77     CLICKED_OTHER_NTP_PANE: 12,
     78     OTHER: 13
     79   };
     80 
     81   /**
     82    * Creates a NewTabView object. NewTabView extends PageListView with
     83    * new tab UI specific logics.
     84    * @constructor
     85    * @extends {PageListView}
     86    */
     87   function NewTabView() {
     88     var pageSwitcherStart = null;
     89     var pageSwitcherEnd = null;
     90     if (loadTimeData.getValue('showApps')) {
     91       pageSwitcherStart = getRequiredElement('page-switcher-start');
     92       pageSwitcherEnd = getRequiredElement('page-switcher-end');
     93     }
     94     this.initialize(getRequiredElement('page-list'),
     95                     getRequiredElement('dot-list'),
     96                     getRequiredElement('card-slider-frame'),
     97                     getRequiredElement('trash'),
     98                     pageSwitcherStart, pageSwitcherEnd);
     99   }
    100 
    101   NewTabView.prototype = {
    102     __proto__: ntp.PageListView.prototype,
    103 
    104     /** @override */
    105     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
    106       ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
    107 
    108       if (promoBubble)
    109         window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
    110     }
    111   };
    112 
    113   /**
    114    * Invoked at startup once the DOM is available to initialize the app.
    115    */
    116   function onLoad() {
    117     sectionsToWaitFor = 0;
    118     if (loadTimeData.getBoolean('showMostvisited'))
    119       sectionsToWaitFor++;
    120     if (loadTimeData.getBoolean('showApps')) {
    121       sectionsToWaitFor++;
    122       if (loadTimeData.getBoolean('showAppLauncherPromo')) {
    123         $('app-launcher-promo-close-button').addEventListener('click',
    124             function() { chrome.send('stopShowingAppLauncherPromo'); });
    125         $('apps-promo-learn-more').addEventListener('click',
    126             function() { chrome.send('onLearnMore'); });
    127       }
    128     }
    129     if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled'))
    130       sectionsToWaitFor++;
    131     measureNavDots();
    132 
    133     // Load the current theme colors.
    134     themeChanged();
    135 
    136     newTabView = new NewTabView();
    137 
    138     notificationContainer = getRequiredElement('notification-container');
    139     notificationContainer.addEventListener(
    140         'webkitTransitionEnd', onNotificationTransitionEnd);
    141 
    142     if (loadTimeData.getBoolean('showRecentlyClosed')) {
    143       cr.ui.decorate($('recently-closed-menu-button'), ntp.RecentMenuButton);
    144       chrome.send('getRecentlyClosedTabs');
    145     } else {
    146       $('recently-closed-menu-button').hidden = true;
    147     }
    148 
    149     if (loadTimeData.getBoolean('showOtherSessionsMenu')) {
    150       otherSessionsButton = getRequiredElement('other-sessions-menu-button');
    151       cr.ui.decorate(otherSessionsButton, ntp.OtherSessionsMenuButton);
    152       otherSessionsButton.initialize(loadTimeData.getBoolean('isUserSignedIn'));
    153     } else {
    154       getRequiredElement('other-sessions-menu-button').hidden = true;
    155     }
    156 
    157     if (loadTimeData.getBoolean('showMostvisited')) {
    158       var mostVisited = new ntp.MostVisitedPage();
    159       // Move the footer into the most visited page if we are in "bare minimum"
    160       // mode.
    161       if (document.body.classList.contains('bare-minimum'))
    162         mostVisited.appendFooter(getRequiredElement('footer'));
    163       newTabView.appendTilePage(mostVisited,
    164                                 loadTimeData.getString('mostvisited'),
    165                                 false);
    166       chrome.send('getMostVisited');
    167     }
    168 
    169     if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled')) {
    170       var suggestionsScript = document.createElement('script');
    171       suggestionsScript.src = 'suggestions_page.js';
    172       suggestionsScript.onload = function() {
    173          newTabView.appendTilePage(new ntp.SuggestionsPage(),
    174                                    loadTimeData.getString('suggestions'),
    175                                    false,
    176                                    (newTabView.appsPages.length > 0) ?
    177                                        newTabView.appsPages[0] : null);
    178          chrome.send('getSuggestions');
    179          cr.dispatchSimpleEvent(document, 'sectionready', true, true);
    180       };
    181       document.querySelector('head').appendChild(suggestionsScript);
    182     }
    183 
    184     if (!loadTimeData.getBoolean('showWebStoreIcon')) {
    185       var webStoreIcon = $('chrome-web-store-link');
    186       // Not all versions of the NTP have a footer, so this may not exist.
    187       if (webStoreIcon)
    188         webStoreIcon.hidden = true;
    189     } else {
    190       var webStoreLink = loadTimeData.getString('webStoreLink');
    191       var url = appendParam(webStoreLink, 'utm_source', 'chrome-ntp-launcher');
    192       $('chrome-web-store-link').href = url;
    193       $('chrome-web-store-link').addEventListener('click',
    194           onChromeWebStoreButtonClick);
    195     }
    196 
    197     // We need to wait for all the footer menu setup to be completed before
    198     // we can compute its layout.
    199     layoutFooter();
    200 
    201     if (loadTimeData.getString('login_status_message')) {
    202       loginBubble = new cr.ui.Bubble;
    203       loginBubble.anchorNode = $('login-container');
    204       loginBubble.arrowLocation = cr.ui.ArrowLocation.TOP_END;
    205       loginBubble.bubbleAlignment =
    206           cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE;
    207       loginBubble.deactivateToDismissDelay = 2000;
    208       loginBubble.closeButtonVisible = false;
    209 
    210       $('login-status-advanced').onclick = function() {
    211         chrome.send('showAdvancedLoginUI');
    212       };
    213       $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
    214 
    215       var bubbleContent = $('login-status-bubble-contents');
    216       loginBubble.content = bubbleContent;
    217 
    218       // The anchor node won't be updated until updateLogin is called so don't
    219       // show the bubble yet.
    220       shouldShowLoginBubble = true;
    221     }
    222 
    223     if (loadTimeData.valueExists('bubblePromoText')) {
    224       promoBubble = new cr.ui.Bubble;
    225       promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor');
    226       promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START;
    227       promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE;
    228       promoBubble.deactivateToDismissDelay = 2000;
    229       promoBubble.content = parseHtmlSubset(
    230           loadTimeData.getString('bubblePromoText'), ['BR']);
    231 
    232       var bubbleLink = promoBubble.querySelector('a');
    233       if (bubbleLink) {
    234         bubbleLink.addEventListener('click', function(e) {
    235           chrome.send('bubblePromoLinkClicked');
    236         });
    237       }
    238 
    239       promoBubble.handleCloseEvent = function() {
    240         promoBubble.hide();
    241         chrome.send('bubblePromoClosed');
    242       };
    243       promoBubble.show();
    244       chrome.send('bubblePromoViewed');
    245     }
    246 
    247     var loginContainer = getRequiredElement('login-container');
    248     loginContainer.addEventListener('click', showSyncLoginUI);
    249     if (loadTimeData.getBoolean('shouldShowSyncLogin'))
    250       chrome.send('initializeSyncLogin');
    251 
    252     doWhenAllSectionsReady(function() {
    253       // Tell the slider about the pages.
    254       newTabView.updateSliderCards();
    255       // Mark the current page.
    256       newTabView.cardSlider.currentCardValue.navigationDot.classList.add(
    257           'selected');
    258 
    259       if (loadTimeData.valueExists('notificationPromoText')) {
    260         var promoText = loadTimeData.getString('notificationPromoText');
    261         var tags = ['IMG'];
    262         var attrs = {
    263           src: function(node, value) {
    264             return node.tagName == 'IMG' &&
    265                    /^data\:image\/(?:png|gif|jpe?g)/.test(value);
    266           },
    267         };
    268 
    269         var promo = parseHtmlSubset(promoText, tags, attrs);
    270         var promoLink = promo.querySelector('a');
    271         if (promoLink) {
    272           promoLink.addEventListener('click', function(e) {
    273             chrome.send('notificationPromoLinkClicked');
    274           });
    275         }
    276 
    277         showNotification(promo, [], function() {
    278           chrome.send('notificationPromoClosed');
    279         }, 60000);
    280         chrome.send('notificationPromoViewed');
    281       }
    282 
    283       cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
    284       document.documentElement.classList.remove('starting-up');
    285 
    286       startTime = Date.now();
    287     });
    288 
    289     preventDefaultOnPoundLinkClicks();  // From webui/js/util.js.
    290     cr.ui.FocusManager.disableMouseFocusOnButtons();
    291   }
    292 
    293   /**
    294    * Launches the chrome web store app with the chrome-ntp-launcher
    295    * source.
    296    * @param {Event} e The click event.
    297    */
    298   function onChromeWebStoreButtonClick(e) {
    299     chrome.send('recordAppLaunchByURL',
    300                 [encodeURIComponent(this.href),
    301                  ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
    302   }
    303 
    304   /*
    305    * The number of sections to wait on.
    306    * @type {number}
    307    */
    308   var sectionsToWaitFor = -1;
    309 
    310   /**
    311    * Queued callbacks which lie in wait for all sections to be ready.
    312    * @type {array}
    313    */
    314   var readyCallbacks = [];
    315 
    316   /**
    317    * Fired as each section of pages becomes ready.
    318    * @param {Event} e Each page's synthetic DOM event.
    319    */
    320   document.addEventListener('sectionready', function(e) {
    321     if (--sectionsToWaitFor <= 0) {
    322       while (readyCallbacks.length) {
    323         readyCallbacks.shift()();
    324       }
    325     }
    326   });
    327 
    328   /**
    329    * This is used to simulate a fire-once event (i.e. $(document).ready() in
    330    * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
    331    * is fired right away. If all pages are not ready yet, the function is queued
    332    * for later execution.
    333    * @param {function} callback The work to be done when ready.
    334    */
    335   function doWhenAllSectionsReady(callback) {
    336     assert(typeof callback == 'function');
    337     if (sectionsToWaitFor > 0)
    338       readyCallbacks.push(callback);
    339     else
    340       window.setTimeout(callback, 0);  // Do soon after, but asynchronously.
    341   }
    342 
    343   /**
    344    * Measure the width of a nav dot with a given title.
    345    * @param {string} id The loadTimeData ID of the desired title.
    346    * @return {number} The width of the nav dot.
    347    */
    348   function measureNavDot(id) {
    349     var measuringDiv = $('fontMeasuringDiv');
    350     measuringDiv.textContent = loadTimeData.getString(id);
    351     // The 4 is for border and padding.
    352     return Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
    353   }
    354 
    355   /**
    356    * Fills in an invisible div with the longest dot title string so that
    357    * its length may be measured and the nav dots sized accordingly.
    358    */
    359   function measureNavDots() {
    360     var pxWidth = measureNavDot('appDefaultPageName');
    361     if (loadTimeData.getBoolean('showMostvisited'))
    362       pxWidth = Math.max(measureNavDot('mostvisited'), pxWidth);
    363 
    364     var styleElement = document.createElement('style');
    365     styleElement.type = 'text/css';
    366     // max-width is used because if we run out of space, the nav dots will be
    367     // shrunk.
    368     styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
    369     document.querySelector('head').appendChild(styleElement);
    370   }
    371 
    372   /**
    373    * Layout the footer so that the nav dots stay centered.
    374    */
    375   function layoutFooter() {
    376     // We need the image to be loaded.
    377     var logo = $('logo-img');
    378     var logoImg = logo.querySelector('img');
    379     if (!logoImg.complete) {
    380       logoImg.onload = layoutFooter;
    381       return;
    382     }
    383 
    384     var menu = $('footer-menu-container');
    385     if (menu.clientWidth > logoImg.width)
    386       logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px';
    387     else
    388       menu.style.WebkitFlex = '0 1 ' + logoImg.width + 'px';
    389   }
    390 
    391   function themeChanged(opt_hasAttribution) {
    392     $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
    393 
    394     if (typeof opt_hasAttribution != 'undefined') {
    395       document.documentElement.setAttribute('hasattribution',
    396                                             opt_hasAttribution);
    397     }
    398 
    399     updateAttribution();
    400   }
    401 
    402   function setBookmarkBarAttached(attached) {
    403     document.documentElement.setAttribute('bookmarkbarattached', attached);
    404   }
    405 
    406   /**
    407    * Attributes the attribution image at the bottom left.
    408    */
    409   function updateAttribution() {
    410     var attribution = $('attribution');
    411     if (document.documentElement.getAttribute('hasattribution') == 'true') {
    412       attribution.hidden = false;
    413     } else {
    414       attribution.hidden = true;
    415     }
    416   }
    417 
    418   /**
    419    * Timeout ID.
    420    * @type {number}
    421    */
    422   var notificationTimeout = 0;
    423 
    424   /**
    425    * Shows the notification bubble.
    426    * @param {string|Node} message The notification message or node to use as
    427    *     message.
    428    * @param {Array.<{text: string, action: function()}>} links An array of
    429    *     records describing the links in the notification. Each record should
    430    *     have a 'text' attribute (the display string) and an 'action' attribute
    431    *     (a function to run when the link is activated).
    432    * @param {Function} opt_closeHandler The callback invoked if the user
    433    *     manually dismisses the notification.
    434    */
    435   function showNotification(message, links, opt_closeHandler, opt_timeout) {
    436     window.clearTimeout(notificationTimeout);
    437 
    438     var span = document.querySelector('#notification > span');
    439     if (typeof message == 'string') {
    440       span.textContent = message;
    441     } else {
    442       span.textContent = '';  // Remove all children.
    443       span.appendChild(message);
    444     }
    445 
    446     var linksBin = $('notificationLinks');
    447     linksBin.textContent = '';
    448     for (var i = 0; i < links.length; i++) {
    449       var link = linksBin.ownerDocument.createElement('div');
    450       link.textContent = links[i].text;
    451       link.action = links[i].action;
    452       link.onclick = function() {
    453         this.action();
    454         hideNotification();
    455       };
    456       link.setAttribute('role', 'button');
    457       link.setAttribute('tabindex', 0);
    458       link.className = 'link-button';
    459       linksBin.appendChild(link);
    460     }
    461 
    462     function closeFunc(e) {
    463       if (opt_closeHandler)
    464         opt_closeHandler();
    465       hideNotification();
    466     }
    467 
    468     document.querySelector('#notification button').onclick = closeFunc;
    469     document.addEventListener('dragstart', closeFunc);
    470 
    471     notificationContainer.hidden = false;
    472     showNotificationOnCurrentPage();
    473 
    474     newTabView.cardSlider.frame.addEventListener(
    475         'cardSlider:card_change_ended', onCardChangeEnded);
    476 
    477     var timeout = opt_timeout || 10000;
    478     notificationTimeout = window.setTimeout(hideNotification, timeout);
    479   }
    480 
    481   /**
    482    * Hide the notification bubble.
    483    */
    484   function hideNotification() {
    485     notificationContainer.classList.add('inactive');
    486 
    487     newTabView.cardSlider.frame.removeEventListener(
    488         'cardSlider:card_change_ended', onCardChangeEnded);
    489   }
    490 
    491   /**
    492    * Happens when 1 or more consecutive card changes end.
    493    * @param {Event} e The cardSlider:card_change_ended event.
    494    */
    495   function onCardChangeEnded(e) {
    496     // If we ended on the same page as we started, ignore.
    497     if (newTabView.cardSlider.currentCardValue.notification)
    498       return;
    499 
    500     // Hide the notification the old page.
    501     notificationContainer.classList.add('card-changed');
    502 
    503     showNotificationOnCurrentPage();
    504   }
    505 
    506   /**
    507    * Move and show the notification on the current page.
    508    */
    509   function showNotificationOnCurrentPage() {
    510     var page = newTabView.cardSlider.currentCardValue;
    511     doWhenAllSectionsReady(function() {
    512       if (page != newTabView.cardSlider.currentCardValue)
    513         return;
    514 
    515       // NOTE: This moves the notification to inside of the current page.
    516       page.notification = notificationContainer;
    517 
    518       // Reveal the notification and instruct it to hide itself if ignored.
    519       notificationContainer.classList.remove('inactive');
    520 
    521       // Gives the browser time to apply this rule before we remove it (causing
    522       // a transition).
    523       window.setTimeout(function() {
    524         notificationContainer.classList.remove('card-changed');
    525       }, 0);
    526     });
    527   }
    528 
    529   /**
    530    * When done fading out, set hidden to true so the notification can't be
    531    * tabbed to or clicked.
    532    * @param {Event} e The webkitTransitionEnd event.
    533    */
    534   function onNotificationTransitionEnd(e) {
    535     if (notificationContainer.classList.contains('inactive'))
    536       notificationContainer.hidden = true;
    537   }
    538 
    539   function setRecentlyClosedTabs(dataItems) {
    540     $('recently-closed-menu-button').dataItems = dataItems;
    541     layoutFooter();
    542   }
    543 
    544   function setMostVisitedPages(data, hasBlacklistedUrls) {
    545     newTabView.mostVisitedPage.data = data;
    546     cr.dispatchSimpleEvent(document, 'sectionready', true, true);
    547   }
    548 
    549   function setSuggestionsPages(data, hasBlacklistedUrls) {
    550     newTabView.suggestionsPage.data = data;
    551   }
    552 
    553   /**
    554    * Set the dominant color for a node. This will be called in response to
    555    * getFaviconDominantColor. The node represented by |id| better have a setter
    556    * for stripeColor.
    557    * @param {string} id The ID of a node.
    558    * @param {string} color The color represented as a CSS string.
    559    */
    560   function setFaviconDominantColor(id, color) {
    561     var node = $(id);
    562     if (node)
    563       node.stripeColor = color;
    564   }
    565 
    566   /**
    567    * Updates the text displayed in the login container. If there is no text then
    568    * the login container is hidden.
    569    * @param {string} loginHeader The first line of text.
    570    * @param {string} loginSubHeader The second line of text.
    571    * @param {string} iconURL The url for the login status icon. If this is null
    572         then the login status icon is hidden.
    573    * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
    574    */
    575   function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) {
    576     if (loginHeader || loginSubHeader) {
    577       $('login-container').hidden = false;
    578       $('login-status-header').innerHTML = loginHeader;
    579       $('login-status-sub-header').innerHTML = loginSubHeader;
    580       $('card-slider-frame').classList.add('showing-login-area');
    581 
    582       if (iconURL) {
    583         $('login-status-header-container').style.backgroundImage = url(iconURL);
    584         $('login-status-header-container').classList.add('login-status-icon');
    585       } else {
    586         $('login-status-header-container').style.backgroundImage = 'none';
    587         $('login-status-header-container').classList.remove(
    588             'login-status-icon');
    589       }
    590     } else {
    591       $('login-container').hidden = true;
    592       $('card-slider-frame').classList.remove('showing-login-area');
    593     }
    594     if (shouldShowLoginBubble) {
    595       window.setTimeout(loginBubble.show.bind(loginBubble), 0);
    596       chrome.send('loginMessageSeen');
    597       shouldShowLoginBubble = false;
    598     } else if (loginBubble) {
    599       loginBubble.reposition();
    600     }
    601     if (otherSessionsButton) {
    602       otherSessionsButton.updateSignInState(isUserSignedIn);
    603       layoutFooter();
    604     }
    605   }
    606 
    607   /**
    608    * Show the sync login UI.
    609    * @param {Event} e The click event.
    610    */
    611   function showSyncLoginUI(e) {
    612     var rect = e.currentTarget.getBoundingClientRect();
    613     chrome.send('showSyncLoginUI',
    614                 [rect.left, rect.top, rect.width, rect.height]);
    615   }
    616 
    617   /**
    618    * Logs the time to click for the specified item.
    619    * @param {string} item The item to log the time-to-click.
    620    */
    621   function logTimeToClick(item) {
    622     var timeToClick = Date.now() - startTime;
    623     chrome.send('logTimeToClick',
    624         ['NewTabPage.TimeToClick' + item, timeToClick]);
    625   }
    626 
    627   /**
    628    * Wrappers to forward the callback to corresponding PageListView member.
    629    */
    630   function appAdded() {
    631     return newTabView.appAdded.apply(newTabView, arguments);
    632   }
    633 
    634   function appMoved() {
    635     return newTabView.appMoved.apply(newTabView, arguments);
    636   }
    637 
    638   function appRemoved() {
    639     return newTabView.appRemoved.apply(newTabView, arguments);
    640   }
    641 
    642   function appsPrefChangeCallback() {
    643     return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
    644   }
    645 
    646   function appLauncherPromoPrefChangeCallback() {
    647     return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView,
    648                                                                arguments);
    649   }
    650 
    651   function appsReordered() {
    652     return newTabView.appsReordered.apply(newTabView, arguments);
    653   }
    654 
    655   function enterRearrangeMode() {
    656     return newTabView.enterRearrangeMode.apply(newTabView, arguments);
    657   }
    658 
    659   function setForeignSessions(sessionList, isTabSyncEnabled) {
    660     if (otherSessionsButton) {
    661       otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
    662       layoutFooter();
    663     }
    664   }
    665 
    666   function getAppsCallback() {
    667     return newTabView.getAppsCallback.apply(newTabView, arguments);
    668   }
    669 
    670   function getAppsPageIndex() {
    671     return newTabView.getAppsPageIndex.apply(newTabView, arguments);
    672   }
    673 
    674   function getCardSlider() {
    675     return newTabView.cardSlider;
    676   }
    677 
    678   function leaveRearrangeMode() {
    679     return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
    680   }
    681 
    682   function saveAppPageName() {
    683     return newTabView.saveAppPageName.apply(newTabView, arguments);
    684   }
    685 
    686   function setAppToBeHighlighted(appId) {
    687     newTabView.highlightAppId = appId;
    688   }
    689 
    690   // Return an object with all the exports
    691   return {
    692     appAdded: appAdded,
    693     appMoved: appMoved,
    694     appRemoved: appRemoved,
    695     appsPrefChangeCallback: appsPrefChangeCallback,
    696     appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
    697     enterRearrangeMode: enterRearrangeMode,
    698     getAppsCallback: getAppsCallback,
    699     getAppsPageIndex: getAppsPageIndex,
    700     getCardSlider: getCardSlider,
    701     onLoad: onLoad,
    702     leaveRearrangeMode: leaveRearrangeMode,
    703     logTimeToClick: logTimeToClick,
    704     NtpFollowAction: NtpFollowAction,
    705     saveAppPageName: saveAppPageName,
    706     setAppToBeHighlighted: setAppToBeHighlighted,
    707     setBookmarkBarAttached: setBookmarkBarAttached,
    708     setForeignSessions: setForeignSessions,
    709     setMostVisitedPages: setMostVisitedPages,
    710     setSuggestionsPages: setSuggestionsPages,
    711     setRecentlyClosedTabs: setRecentlyClosedTabs,
    712     setFaviconDominantColor: setFaviconDominantColor,
    713     showNotification: showNotification,
    714     themeChanged: themeChanged,
    715     updateLogin: updateLogin
    716   };
    717 });
    718 
    719 document.addEventListener('DOMContentLoaded', ntp.onLoad);
    720 
    721 var toCssPx = cr.ui.toCssPx;
    722