• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
13cr.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   * Fills in an invisible div with the 'Most Visited' string so that
345   * its length may be measured and the nav dots sized accordingly.
346   */
347  function measureNavDots() {
348    var measuringDiv = $('fontMeasuringDiv');
349    if (loadTimeData.getBoolean('showMostvisited'))
350      measuringDiv.textContent = loadTimeData.getString('mostvisited');
351
352    // The 4 is for border and padding.
353    var pxWidth = Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
354
355    var styleElement = document.createElement('style');
356    styleElement.type = 'text/css';
357    // max-width is used because if we run out of space, the nav dots will be
358    // shrunk.
359    styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
360    document.querySelector('head').appendChild(styleElement);
361  }
362
363  /**
364   * Layout the footer so that the nav dots stay centered.
365   */
366  function layoutFooter() {
367    var menu = $('footer-menu-container');
368    var logo = $('logo-img');
369    if (menu.clientWidth > logo.clientWidth)
370      logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px';
371    else
372      menu.style.WebkitFlex = '0 1 ' + logo.clientWidth + 'px';
373  }
374
375  function themeChanged(opt_hasAttribution) {
376    $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
377
378    if (typeof opt_hasAttribution != 'undefined') {
379      document.documentElement.setAttribute('hasattribution',
380                                            opt_hasAttribution);
381    }
382
383    updateAttribution();
384  }
385
386  function setBookmarkBarAttached(attached) {
387    document.documentElement.setAttribute('bookmarkbarattached', attached);
388  }
389
390  /**
391   * Attributes the attribution image at the bottom left.
392   */
393  function updateAttribution() {
394    var attribution = $('attribution');
395    if (document.documentElement.getAttribute('hasattribution') == 'true') {
396      attribution.hidden = false;
397    } else {
398      attribution.hidden = true;
399    }
400  }
401
402  /**
403   * Timeout ID.
404   * @type {number}
405   */
406  var notificationTimeout = 0;
407
408  /**
409   * Shows the notification bubble.
410   * @param {string|Node} message The notification message or node to use as
411   *     message.
412   * @param {Array.<{text: string, action: function()}>} links An array of
413   *     records describing the links in the notification. Each record should
414   *     have a 'text' attribute (the display string) and an 'action' attribute
415   *     (a function to run when the link is activated).
416   * @param {Function} opt_closeHandler The callback invoked if the user
417   *     manually dismisses the notification.
418   */
419  function showNotification(message, links, opt_closeHandler, opt_timeout) {
420    window.clearTimeout(notificationTimeout);
421
422    var span = document.querySelector('#notification > span');
423    if (typeof message == 'string') {
424      span.textContent = message;
425    } else {
426      span.textContent = '';  // Remove all children.
427      span.appendChild(message);
428    }
429
430    var linksBin = $('notificationLinks');
431    linksBin.textContent = '';
432    for (var i = 0; i < links.length; i++) {
433      var link = linksBin.ownerDocument.createElement('div');
434      link.textContent = links[i].text;
435      link.action = links[i].action;
436      link.onclick = function() {
437        this.action();
438        hideNotification();
439      };
440      link.setAttribute('role', 'button');
441      link.setAttribute('tabindex', 0);
442      link.className = 'link-button';
443      linksBin.appendChild(link);
444    }
445
446    function closeFunc(e) {
447      if (opt_closeHandler)
448        opt_closeHandler();
449      hideNotification();
450    }
451
452    document.querySelector('#notification button').onclick = closeFunc;
453    document.addEventListener('dragstart', closeFunc);
454
455    notificationContainer.hidden = false;
456    showNotificationOnCurrentPage();
457
458    newTabView.cardSlider.frame.addEventListener(
459        'cardSlider:card_change_ended', onCardChangeEnded);
460
461    var timeout = opt_timeout || 10000;
462    notificationTimeout = window.setTimeout(hideNotification, timeout);
463  }
464
465  /**
466   * Hide the notification bubble.
467   */
468  function hideNotification() {
469    notificationContainer.classList.add('inactive');
470
471    newTabView.cardSlider.frame.removeEventListener(
472        'cardSlider:card_change_ended', onCardChangeEnded);
473  }
474
475  /**
476   * Happens when 1 or more consecutive card changes end.
477   * @param {Event} e The cardSlider:card_change_ended event.
478   */
479  function onCardChangeEnded(e) {
480    // If we ended on the same page as we started, ignore.
481    if (newTabView.cardSlider.currentCardValue.notification)
482      return;
483
484    // Hide the notification the old page.
485    notificationContainer.classList.add('card-changed');
486
487    showNotificationOnCurrentPage();
488  }
489
490  /**
491   * Move and show the notification on the current page.
492   */
493  function showNotificationOnCurrentPage() {
494    var page = newTabView.cardSlider.currentCardValue;
495    doWhenAllSectionsReady(function() {
496      if (page != newTabView.cardSlider.currentCardValue)
497        return;
498
499      // NOTE: This moves the notification to inside of the current page.
500      page.notification = notificationContainer;
501
502      // Reveal the notification and instruct it to hide itself if ignored.
503      notificationContainer.classList.remove('inactive');
504
505      // Gives the browser time to apply this rule before we remove it (causing
506      // a transition).
507      window.setTimeout(function() {
508        notificationContainer.classList.remove('card-changed');
509      }, 0);
510    });
511  }
512
513  /**
514   * When done fading out, set hidden to true so the notification can't be
515   * tabbed to or clicked.
516   * @param {Event} e The webkitTransitionEnd event.
517   */
518  function onNotificationTransitionEnd(e) {
519    if (notificationContainer.classList.contains('inactive'))
520      notificationContainer.hidden = true;
521  }
522
523  function setRecentlyClosedTabs(dataItems) {
524    $('recently-closed-menu-button').dataItems = dataItems;
525    layoutFooter();
526  }
527
528  function setMostVisitedPages(data, hasBlacklistedUrls) {
529    newTabView.mostVisitedPage.data = data;
530    cr.dispatchSimpleEvent(document, 'sectionready', true, true);
531  }
532
533  function setSuggestionsPages(data, hasBlacklistedUrls) {
534    newTabView.suggestionsPage.data = data;
535  }
536
537  /**
538   * Set the dominant color for a node. This will be called in response to
539   * getFaviconDominantColor. The node represented by |id| better have a setter
540   * for stripeColor.
541   * @param {string} id The ID of a node.
542   * @param {string} color The color represented as a CSS string.
543   */
544  function setFaviconDominantColor(id, color) {
545    var node = $(id);
546    if (node)
547      node.stripeColor = color;
548  }
549
550  /**
551   * Updates the text displayed in the login container. If there is no text then
552   * the login container is hidden.
553   * @param {string} loginHeader The first line of text.
554   * @param {string} loginSubHeader The second line of text.
555   * @param {string} iconURL The url for the login status icon. If this is null
556        then the login status icon is hidden.
557   * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
558   */
559  function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) {
560    if (loginHeader || loginSubHeader) {
561      $('login-container').hidden = false;
562      $('login-status-header').innerHTML = loginHeader;
563      $('login-status-sub-header').innerHTML = loginSubHeader;
564      $('card-slider-frame').classList.add('showing-login-area');
565
566      if (iconURL) {
567        $('login-status-header-container').style.backgroundImage = url(iconURL);
568        $('login-status-header-container').classList.add('login-status-icon');
569      } else {
570        $('login-status-header-container').style.backgroundImage = 'none';
571        $('login-status-header-container').classList.remove(
572            'login-status-icon');
573      }
574    } else {
575      $('login-container').hidden = true;
576      $('card-slider-frame').classList.remove('showing-login-area');
577    }
578    if (shouldShowLoginBubble) {
579      window.setTimeout(loginBubble.show.bind(loginBubble), 0);
580      chrome.send('loginMessageSeen');
581      shouldShowLoginBubble = false;
582    } else if (loginBubble) {
583      loginBubble.reposition();
584    }
585    if (otherSessionsButton) {
586      otherSessionsButton.updateSignInState(isUserSignedIn);
587      layoutFooter();
588    }
589  }
590
591  /**
592   * Show the sync login UI.
593   * @param {Event} e The click event.
594   */
595  function showSyncLoginUI(e) {
596    var rect = e.currentTarget.getBoundingClientRect();
597    chrome.send('showSyncLoginUI',
598                [rect.left, rect.top, rect.width, rect.height]);
599  }
600
601  /**
602   * Logs the time to click for the specified item.
603   * @param {string} item The item to log the time-to-click.
604   */
605  function logTimeToClick(item) {
606    var timeToClick = Date.now() - startTime;
607    chrome.send('logTimeToClick',
608        ['NewTabPage.TimeToClick' + item, timeToClick]);
609  }
610
611  /**
612   * Wrappers to forward the callback to corresponding PageListView member.
613   */
614  function appAdded() {
615    return newTabView.appAdded.apply(newTabView, arguments);
616  }
617
618  function appMoved() {
619    return newTabView.appMoved.apply(newTabView, arguments);
620  }
621
622  function appRemoved() {
623    return newTabView.appRemoved.apply(newTabView, arguments);
624  }
625
626  function appsPrefChangeCallback() {
627    return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
628  }
629
630  function appLauncherPromoPrefChangeCallback() {
631    return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView,
632                                                               arguments);
633  }
634
635  function appsReordered() {
636    return newTabView.appsReordered.apply(newTabView, arguments);
637  }
638
639  function enterRearrangeMode() {
640    return newTabView.enterRearrangeMode.apply(newTabView, arguments);
641  }
642
643  function setForeignSessions(sessionList, isTabSyncEnabled) {
644    if (otherSessionsButton) {
645      otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
646      layoutFooter();
647    }
648  }
649
650  function getAppsCallback() {
651    return newTabView.getAppsCallback.apply(newTabView, arguments);
652  }
653
654  function getAppsPageIndex() {
655    return newTabView.getAppsPageIndex.apply(newTabView, arguments);
656  }
657
658  function getCardSlider() {
659    return newTabView.cardSlider;
660  }
661
662  function leaveRearrangeMode() {
663    return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
664  }
665
666  function saveAppPageName() {
667    return newTabView.saveAppPageName.apply(newTabView, arguments);
668  }
669
670  function setAppToBeHighlighted(appId) {
671    newTabView.highlightAppId = appId;
672  }
673
674  // Return an object with all the exports
675  return {
676    appAdded: appAdded,
677    appMoved: appMoved,
678    appRemoved: appRemoved,
679    appsPrefChangeCallback: appsPrefChangeCallback,
680    appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
681    enterRearrangeMode: enterRearrangeMode,
682    getAppsCallback: getAppsCallback,
683    getAppsPageIndex: getAppsPageIndex,
684    getCardSlider: getCardSlider,
685    onLoad: onLoad,
686    leaveRearrangeMode: leaveRearrangeMode,
687    logTimeToClick: logTimeToClick,
688    NtpFollowAction: NtpFollowAction,
689    saveAppPageName: saveAppPageName,
690    setAppToBeHighlighted: setAppToBeHighlighted,
691    setBookmarkBarAttached: setBookmarkBarAttached,
692    setForeignSessions: setForeignSessions,
693    setMostVisitedPages: setMostVisitedPages,
694    setSuggestionsPages: setSuggestionsPages,
695    setRecentlyClosedTabs: setRecentlyClosedTabs,
696    setFaviconDominantColor: setFaviconDominantColor,
697    showNotification: showNotification,
698    themeChanged: themeChanged,
699    updateLogin: updateLogin
700  };
701});
702
703document.addEventListener('DOMContentLoaded', ntp.onLoad);
704
705var toCssPx = cr.ui.toCssPx;
706