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 {!cr.ui.Bubble|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 {!cr.ui.Bubble|undefined}
44 * true if |loginBubble| should be shown.
47 var shouldShowLoginBubble = false;
50 * The time when all sections are ready.
51 * @type {number|undefined}
57 * The time in milliseconds for most transitions. This should match what's
58 * in new_tab.css. Unfortunately there's no better way to try to time
59 * something to occur until after a transition has completed.
63 var DEFAULT_TRANSITION_TIME = 500;
66 * See description for these values in ntp_stats.h.
69 var NtpFollowAction = {
71 CLICKED_OTHER_NTP_PANE: 12,
76 * Creates a NewTabView object. NewTabView extends PageListView with
77 * new tab UI specific logics.
79 * @extends {ntp.PageListView}
81 function NewTabView() {
82 var pageSwitcherStart;
84 if (loadTimeData.getValue('showApps')) {
85 pageSwitcherStart = /** @type {!ntp.PageSwitcher} */(
86 getRequiredElement('page-switcher-start'));
87 pageSwitcherEnd = /** @type {!ntp.PageSwitcher} */(
88 getRequiredElement('page-switcher-end'));
90 this.initialize(getRequiredElement('page-list'),
91 getRequiredElement('dot-list'),
92 getRequiredElement('card-slider-frame'),
93 getRequiredElement('trash'),
94 pageSwitcherStart, pageSwitcherEnd);
97 NewTabView.prototype = {
98 __proto__: ntp.PageListView.prototype,
101 appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
102 ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
105 window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
110 * Invoked at startup once the DOM is available to initialize the app.
113 sectionsToWaitFor = 0;
114 if (loadTimeData.getBoolean('showApps')) {
116 if (loadTimeData.getBoolean('showAppLauncherPromo')) {
117 $('app-launcher-promo-close-button').addEventListener('click',
118 function() { chrome.send('stopShowingAppLauncherPromo'); });
119 $('apps-promo-learn-more').addEventListener('click',
120 function() { chrome.send('onLearnMore'); });
125 // Load the current theme colors.
128 newTabView = new NewTabView();
130 notificationContainer = getRequiredElement('notification-container');
131 notificationContainer.addEventListener(
132 'webkitTransitionEnd', onNotificationTransitionEnd);
134 if (!loadTimeData.getBoolean('showWebStoreIcon')) {
135 var webStoreIcon = $('chrome-web-store-link');
136 // Not all versions of the NTP have a footer, so this may not exist.
138 webStoreIcon.hidden = true;
140 var webStoreLink = loadTimeData.getString('webStoreLink');
141 var url = appendParam(webStoreLink, 'utm_source', 'chrome-ntp-launcher');
142 $('chrome-web-store-link').href = url;
143 $('chrome-web-store-link').addEventListener('click',
144 onChromeWebStoreButtonClick);
147 // We need to wait for all the footer menu setup to be completed before
148 // we can compute its layout.
151 if (loadTimeData.getString('login_status_message')) {
152 loginBubble = new cr.ui.Bubble;
153 loginBubble.anchorNode = $('login-container');
154 loginBubble.arrowLocation = cr.ui.ArrowLocation.TOP_END;
155 loginBubble.bubbleAlignment =
156 cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE;
157 loginBubble.deactivateToDismissDelay = 2000;
158 loginBubble.closeButtonVisible = false;
160 $('login-status-advanced').onclick = function() {
161 chrome.send('showAdvancedLoginUI');
163 $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
165 var bubbleContent = $('login-status-bubble-contents');
166 loginBubble.content = bubbleContent;
168 // The anchor node won't be updated until updateLogin is called so don't
169 // show the bubble yet.
170 shouldShowLoginBubble = true;
173 if (loadTimeData.valueExists('bubblePromoText')) {
174 promoBubble = new cr.ui.Bubble;
175 promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor');
176 promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START;
177 promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE;
178 promoBubble.deactivateToDismissDelay = 2000;
179 promoBubble.content = parseHtmlSubset(
180 loadTimeData.getString('bubblePromoText'), ['BR']);
182 var bubbleLink = promoBubble.querySelector('a');
184 bubbleLink.addEventListener('click', function(e) {
185 chrome.send('bubblePromoLinkClicked');
189 promoBubble.handleCloseEvent = function() {
191 chrome.send('bubblePromoClosed');
194 chrome.send('bubblePromoViewed');
197 $('login-container').addEventListener('click', showSyncLoginUI);
198 if (loadTimeData.getBoolean('shouldShowSyncLogin'))
199 chrome.send('initializeSyncLogin');
201 doWhenAllSectionsReady(function() {
202 // Tell the slider about the pages.
203 newTabView.updateSliderCards();
204 // Mark the current page.
205 newTabView.cardSlider.currentCardValue.navigationDot.classList.add(
208 if (loadTimeData.valueExists('notificationPromoText')) {
209 var promoText = loadTimeData.getString('notificationPromoText');
212 src: function(node, value) {
213 return node.tagName == 'IMG' &&
214 /^data\:image\/(?:png|gif|jpe?g)/.test(value);
218 var promo = parseHtmlSubset(promoText, tags, attrs);
219 var promoLink = promo.querySelector('a');
221 promoLink.addEventListener('click', function(e) {
222 chrome.send('notificationPromoLinkClicked');
226 showNotification(promo, [], function() {
227 chrome.send('notificationPromoClosed');
229 chrome.send('notificationPromoViewed');
232 cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
233 document.documentElement.classList.remove('starting-up');
235 startTime = Date.now();
240 * Launches the chrome web store app with the chrome-ntp-launcher
242 * @param {Event} e The click event.
244 function onChromeWebStoreButtonClick(e) {
245 chrome.send('recordAppLaunchByURL',
246 [encodeURIComponent(this.href),
247 ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
251 * The number of sections to wait on.
254 var sectionsToWaitFor = -1;
257 * Queued callbacks which lie in wait for all sections to be ready.
260 var readyCallbacks = [];
263 * Fired as each section of pages becomes ready.
264 * @param {Event} e Each page's synthetic DOM event.
266 document.addEventListener('sectionready', function(e) {
267 if (--sectionsToWaitFor <= 0) {
268 while (readyCallbacks.length) {
269 readyCallbacks.shift()();
275 * This is used to simulate a fire-once event (i.e. $(document).ready() in
276 * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
277 * is fired right away. If all pages are not ready yet, the function is queued
278 * for later execution.
279 * @param {Function} callback The work to be done when ready.
281 function doWhenAllSectionsReady(callback) {
282 assert(typeof callback == 'function');
283 if (sectionsToWaitFor > 0)
284 readyCallbacks.push(callback);
286 window.setTimeout(callback, 0); // Do soon after, but asynchronously.
290 * Measure the width of a nav dot with a given title.
291 * @param {string} id The loadTimeData ID of the desired title.
292 * @return {number} The width of the nav dot.
294 function measureNavDot(id) {
295 var measuringDiv = $('fontMeasuringDiv');
296 measuringDiv.textContent = loadTimeData.getString(id);
297 // The 4 is for border and padding.
298 return Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
302 * Fills in an invisible div with the longest dot title string so that
303 * its length may be measured and the nav dots sized accordingly.
305 function measureNavDots() {
306 var styleElement = document.createElement('style');
307 styleElement.type = 'text/css';
308 // max-width is used because if we run out of space, the nav dots will be
310 var pxWidth = measureNavDot('appDefaultPageName');
311 styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
312 document.querySelector('head').appendChild(styleElement);
316 * Layout the footer so that the nav dots stay centered.
318 function layoutFooter() {
319 // We need the image to be loaded.
320 var logo = $('logo-img');
321 var logoImg = logo.querySelector('img');
322 if (!logoImg.complete) {
323 logoImg.onload = layoutFooter;
327 var menu = $('footer-menu-container');
328 if (menu.clientWidth > logoImg.width)
329 logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px';
331 menu.style.WebkitFlex = '0 1 ' + logoImg.width + 'px';
335 * @param {boolean=} opt_hasAttribution
337 function themeChanged(opt_hasAttribution) {
338 $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
340 if (typeof opt_hasAttribution != 'undefined') {
341 document.documentElement.setAttribute('hasattribution',
348 function setBookmarkBarAttached(attached) {
349 document.documentElement.setAttribute('bookmarkbarattached', attached);
353 * Attributes the attribution image at the bottom left.
355 function updateAttribution() {
356 var attribution = $('attribution');
357 if (document.documentElement.getAttribute('hasattribution') == 'true') {
358 attribution.hidden = false;
360 attribution.hidden = true;
368 var notificationTimeout = 0;
371 * Shows the notification bubble.
372 * @param {string|Node} message The notification message or node to use as
374 * @param {Array<{text: string, action: function()}>} links An array of
375 * records describing the links in the notification. Each record should
376 * have a 'text' attribute (the display string) and an 'action' attribute
377 * (a function to run when the link is activated).
378 * @param {Function=} opt_closeHandler The callback invoked if the user
379 * manually dismisses the notification.
380 * @param {number=} opt_timeout
382 function showNotification(message, links, opt_closeHandler, opt_timeout) {
383 window.clearTimeout(notificationTimeout);
385 var span = document.querySelector('#notification > span');
386 if (typeof message == 'string') {
387 span.textContent = message;
389 span.textContent = ''; // Remove all children.
390 span.appendChild(message);
393 var linksBin = $('notificationLinks');
394 linksBin.textContent = '';
395 for (var i = 0; i < links.length; i++) {
396 var link = new ActionLink;
397 link.textContent = links[i].text;
398 link.action = links[i].action;
399 link.onclick = function() {
403 linksBin.appendChild(link);
406 function closeFunc(e) {
407 if (opt_closeHandler)
412 document.querySelector('#notification button').onclick = closeFunc;
413 document.addEventListener('dragstart', closeFunc);
415 notificationContainer.hidden = false;
416 showNotificationOnCurrentPage();
418 newTabView.cardSlider.frame.addEventListener(
419 'cardSlider:card_change_ended', onCardChangeEnded);
421 var timeout = opt_timeout || 10000;
422 notificationTimeout = window.setTimeout(hideNotification, timeout);
426 * Hide the notification bubble.
428 function hideNotification() {
429 notificationContainer.classList.add('inactive');
431 newTabView.cardSlider.frame.removeEventListener(
432 'cardSlider:card_change_ended', onCardChangeEnded);
436 * Happens when 1 or more consecutive card changes end.
437 * @param {Event} e The cardSlider:card_change_ended event.
439 function onCardChangeEnded(e) {
440 // If we ended on the same page as we started, ignore.
441 if (newTabView.cardSlider.currentCardValue.notification)
444 // Hide the notification the old page.
445 notificationContainer.classList.add('card-changed');
447 showNotificationOnCurrentPage();
451 * Move and show the notification on the current page.
453 function showNotificationOnCurrentPage() {
454 var page = newTabView.cardSlider.currentCardValue;
455 doWhenAllSectionsReady(function() {
456 if (page != newTabView.cardSlider.currentCardValue)
459 // NOTE: This moves the notification to inside of the current page.
460 page.notification = notificationContainer;
462 // Reveal the notification and instruct it to hide itself if ignored.
463 notificationContainer.classList.remove('inactive');
465 // Gives the browser time to apply this rule before we remove it (causing
467 window.setTimeout(function() {
468 notificationContainer.classList.remove('card-changed');
474 * When done fading out, set hidden to true so the notification can't be
475 * tabbed to or clicked.
476 * @param {Event} e The webkitTransitionEnd event.
478 function onNotificationTransitionEnd(e) {
479 if (notificationContainer.classList.contains('inactive'))
480 notificationContainer.hidden = true;
484 * Set the dominant color for a node. This will be called in response to
485 * getFaviconDominantColor. The node represented by |id| better have a setter
487 * @param {string} id The ID of a node.
488 * @param {string} color The color represented as a CSS string.
490 function setFaviconDominantColor(id, color) {
493 node.stripeColor = color;
497 * Updates the text displayed in the login container. If there is no text then
498 * the login container is hidden.
499 * @param {string} loginHeader The first line of text.
500 * @param {string} loginSubHeader The second line of text.
501 * @param {string} iconURL The url for the login status icon. If this is null
502 then the login status icon is hidden.
503 * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
505 function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) {
506 /** @const */ var showLogin = loginHeader || loginSubHeader;
508 $('login-container').hidden = !showLogin;
509 $('login-container').classList.toggle('signed-in', isUserSignedIn);
510 $('card-slider-frame').classList.toggle('showing-login-area', !!showLogin);
513 // TODO(dbeam): we should use .textContent instead to mitigate XSS.
514 $('login-status-header').innerHTML = loginHeader;
515 $('login-status-sub-header').innerHTML = loginSubHeader;
517 var headerContainer = $('login-status-header-container');
518 headerContainer.classList.toggle('login-status-icon', !!iconURL);
519 headerContainer.style.backgroundImage = iconURL ? url(iconURL) : 'none';
522 if (shouldShowLoginBubble) {
523 window.setTimeout(loginBubble.show.bind(loginBubble), 0);
524 chrome.send('loginMessageSeen');
525 shouldShowLoginBubble = false;
526 } else if (loginBubble) {
527 loginBubble.reposition();
532 * Show the sync login UI.
533 * @param {Event} e The click event.
535 function showSyncLoginUI(e) {
536 var rect = e.currentTarget.getBoundingClientRect();
537 chrome.send('showSyncLoginUI',
538 [rect.left, rect.top, rect.width, rect.height]);
542 * Wrappers to forward the callback to corresponding PageListView member.
546 * Called by chrome when a new app has been added to chrome or has been
547 * enabled if previously disabled.
548 * @param {Object} appData A data structure full of relevant information for
550 * @param {boolean=} opt_highlight Whether the app about to be added should
553 function appAdded(appData, opt_highlight) {
554 newTabView.appAdded(appData, opt_highlight);
558 * Called by chrome when an app has changed positions.
559 * @param {Object} appData The data for the app. This contains page and
562 function appMoved(appData) {
563 newTabView.appMoved(appData);
567 * Called by chrome when an existing app has been disabled or
568 * removed/uninstalled from chrome.
569 * @param {Object} appData A data structure full of relevant information for
571 * @param {boolean} isUninstall True if the app is being uninstalled;
572 * false if the app is being disabled.
573 * @param {boolean} fromPage True if the removal was from the current page.
575 function appRemoved(appData, isUninstall, fromPage) {
576 newTabView.appRemoved(appData, isUninstall, fromPage);
580 * Callback invoked by chrome whenever an app preference changes.
581 * @param {Object} data An object with all the data on available
584 function appsPrefChangeCallback(data) {
585 newTabView.appsPrefChangedCallback(data);
589 * Callback invoked by chrome whenever the app launcher promo pref changes.
590 * @param {boolean} show Identifies if we should show or hide the promo.
592 function appLauncherPromoPrefChangeCallback(show) {
593 newTabView.appLauncherPromoPrefChangeCallback(show);
597 * Called whenever tiles should be re-arranging themselves out of the way
598 * of a moving or insert tile.
600 function enterRearrangeMode() {
601 newTabView.enterRearrangeMode();
605 * Callback invoked by chrome with the apps available.
607 * Note that calls to this function can occur at any time, not just in
608 * response to a getApps request. For example, when a user
609 * installs/uninstalls an app on another synchronized devices.
610 * @param {Object} data An object with all the data on available
613 function getAppsCallback(data) {
614 newTabView.getAppsCallback(data);
618 * Return the index of the given apps page.
619 * @param {ntp.AppsPage} page The AppsPage we wish to find.
620 * @return {number} The index of |page| or -1 if it is not in the collection.
622 function getAppsPageIndex(page) {
623 return newTabView.getAppsPageIndex(page);
626 function getCardSlider() {
627 return newTabView.cardSlider;
631 * Invoked whenever some app is released
633 function leaveRearrangeMode() {
634 newTabView.leaveRearrangeMode();
638 * Save the name of an apps page.
639 * Store the apps page name into the preferences store.
640 * @param {ntp.AppsPage} appPage The app page for which we wish to save.
641 * @param {string} name The name of the page.
643 function saveAppPageName(appPage, name) {
644 newTabView.saveAppPageName(appPage, name);
647 function setAppToBeHighlighted(appId) {
648 newTabView.highlightAppId = appId;
651 // Return an object with all the exports
655 appRemoved: appRemoved,
656 appsPrefChangeCallback: appsPrefChangeCallback,
657 appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
658 enterRearrangeMode: enterRearrangeMode,
659 getAppsCallback: getAppsCallback,
660 getAppsPageIndex: getAppsPageIndex,
661 getCardSlider: getCardSlider,
663 leaveRearrangeMode: leaveRearrangeMode,
664 NtpFollowAction: NtpFollowAction,
665 saveAppPageName: saveAppPageName,
666 setAppToBeHighlighted: setAppToBeHighlighted,
667 setBookmarkBarAttached: setBookmarkBarAttached,
668 setFaviconDominantColor: setFaviconDominantColor,
669 showNotification: showNotification,
670 themeChanged: themeChanged,
671 updateLogin: updateLogin
675 document.addEventListener('DOMContentLoaded', ntp.onLoad);
677 var toCssPx = cr.ui.toCssPx;