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.
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() {
17 * NewTabView instance.
18 * @type {!Object|undefined}
23 * The 'notification-container' element.
24 * @type {!Element|undefined}
26 var notificationContainer;
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
32 * @type {!Element|undefined}
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}
44 * true if |loginBubble| should be shown.
47 var shouldShowLoginBubble = false;
50 * The 'other-sessions-menu-button' element.
51 * @type {!Element|undefined}
53 var otherSessionsButton;
56 * The time when all sections are ready.
57 * @type {number|undefined}
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.
69 var DEFAULT_TRANSITION_TIME = 500;
72 * See description for these values in ntp_stats.h.
75 var NtpFollowAction = {
77 CLICKED_OTHER_NTP_PANE: 12,
82 * Creates a NewTabView object. NewTabView extends PageListView with
83 * new tab UI specific logics.
85 * @extends {PageListView}
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');
94 this.initialize(getRequiredElement('page-list'),
95 getRequiredElement('dot-list'),
96 getRequiredElement('card-slider-frame'),
97 getRequiredElement('trash'),
98 pageSwitcherStart, pageSwitcherEnd);
101 NewTabView.prototype = {
102 __proto__: ntp.PageListView.prototype,
105 appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
106 ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
109 window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
114 * Invoked at startup once the DOM is available to initialize the app.
117 sectionsToWaitFor = 0;
118 if (loadTimeData.getBoolean('showMostvisited'))
120 if (loadTimeData.getBoolean('showApps')) {
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'); });
129 if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled'))
133 // Load the current theme colors.
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');
146 $('recently-closed-menu-button').hidden = true;
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'));
154 getRequiredElement('other-sessions-menu-button').hidden = true;
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"
161 if (document.body.classList.contains('bare-minimum'))
162 mostVisited.appendFooter(getRequiredElement('footer'));
163 newTabView.appendTilePage(mostVisited,
164 loadTimeData.getString('mostvisited'),
166 chrome.send('getMostVisited');
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'),
176 (newTabView.appsPages.length > 0) ?
177 newTabView.appsPages[0] : null);
178 chrome.send('getSuggestions');
179 cr.dispatchSimpleEvent(document, 'sectionready', true, true);
181 document.querySelector('head').appendChild(suggestionsScript);
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.
188 webStoreIcon.hidden = true;
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);
197 // We need to wait for all the footer menu setup to be completed before
198 // we can compute its layout.
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');
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;
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');
234 bubbleLink.addEventListener('click', function(e) {
235 chrome.send('bubblePromoLinkClicked');
239 promoBubble.handleCloseEvent = function() {
241 chrome.send('bubblePromoClosed');
244 chrome.send('bubblePromoViewed');
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(
259 if (loadTimeData.valueExists('notificationPromoText')) {
260 var promoText = loadTimeData.getString('notificationPromoText');
263 src: function(node, value) {
264 return node.tagName == 'IMG' &&
265 /^data\:image\/(?:png|gif|jpe?g)/.test(value);
269 var promo = parseHtmlSubset(promoText, tags, attrs);
270 var promoLink = promo.querySelector('a');
272 promoLink.addEventListener('click', function(e) {
273 chrome.send('notificationPromoLinkClicked');
277 showNotification(promo, [], function() {
278 chrome.send('notificationPromoClosed');
280 chrome.send('notificationPromoViewed');
283 cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
284 document.documentElement.classList.remove('starting-up');
286 startTime = Date.now();
289 preventDefaultOnPoundLinkClicks(); // From webui/js/util.js.
290 cr.ui.FocusManager.disableMouseFocusOnButtons();
294 * Launches the chrome web store app with the chrome-ntp-launcher
296 * @param {Event} e The click event.
298 function onChromeWebStoreButtonClick(e) {
299 chrome.send('recordAppLaunchByURL',
300 [encodeURIComponent(this.href),
301 ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
305 * The number of sections to wait on.
308 var sectionsToWaitFor = -1;
311 * Queued callbacks which lie in wait for all sections to be ready.
314 var readyCallbacks = [];
317 * Fired as each section of pages becomes ready.
318 * @param {Event} e Each page's synthetic DOM event.
320 document.addEventListener('sectionready', function(e) {
321 if (--sectionsToWaitFor <= 0) {
322 while (readyCallbacks.length) {
323 readyCallbacks.shift()();
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.
335 function doWhenAllSectionsReady(callback) {
336 assert(typeof callback == 'function');
337 if (sectionsToWaitFor > 0)
338 readyCallbacks.push(callback);
340 window.setTimeout(callback, 0); // Do soon after, but asynchronously.
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.
347 function measureNavDots() {
348 var measuringDiv = $('fontMeasuringDiv');
349 if (loadTimeData.getBoolean('showMostvisited'))
350 measuringDiv.textContent = loadTimeData.getString('mostvisited');
352 // The 4 is for border and padding.
353 var pxWidth = Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
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
359 styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
360 document.querySelector('head').appendChild(styleElement);
364 * Layout the footer so that the nav dots stay centered.
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';
372 menu.style.WebkitFlex = '0 1 ' + logo.clientWidth + 'px';
375 function themeChanged(opt_hasAttribution) {
376 $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
378 if (typeof opt_hasAttribution != 'undefined') {
379 document.documentElement.setAttribute('hasattribution',
386 function setBookmarkBarAttached(attached) {
387 document.documentElement.setAttribute('bookmarkbarattached', attached);
391 * Attributes the attribution image at the bottom left.
393 function updateAttribution() {
394 var attribution = $('attribution');
395 if (document.documentElement.getAttribute('hasattribution') == 'true') {
396 attribution.hidden = false;
398 attribution.hidden = true;
406 var notificationTimeout = 0;
409 * Shows the notification bubble.
410 * @param {string|Node} message The notification message or node to use as
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.
419 function showNotification(message, links, opt_closeHandler, opt_timeout) {
420 window.clearTimeout(notificationTimeout);
422 var span = document.querySelector('#notification > span');
423 if (typeof message == 'string') {
424 span.textContent = message;
426 span.textContent = ''; // Remove all children.
427 span.appendChild(message);
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() {
440 link.setAttribute('role', 'button');
441 link.setAttribute('tabindex', 0);
442 link.className = 'link-button';
443 linksBin.appendChild(link);
446 function closeFunc(e) {
447 if (opt_closeHandler)
452 document.querySelector('#notification button').onclick = closeFunc;
453 document.addEventListener('dragstart', closeFunc);
455 notificationContainer.hidden = false;
456 showNotificationOnCurrentPage();
458 newTabView.cardSlider.frame.addEventListener(
459 'cardSlider:card_change_ended', onCardChangeEnded);
461 var timeout = opt_timeout || 10000;
462 notificationTimeout = window.setTimeout(hideNotification, timeout);
466 * Hide the notification bubble.
468 function hideNotification() {
469 notificationContainer.classList.add('inactive');
471 newTabView.cardSlider.frame.removeEventListener(
472 'cardSlider:card_change_ended', onCardChangeEnded);
476 * Happens when 1 or more consecutive card changes end.
477 * @param {Event} e The cardSlider:card_change_ended event.
479 function onCardChangeEnded(e) {
480 // If we ended on the same page as we started, ignore.
481 if (newTabView.cardSlider.currentCardValue.notification)
484 // Hide the notification the old page.
485 notificationContainer.classList.add('card-changed');
487 showNotificationOnCurrentPage();
491 * Move and show the notification on the current page.
493 function showNotificationOnCurrentPage() {
494 var page = newTabView.cardSlider.currentCardValue;
495 doWhenAllSectionsReady(function() {
496 if (page != newTabView.cardSlider.currentCardValue)
499 // NOTE: This moves the notification to inside of the current page.
500 page.notification = notificationContainer;
502 // Reveal the notification and instruct it to hide itself if ignored.
503 notificationContainer.classList.remove('inactive');
505 // Gives the browser time to apply this rule before we remove it (causing
507 window.setTimeout(function() {
508 notificationContainer.classList.remove('card-changed');
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.
518 function onNotificationTransitionEnd(e) {
519 if (notificationContainer.classList.contains('inactive'))
520 notificationContainer.hidden = true;
523 function setRecentlyClosedTabs(dataItems) {
524 $('recently-closed-menu-button').dataItems = dataItems;
528 function setMostVisitedPages(data, hasBlacklistedUrls) {
529 newTabView.mostVisitedPage.data = data;
530 cr.dispatchSimpleEvent(document, 'sectionready', true, true);
533 function setSuggestionsPages(data, hasBlacklistedUrls) {
534 newTabView.suggestionsPage.data = data;
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
541 * @param {string} id The ID of a node.
542 * @param {string} color The color represented as a CSS string.
544 function setFaviconDominantColor(id, color) {
547 node.stripeColor = color;
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.
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');
567 $('login-status-header-container').style.backgroundImage = url(iconURL);
568 $('login-status-header-container').classList.add('login-status-icon');
570 $('login-status-header-container').style.backgroundImage = 'none';
571 $('login-status-header-container').classList.remove(
572 'login-status-icon');
575 $('login-container').hidden = true;
576 $('card-slider-frame').classList.remove('showing-login-area');
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();
585 if (otherSessionsButton) {
586 otherSessionsButton.updateSignInState(isUserSignedIn);
592 * Show the sync login UI.
593 * @param {Event} e The click event.
595 function showSyncLoginUI(e) {
596 var rect = e.currentTarget.getBoundingClientRect();
597 chrome.send('showSyncLoginUI',
598 [rect.left, rect.top, rect.width, rect.height]);
602 * Logs the time to click for the specified item.
603 * @param {string} item The item to log the time-to-click.
605 function logTimeToClick(item) {
606 var timeToClick = Date.now() - startTime;
607 chrome.send('logTimeToClick',
608 ['NewTabPage.TimeToClick' + item, timeToClick]);
612 * Wrappers to forward the callback to corresponding PageListView member.
614 function appAdded() {
615 return newTabView.appAdded.apply(newTabView, arguments);
618 function appMoved() {
619 return newTabView.appMoved.apply(newTabView, arguments);
622 function appRemoved() {
623 return newTabView.appRemoved.apply(newTabView, arguments);
626 function appsPrefChangeCallback() {
627 return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
630 function appLauncherPromoPrefChangeCallback() {
631 return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView,
635 function appsReordered() {
636 return newTabView.appsReordered.apply(newTabView, arguments);
639 function enterRearrangeMode() {
640 return newTabView.enterRearrangeMode.apply(newTabView, arguments);
643 function setForeignSessions(sessionList, isTabSyncEnabled) {
644 if (otherSessionsButton) {
645 otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
650 function getAppsCallback() {
651 return newTabView.getAppsCallback.apply(newTabView, arguments);
654 function getAppsPageIndex() {
655 return newTabView.getAppsPageIndex.apply(newTabView, arguments);
658 function getCardSlider() {
659 return newTabView.cardSlider;
662 function leaveRearrangeMode() {
663 return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
666 function saveAppPageName() {
667 return newTabView.saveAppPageName.apply(newTabView, arguments);
670 function setAppToBeHighlighted(appId) {
671 newTabView.highlightAppId = appId;
674 // Return an object with all the exports
678 appRemoved: appRemoved,
679 appsPrefChangeCallback: appsPrefChangeCallback,
680 appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
681 enterRearrangeMode: enterRearrangeMode,
682 getAppsCallback: getAppsCallback,
683 getAppsPageIndex: getAppsPageIndex,
684 getCardSlider: getCardSlider,
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
703 document.addEventListener('DOMContentLoaded', ntp.onLoad);
705 var toCssPx = cr.ui.toCssPx;