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.
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.
12 * @typedef {{direction: string,
13 * filler: (boolean|undefined),
16 * @see chrome/browser/ui/webui/ntp/most_visited_handler.cc
20 // Use an anonymous function to enable strict mode just for this file (which
21 // will be concatenated with other files when embedded in Chrome
22 cr.define('ntp', function() {
26 * NewTabView instance.
27 * @type {!Object|undefined}
32 * The 'notification-container' element.
33 * @type {!Element|undefined}
35 var notificationContainer;
38 * If non-null, an info bubble for showing messages to the user. It points at
39 * the Most Visited label, and is used to draw more attention to the
41 * @type {!cr.ui.Bubble|undefined}
46 * If non-null, an bubble confirming that the user has signed into sync. It
47 * points at the login status at the top of the page.
48 * @type {!cr.ui.Bubble|undefined}
53 * true if |loginBubble| should be shown.
56 var shouldShowLoginBubble = false;
59 * The 'other-sessions-menu-button' element.
60 * @type {!ntp.OtherSessionsMenuButton|undefined}
62 var otherSessionsButton;
65 * The time when all sections are ready.
66 * @type {number|undefined}
72 * The time in milliseconds for most transitions. This should match what's
73 * in new_tab.css. Unfortunately there's no better way to try to time
74 * something to occur until after a transition has completed.
78 var DEFAULT_TRANSITION_TIME = 500;
81 * See description for these values in ntp_stats.h.
84 var NtpFollowAction = {
86 CLICKED_OTHER_NTP_PANE: 12,
91 * Creates a NewTabView object. NewTabView extends PageListView with
92 * new tab UI specific logics.
94 * @extends {ntp.PageListView}
96 function NewTabView() {
97 var pageSwitcherStart;
99 if (loadTimeData.getValue('showApps')) {
100 pageSwitcherStart = /** @type {!ntp.PageSwitcher} */(
101 getRequiredElement('page-switcher-start'));
102 pageSwitcherEnd = /** @type {!ntp.PageSwitcher} */(
103 getRequiredElement('page-switcher-end'));
105 this.initialize(getRequiredElement('page-list'),
106 getRequiredElement('dot-list'),
107 getRequiredElement('card-slider-frame'),
108 getRequiredElement('trash'),
109 pageSwitcherStart, pageSwitcherEnd);
112 NewTabView.prototype = {
113 __proto__: ntp.PageListView.prototype,
116 appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
117 ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
120 window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
125 * Invoked at startup once the DOM is available to initialize the app.
128 sectionsToWaitFor = 0;
129 if (loadTimeData.getBoolean('showMostvisited'))
131 if (loadTimeData.getBoolean('showApps')) {
133 if (loadTimeData.getBoolean('showAppLauncherPromo')) {
134 $('app-launcher-promo-close-button').addEventListener('click',
135 function() { chrome.send('stopShowingAppLauncherPromo'); });
136 $('apps-promo-learn-more').addEventListener('click',
137 function() { chrome.send('onLearnMore'); });
140 if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled'))
144 // Load the current theme colors.
147 newTabView = new NewTabView();
149 notificationContainer = getRequiredElement('notification-container');
150 notificationContainer.addEventListener(
151 'webkitTransitionEnd', onNotificationTransitionEnd);
153 if (loadTimeData.getBoolean('showRecentlyClosed')) {
154 cr.ui.decorate(getRequiredElement('recently-closed-menu-button'),
155 ntp.RecentMenuButton);
156 chrome.send('getRecentlyClosedTabs');
158 $('recently-closed-menu-button').hidden = true;
161 if (loadTimeData.getBoolean('showOtherSessionsMenu')) {
162 otherSessionsButton = /** @type {!ntp.OtherSessionsMenuButton} */(
163 getRequiredElement('other-sessions-menu-button'));
164 cr.ui.decorate(otherSessionsButton, ntp.OtherSessionsMenuButton);
165 otherSessionsButton.initialize(loadTimeData.getBoolean('isUserSignedIn'));
167 getRequiredElement('other-sessions-menu-button').hidden = true;
170 if (loadTimeData.getBoolean('showMostvisited')) {
171 var mostVisited = new ntp.MostVisitedPage();
172 // Move the footer into the most visited page if we are in "bare minimum"
174 if (document.body.classList.contains('bare-minimum'))
175 mostVisited.appendFooter(getRequiredElement('footer'));
176 newTabView.appendTilePage(mostVisited,
177 loadTimeData.getString('mostvisited'),
179 chrome.send('getMostVisited');
182 if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled')) {
183 var suggestionsScript = document.createElement('script');
184 suggestionsScript.src = 'suggestions_page.js';
185 suggestionsScript.onload = function() {
186 newTabView.appendTilePage(new ntp.SuggestionsPage(),
187 loadTimeData.getString('suggestions'),
189 (newTabView.appsPages.length > 0) ?
190 newTabView.appsPages[0] : null);
191 chrome.send('getSuggestions');
192 cr.dispatchSimpleEvent(document, 'sectionready', true, true);
194 document.querySelector('head').appendChild(suggestionsScript);
197 if (!loadTimeData.getBoolean('showWebStoreIcon')) {
198 var webStoreIcon = $('chrome-web-store-link');
199 // Not all versions of the NTP have a footer, so this may not exist.
201 webStoreIcon.hidden = true;
203 var webStoreLink = loadTimeData.getString('webStoreLink');
204 var url = appendParam(webStoreLink, 'utm_source', 'chrome-ntp-launcher');
205 $('chrome-web-store-link').href = url;
206 $('chrome-web-store-link').addEventListener('click',
207 onChromeWebStoreButtonClick);
210 // We need to wait for all the footer menu setup to be completed before
211 // we can compute its layout.
214 if (loadTimeData.getString('login_status_message')) {
215 loginBubble = new cr.ui.Bubble;
216 loginBubble.anchorNode = $('login-container');
217 loginBubble.arrowLocation = cr.ui.ArrowLocation.TOP_END;
218 loginBubble.bubbleAlignment =
219 cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE;
220 loginBubble.deactivateToDismissDelay = 2000;
221 loginBubble.closeButtonVisible = false;
223 $('login-status-advanced').onclick = function() {
224 chrome.send('showAdvancedLoginUI');
226 $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
228 var bubbleContent = $('login-status-bubble-contents');
229 loginBubble.content = bubbleContent;
231 // The anchor node won't be updated until updateLogin is called so don't
232 // show the bubble yet.
233 shouldShowLoginBubble = true;
236 if (loadTimeData.valueExists('bubblePromoText')) {
237 promoBubble = new cr.ui.Bubble;
238 promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor');
239 promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START;
240 promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE;
241 promoBubble.deactivateToDismissDelay = 2000;
242 promoBubble.content = parseHtmlSubset(
243 loadTimeData.getString('bubblePromoText'), ['BR']);
245 var bubbleLink = promoBubble.querySelector('a');
247 bubbleLink.addEventListener('click', function(e) {
248 chrome.send('bubblePromoLinkClicked');
252 promoBubble.handleCloseEvent = function() {
254 chrome.send('bubblePromoClosed');
257 chrome.send('bubblePromoViewed');
260 $('login-container').addEventListener('click', showSyncLoginUI);
261 if (loadTimeData.getBoolean('shouldShowSyncLogin'))
262 chrome.send('initializeSyncLogin');
264 doWhenAllSectionsReady(function() {
265 // Tell the slider about the pages.
266 newTabView.updateSliderCards();
267 // Mark the current page.
268 newTabView.cardSlider.currentCardValue.navigationDot.classList.add(
271 if (loadTimeData.valueExists('notificationPromoText')) {
272 var promoText = loadTimeData.getString('notificationPromoText');
275 src: function(node, value) {
276 return node.tagName == 'IMG' &&
277 /^data\:image\/(?:png|gif|jpe?g)/.test(value);
281 var promo = parseHtmlSubset(promoText, tags, attrs);
282 var promoLink = promo.querySelector('a');
284 promoLink.addEventListener('click', function(e) {
285 chrome.send('notificationPromoLinkClicked');
289 showNotification(promo, [], function() {
290 chrome.send('notificationPromoClosed');
292 chrome.send('notificationPromoViewed');
295 cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
296 document.documentElement.classList.remove('starting-up');
298 startTime = Date.now();
303 * Launches the chrome web store app with the chrome-ntp-launcher
305 * @param {Event} e The click event.
307 function onChromeWebStoreButtonClick(e) {
308 chrome.send('recordAppLaunchByURL',
309 [encodeURIComponent(this.href),
310 ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
314 * The number of sections to wait on.
317 var sectionsToWaitFor = -1;
320 * Queued callbacks which lie in wait for all sections to be ready.
323 var readyCallbacks = [];
326 * Fired as each section of pages becomes ready.
327 * @param {Event} e Each page's synthetic DOM event.
329 document.addEventListener('sectionready', function(e) {
330 if (--sectionsToWaitFor <= 0) {
331 while (readyCallbacks.length) {
332 readyCallbacks.shift()();
338 * This is used to simulate a fire-once event (i.e. $(document).ready() in
339 * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
340 * is fired right away. If all pages are not ready yet, the function is queued
341 * for later execution.
342 * @param {Function} callback The work to be done when ready.
344 function doWhenAllSectionsReady(callback) {
345 assert(typeof callback == 'function');
346 if (sectionsToWaitFor > 0)
347 readyCallbacks.push(callback);
349 window.setTimeout(callback, 0); // Do soon after, but asynchronously.
353 * Measure the width of a nav dot with a given title.
354 * @param {string} id The loadTimeData ID of the desired title.
355 * @return {number} The width of the nav dot.
357 function measureNavDot(id) {
358 var measuringDiv = $('fontMeasuringDiv');
359 measuringDiv.textContent = loadTimeData.getString(id);
360 // The 4 is for border and padding.
361 return Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
365 * Fills in an invisible div with the longest dot title string so that
366 * its length may be measured and the nav dots sized accordingly.
368 function measureNavDots() {
369 var pxWidth = measureNavDot('appDefaultPageName');
370 if (loadTimeData.getBoolean('showMostvisited'))
371 pxWidth = Math.max(measureNavDot('mostvisited'), pxWidth);
373 var styleElement = document.createElement('style');
374 styleElement.type = 'text/css';
375 // max-width is used because if we run out of space, the nav dots will be
377 styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
378 document.querySelector('head').appendChild(styleElement);
382 * Layout the footer so that the nav dots stay centered.
384 function layoutFooter() {
385 // We need the image to be loaded.
386 var logo = $('logo-img');
387 var logoImg = logo.querySelector('img');
388 if (!logoImg.complete) {
389 logoImg.onload = layoutFooter;
393 var menu = $('footer-menu-container');
394 if (menu.clientWidth > logoImg.width)
395 logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px';
397 menu.style.WebkitFlex = '0 1 ' + logoImg.width + 'px';
401 * @param {boolean=} opt_hasAttribution
403 function themeChanged(opt_hasAttribution) {
404 $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
406 if (typeof opt_hasAttribution != 'undefined') {
407 document.documentElement.setAttribute('hasattribution',
414 function setBookmarkBarAttached(attached) {
415 document.documentElement.setAttribute('bookmarkbarattached', attached);
419 * Attributes the attribution image at the bottom left.
421 function updateAttribution() {
422 var attribution = $('attribution');
423 if (document.documentElement.getAttribute('hasattribution') == 'true') {
424 attribution.hidden = false;
426 attribution.hidden = true;
434 var notificationTimeout = 0;
437 * Shows the notification bubble.
438 * @param {string|Node} message The notification message or node to use as
440 * @param {Array<{text: string, action: function()}>} links An array of
441 * records describing the links in the notification. Each record should
442 * have a 'text' attribute (the display string) and an 'action' attribute
443 * (a function to run when the link is activated).
444 * @param {Function=} opt_closeHandler The callback invoked if the user
445 * manually dismisses the notification.
446 * @param {number=} opt_timeout
448 function showNotification(message, links, opt_closeHandler, opt_timeout) {
449 window.clearTimeout(notificationTimeout);
451 var span = document.querySelector('#notification > span');
452 if (typeof message == 'string') {
453 span.textContent = message;
455 span.textContent = ''; // Remove all children.
456 span.appendChild(message);
459 var linksBin = $('notificationLinks');
460 linksBin.textContent = '';
461 for (var i = 0; i < links.length; i++) {
462 var link = new ActionLink;
463 link.textContent = links[i].text;
464 link.action = links[i].action;
465 link.onclick = function() {
469 linksBin.appendChild(link);
472 function closeFunc(e) {
473 if (opt_closeHandler)
478 document.querySelector('#notification button').onclick = closeFunc;
479 document.addEventListener('dragstart', closeFunc);
481 notificationContainer.hidden = false;
482 showNotificationOnCurrentPage();
484 newTabView.cardSlider.frame.addEventListener(
485 'cardSlider:card_change_ended', onCardChangeEnded);
487 var timeout = opt_timeout || 10000;
488 notificationTimeout = window.setTimeout(hideNotification, timeout);
492 * Hide the notification bubble.
494 function hideNotification() {
495 notificationContainer.classList.add('inactive');
497 newTabView.cardSlider.frame.removeEventListener(
498 'cardSlider:card_change_ended', onCardChangeEnded);
502 * Happens when 1 or more consecutive card changes end.
503 * @param {Event} e The cardSlider:card_change_ended event.
505 function onCardChangeEnded(e) {
506 // If we ended on the same page as we started, ignore.
507 if (newTabView.cardSlider.currentCardValue.notification)
510 // Hide the notification the old page.
511 notificationContainer.classList.add('card-changed');
513 showNotificationOnCurrentPage();
517 * Move and show the notification on the current page.
519 function showNotificationOnCurrentPage() {
520 var page = newTabView.cardSlider.currentCardValue;
521 doWhenAllSectionsReady(function() {
522 if (page != newTabView.cardSlider.currentCardValue)
525 // NOTE: This moves the notification to inside of the current page.
526 page.notification = notificationContainer;
528 // Reveal the notification and instruct it to hide itself if ignored.
529 notificationContainer.classList.remove('inactive');
531 // Gives the browser time to apply this rule before we remove it (causing
533 window.setTimeout(function() {
534 notificationContainer.classList.remove('card-changed');
540 * When done fading out, set hidden to true so the notification can't be
541 * tabbed to or clicked.
542 * @param {Event} e The webkitTransitionEnd event.
544 function onNotificationTransitionEnd(e) {
545 if (notificationContainer.classList.contains('inactive'))
546 notificationContainer.hidden = true;
549 function setRecentlyClosedTabs(dataItems) {
550 $('recently-closed-menu-button').dataItems = dataItems;
555 * @param {Array<PageData>} data
556 * @param {boolean} hasBlacklistedUrls
558 function setMostVisitedPages(data, hasBlacklistedUrls) {
559 newTabView.mostVisitedPage.data = data;
560 cr.dispatchSimpleEvent(document, 'sectionready', true, true);
563 function setSuggestionsPages(data, hasBlacklistedUrls) {
564 newTabView.suggestionsPage.data = data;
568 * Set the dominant color for a node. This will be called in response to
569 * getFaviconDominantColor. The node represented by |id| better have a setter
571 * @param {string} id The ID of a node.
572 * @param {string} color The color represented as a CSS string.
574 function setFaviconDominantColor(id, color) {
577 node.stripeColor = color;
581 * Updates the text displayed in the login container. If there is no text then
582 * the login container is hidden.
583 * @param {string} loginHeader The first line of text.
584 * @param {string} loginSubHeader The second line of text.
585 * @param {string} iconURL The url for the login status icon. If this is null
586 then the login status icon is hidden.
587 * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
589 function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) {
590 /** @const */ var showLogin = loginHeader || loginSubHeader;
592 $('login-container').hidden = !showLogin;
593 $('card-slider-frame').classList.toggle('showing-login-area', !!showLogin);
596 // TODO(dbeam): we should use .textContent instead to mitigate XSS.
597 $('login-status-header').innerHTML = loginHeader;
598 $('login-status-sub-header').innerHTML = loginSubHeader;
600 var headerContainer = $('login-status-header-container');
601 headerContainer.classList.toggle('login-status-icon', !!iconURL);
602 headerContainer.style.backgroundImage = iconURL ? url(iconURL) : 'none';
605 if (shouldShowLoginBubble) {
606 window.setTimeout(loginBubble.show.bind(loginBubble), 0);
607 chrome.send('loginMessageSeen');
608 shouldShowLoginBubble = false;
609 } else if (loginBubble) {
610 loginBubble.reposition();
612 if (otherSessionsButton) {
613 otherSessionsButton.updateSignInState(isUserSignedIn);
619 * Show the sync login UI.
620 * @param {Event} e The click event.
622 function showSyncLoginUI(e) {
623 var rect = e.currentTarget.getBoundingClientRect();
624 chrome.send('showSyncLoginUI',
625 [rect.left, rect.top, rect.width, rect.height]);
629 * Logs the time to click for the specified item.
630 * @param {string} item The item to log the time-to-click.
632 function logTimeToClick(item) {
633 var timeToClick = Date.now() - startTime;
634 chrome.send('logTimeToClick',
635 ['NewTabPage.TimeToClick' + item, timeToClick]);
639 * Wrappers to forward the callback to corresponding PageListView member.
643 * Called by chrome when a new app has been added to chrome or has been
644 * enabled if previously disabled.
645 * @param {Object} appData A data structure full of relevant information for
647 * @param {boolean=} opt_highlight Whether the app about to be added should
650 function appAdded(appData, opt_highlight) {
651 newTabView.appAdded(appData, opt_highlight);
655 * Called by chrome when an app has changed positions.
656 * @param {Object} appData The data for the app. This contains page and
659 function appMoved(appData) {
660 newTabView.appMoved(appData);
664 * Called by chrome when an existing app has been disabled or
665 * removed/uninstalled from chrome.
666 * @param {Object} appData A data structure full of relevant information for
668 * @param {boolean} isUninstall True if the app is being uninstalled;
669 * false if the app is being disabled.
670 * @param {boolean} fromPage True if the removal was from the current page.
672 function appRemoved(appData, isUninstall, fromPage) {
673 newTabView.appRemoved(appData, isUninstall, fromPage);
677 * Callback invoked by chrome whenever an app preference changes.
678 * @param {Object} data An object with all the data on available
681 function appsPrefChangeCallback(data) {
682 newTabView.appsPrefChangedCallback(data);
686 * Callback invoked by chrome whenever the app launcher promo pref changes.
687 * @param {boolean} show Identifies if we should show or hide the promo.
689 function appLauncherPromoPrefChangeCallback(show) {
690 newTabView.appLauncherPromoPrefChangeCallback(show);
694 * Called whenever tiles should be re-arranging themselves out of the way
695 * of a moving or insert tile.
697 function enterRearrangeMode() {
698 newTabView.enterRearrangeMode();
701 function setForeignSessions(sessionList, isTabSyncEnabled) {
702 if (otherSessionsButton) {
703 otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
709 * Callback invoked by chrome with the apps available.
711 * Note that calls to this function can occur at any time, not just in
712 * response to a getApps request. For example, when a user
713 * installs/uninstalls an app on another synchronized devices.
714 * @param {Object} data An object with all the data on available
717 function getAppsCallback(data) {
718 newTabView.getAppsCallback(data);
722 * Return the index of the given apps page.
723 * @param {ntp.AppsPage} page The AppsPage we wish to find.
724 * @return {number} The index of |page| or -1 if it is not in the collection.
726 function getAppsPageIndex(page) {
727 return newTabView.getAppsPageIndex(page);
730 function getCardSlider() {
731 return newTabView.cardSlider;
735 * Invoked whenever some app is released
737 function leaveRearrangeMode() {
738 newTabView.leaveRearrangeMode();
742 * Save the name of an apps page.
743 * Store the apps page name into the preferences store.
744 * @param {ntp.AppsPage} appPage The app page for which we wish to save.
745 * @param {string} name The name of the page.
747 function saveAppPageName(appPage, name) {
748 newTabView.saveAppPageName(appPage, name);
751 function setAppToBeHighlighted(appId) {
752 newTabView.highlightAppId = appId;
755 // Return an object with all the exports
759 appRemoved: appRemoved,
760 appsPrefChangeCallback: appsPrefChangeCallback,
761 appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
762 enterRearrangeMode: enterRearrangeMode,
763 getAppsCallback: getAppsCallback,
764 getAppsPageIndex: getAppsPageIndex,
765 getCardSlider: getCardSlider,
767 leaveRearrangeMode: leaveRearrangeMode,
768 logTimeToClick: logTimeToClick,
769 NtpFollowAction: NtpFollowAction,
770 saveAppPageName: saveAppPageName,
771 setAppToBeHighlighted: setAppToBeHighlighted,
772 setBookmarkBarAttached: setBookmarkBarAttached,
773 setForeignSessions: setForeignSessions,
774 setMostVisitedPages: setMostVisitedPages,
775 setSuggestionsPages: setSuggestionsPages,
776 setRecentlyClosedTabs: setRecentlyClosedTabs,
777 setFaviconDominantColor: setFaviconDominantColor,
778 showNotification: showNotification,
779 themeChanged: themeChanged,
780 updateLogin: updateLogin
784 document.addEventListener('DOMContentLoaded', ntp.onLoad);
786 var toCssPx = cr.ui.toCssPx;