Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / new_tab.js
bloba626afc6141dc123898e8befb07ae3ccd767b5df
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.
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  */
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';
16   /**
17    * NewTabView instance.
18    * @type {!Object|undefined}
19    */
20   var newTabView;
22   /**
23    * The 'notification-container' element.
24    * @type {!Element|undefined}
25    */
26   var notificationContainer;
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;
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;
43   /**
44    * true if |loginBubble| should be shown.
45    * @type {boolean}
46    */
47   var shouldShowLoginBubble = false;
49   /**
50    * The 'other-sessions-menu-button' element.
51    * @type {!Element|undefined}
52    */
53   var otherSessionsButton;
55   /**
56    * The time when all sections are ready.
57    * @type {number|undefined}
58    * @private
59    */
60   var startTime;
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;
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   };
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   }
101   NewTabView.prototype = {
102     __proto__: ntp.PageListView.prototype,
104     /** @override */
105     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
106       ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
108       if (promoBubble)
109         window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
110     }
111   };
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();
133     // Load the current theme colors.
134     themeChanged();
136     newTabView = new NewTabView();
138     notificationContainer = getRequiredElement('notification-container');
139     notificationContainer.addEventListener(
140         'webkitTransitionEnd', onNotificationTransitionEnd);
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     }
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     }
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     }
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     }
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     }
197     // We need to wait for all the footer menu setup to be completed before
198     // we can compute its layout.
199     layoutFooter();
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;
210       $('login-status-advanced').onclick = function() {
211         chrome.send('showAdvancedLoginUI');
212       };
213       $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
215       var bubbleContent = $('login-status-bubble-contents');
216       loginBubble.content = bubbleContent;
218       // The anchor node won't be updated until updateLogin is called so don't
219       // show the bubble yet.
220       shouldShowLoginBubble = true;
221     }
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']);
232       var bubbleLink = promoBubble.querySelector('a');
233       if (bubbleLink) {
234         bubbleLink.addEventListener('click', function(e) {
235           chrome.send('bubblePromoLinkClicked');
236         });
237       }
239       promoBubble.handleCloseEvent = function() {
240         promoBubble.hide();
241         chrome.send('bubblePromoClosed');
242       };
243       promoBubble.show();
244       chrome.send('bubblePromoViewed');
245     }
247     var loginContainer = getRequiredElement('login-container');
248     loginContainer.addEventListener('click', showSyncLoginUI);
249     if (loadTimeData.getBoolean('shouldShowSyncLogin'))
250       chrome.send('initializeSyncLogin');
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');
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         };
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         }
277         showNotification(promo, [], function() {
278           chrome.send('notificationPromoClosed');
279         }, 60000);
280         chrome.send('notificationPromoViewed');
281       }
283       cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
284       document.documentElement.classList.remove('starting-up');
286       startTime = Date.now();
287     });
289     preventDefaultOnPoundLinkClicks();  // From webui/js/util.js.
290     cr.ui.FocusManager.disableMouseFocusOnButtons();
291   }
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   }
304   /*
305    * The number of sections to wait on.
306    * @type {number}
307    */
308   var sectionsToWaitFor = -1;
310   /**
311    * Queued callbacks which lie in wait for all sections to be ready.
312    * @type {array}
313    */
314   var readyCallbacks = [];
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   });
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   }
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   }
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);
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   }
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     }
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   }
391   function themeChanged(opt_hasAttribution) {
392     $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
394     if (typeof opt_hasAttribution != 'undefined') {
395       document.documentElement.setAttribute('hasattribution',
396                                             opt_hasAttribution);
397     }
399     updateAttribution();
400   }
402   function setBookmarkBarAttached(attached) {
403     document.documentElement.setAttribute('bookmarkbarattached', attached);
404   }
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   }
418   /**
419    * Timeout ID.
420    * @type {number}
421    */
422   var notificationTimeout = 0;
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);
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     }
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     }
462     function closeFunc(e) {
463       if (opt_closeHandler)
464         opt_closeHandler();
465       hideNotification();
466     }
468     document.querySelector('#notification button').onclick = closeFunc;
469     document.addEventListener('dragstart', closeFunc);
471     notificationContainer.hidden = false;
472     showNotificationOnCurrentPage();
474     newTabView.cardSlider.frame.addEventListener(
475         'cardSlider:card_change_ended', onCardChangeEnded);
477     var timeout = opt_timeout || 10000;
478     notificationTimeout = window.setTimeout(hideNotification, timeout);
479   }
481   /**
482    * Hide the notification bubble.
483    */
484   function hideNotification() {
485     notificationContainer.classList.add('inactive');
487     newTabView.cardSlider.frame.removeEventListener(
488         'cardSlider:card_change_ended', onCardChangeEnded);
489   }
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;
500     // Hide the notification the old page.
501     notificationContainer.classList.add('card-changed');
503     showNotificationOnCurrentPage();
504   }
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;
515       // NOTE: This moves the notification to inside of the current page.
516       page.notification = notificationContainer;
518       // Reveal the notification and instruct it to hide itself if ignored.
519       notificationContainer.classList.remove('inactive');
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   }
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   }
539   function setRecentlyClosedTabs(dataItems) {
540     $('recently-closed-menu-button').dataItems = dataItems;
541     layoutFooter();
542   }
544   function setMostVisitedPages(data, hasBlacklistedUrls) {
545     newTabView.mostVisitedPage.data = data;
546     cr.dispatchSimpleEvent(document, 'sectionready', true, true);
547   }
549   function setSuggestionsPages(data, hasBlacklistedUrls) {
550     newTabView.suggestionsPage.data = data;
551   }
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   }
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');
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   }
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   }
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   }
627   /**
628    * Wrappers to forward the callback to corresponding PageListView member.
629    */
630   function appAdded() {
631     return newTabView.appAdded.apply(newTabView, arguments);
632   }
634   function appMoved() {
635     return newTabView.appMoved.apply(newTabView, arguments);
636   }
638   function appRemoved() {
639     return newTabView.appRemoved.apply(newTabView, arguments);
640   }
642   function appsPrefChangeCallback() {
643     return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
644   }
646   function appLauncherPromoPrefChangeCallback() {
647     return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView,
648                                                                arguments);
649   }
651   function appsReordered() {
652     return newTabView.appsReordered.apply(newTabView, arguments);
653   }
655   function enterRearrangeMode() {
656     return newTabView.enterRearrangeMode.apply(newTabView, arguments);
657   }
659   function setForeignSessions(sessionList, isTabSyncEnabled) {
660     if (otherSessionsButton) {
661       otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
662       layoutFooter();
663     }
664   }
666   function getAppsCallback() {
667     return newTabView.getAppsCallback.apply(newTabView, arguments);
668   }
670   function getAppsPageIndex() {
671     return newTabView.getAppsPageIndex.apply(newTabView, arguments);
672   }
674   function getCardSlider() {
675     return newTabView.cardSlider;
676   }
678   function leaveRearrangeMode() {
679     return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
680   }
682   function saveAppPageName() {
683     return newTabView.saveAppPageName.apply(newTabView, arguments);
684   }
686   function setAppToBeHighlighted(appId) {
687     newTabView.highlightAppId = appId;
688   }
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   };
719 document.addEventListener('DOMContentLoaded', ntp.onLoad);
721 var toCssPx = cr.ui.toCssPx;