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 * Called when the theme has changed.
337 function themeChanged() {
338 $('themecss').href
= 'chrome://theme/css/new_tab_theme.css?' + Date
.now();
341 function setBookmarkBarAttached(attached
) {
342 document
.documentElement
.setAttribute('bookmarkbarattached', attached
);
349 var notificationTimeout
= 0;
352 * Shows the notification bubble.
353 * @param {string|Node} message The notification message or node to use as
355 * @param {Array<{text: string, action: function()}>} links An array of
356 * records describing the links in the notification. Each record should
357 * have a 'text' attribute (the display string) and an 'action' attribute
358 * (a function to run when the link is activated).
359 * @param {Function=} opt_closeHandler The callback invoked if the user
360 * manually dismisses the notification.
361 * @param {number=} opt_timeout
363 function showNotification(message
, links
, opt_closeHandler
, opt_timeout
) {
364 window
.clearTimeout(notificationTimeout
);
366 var span
= document
.querySelector('#notification > span');
367 if (typeof message
== 'string') {
368 span
.textContent
= message
;
370 span
.textContent
= ''; // Remove all children.
371 span
.appendChild(message
);
374 var linksBin
= $('notificationLinks');
375 linksBin
.textContent
= '';
376 for (var i
= 0; i
< links
.length
; i
++) {
377 var link
= new ActionLink
;
378 link
.textContent
= links
[i
].text
;
379 link
.action
= links
[i
].action
;
380 link
.onclick = function() {
384 linksBin
.appendChild(link
);
387 function closeFunc(e
) {
388 if (opt_closeHandler
)
393 document
.querySelector('#notification button').onclick
= closeFunc
;
394 document
.addEventListener('dragstart', closeFunc
);
396 notificationContainer
.hidden
= false;
397 showNotificationOnCurrentPage();
399 newTabView
.cardSlider
.frame
.addEventListener(
400 'cardSlider:card_change_ended', onCardChangeEnded
);
402 var timeout
= opt_timeout
|| 10000;
403 notificationTimeout
= window
.setTimeout(hideNotification
, timeout
);
407 * Hide the notification bubble.
409 function hideNotification() {
410 notificationContainer
.classList
.add('inactive');
412 newTabView
.cardSlider
.frame
.removeEventListener(
413 'cardSlider:card_change_ended', onCardChangeEnded
);
417 * Happens when 1 or more consecutive card changes end.
418 * @param {Event} e The cardSlider:card_change_ended event.
420 function onCardChangeEnded(e
) {
421 // If we ended on the same page as we started, ignore.
422 if (newTabView
.cardSlider
.currentCardValue
.notification
)
425 // Hide the notification the old page.
426 notificationContainer
.classList
.add('card-changed');
428 showNotificationOnCurrentPage();
432 * Move and show the notification on the current page.
434 function showNotificationOnCurrentPage() {
435 var page
= newTabView
.cardSlider
.currentCardValue
;
436 doWhenAllSectionsReady(function() {
437 if (page
!= newTabView
.cardSlider
.currentCardValue
)
440 // NOTE: This moves the notification to inside of the current page.
441 page
.notification
= notificationContainer
;
443 // Reveal the notification and instruct it to hide itself if ignored.
444 notificationContainer
.classList
.remove('inactive');
446 // Gives the browser time to apply this rule before we remove it (causing
448 window
.setTimeout(function() {
449 notificationContainer
.classList
.remove('card-changed');
455 * When done fading out, set hidden to true so the notification can't be
456 * tabbed to or clicked.
457 * @param {Event} e The webkitTransitionEnd event.
459 function onNotificationTransitionEnd(e
) {
460 if (notificationContainer
.classList
.contains('inactive'))
461 notificationContainer
.hidden
= true;
465 * Set the dominant color for a node. This will be called in response to
466 * getFaviconDominantColor. The node represented by |id| better have a setter
468 * @param {string} id The ID of a node.
469 * @param {string} color The color represented as a CSS string.
471 function setFaviconDominantColor(id
, color
) {
474 node
.stripeColor
= color
;
478 * Updates the text displayed in the login container. If there is no text then
479 * the login container is hidden.
480 * @param {string} loginHeader The first line of text.
481 * @param {string} loginSubHeader The second line of text.
482 * @param {string} iconURL The url for the login status icon. If this is null
483 then the login status icon is hidden.
484 * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
486 function updateLogin(loginHeader
, loginSubHeader
, iconURL
, isUserSignedIn
) {
487 /** @const */ var showLogin
= loginHeader
|| loginSubHeader
;
489 $('login-container').hidden
= !showLogin
;
490 $('login-container').classList
.toggle('signed-in', isUserSignedIn
);
491 $('card-slider-frame').classList
.toggle('showing-login-area', !!showLogin
);
494 // TODO(dbeam): we should use .textContent instead to mitigate XSS.
495 $('login-status-header').innerHTML
= loginHeader
;
496 $('login-status-sub-header').innerHTML
= loginSubHeader
;
498 var headerContainer
= $('login-status-header-container');
499 headerContainer
.classList
.toggle('login-status-icon', !!iconURL
);
500 headerContainer
.style
.backgroundImage
= iconURL
? url(iconURL
) : 'none';
503 if (shouldShowLoginBubble
) {
504 window
.setTimeout(loginBubble
.show
.bind(loginBubble
), 0);
505 chrome
.send('loginMessageSeen');
506 shouldShowLoginBubble
= false;
507 } else if (loginBubble
) {
508 loginBubble
.reposition();
513 * Show the sync login UI.
514 * @param {Event} e The click event.
516 function showSyncLoginUI(e
) {
517 var rect
= e
.currentTarget
.getBoundingClientRect();
518 chrome
.send('showSyncLoginUI',
519 [rect
.left
, rect
.top
, rect
.width
, rect
.height
]);
523 * Wrappers to forward the callback to corresponding PageListView member.
527 * Called by chrome when a new app has been added to chrome or has been
528 * enabled if previously disabled.
529 * @param {Object} appData A data structure full of relevant information for
531 * @param {boolean=} opt_highlight Whether the app about to be added should
534 function appAdded(appData
, opt_highlight
) {
535 newTabView
.appAdded(appData
, opt_highlight
);
539 * Called by chrome when an app has changed positions.
540 * @param {Object} appData The data for the app. This contains page and
543 function appMoved(appData
) {
544 newTabView
.appMoved(appData
);
548 * Called by chrome when an existing app has been disabled or
549 * removed/uninstalled from chrome.
550 * @param {Object} appData A data structure full of relevant information for
552 * @param {boolean} isUninstall True if the app is being uninstalled;
553 * false if the app is being disabled.
554 * @param {boolean} fromPage True if the removal was from the current page.
556 function appRemoved(appData
, isUninstall
, fromPage
) {
557 newTabView
.appRemoved(appData
, isUninstall
, fromPage
);
561 * Callback invoked by chrome whenever an app preference changes.
562 * @param {Object} data An object with all the data on available
565 function appsPrefChangeCallback(data
) {
566 newTabView
.appsPrefChangedCallback(data
);
570 * Callback invoked by chrome whenever the app launcher promo pref changes.
571 * @param {boolean} show Identifies if we should show or hide the promo.
573 function appLauncherPromoPrefChangeCallback(show
) {
574 newTabView
.appLauncherPromoPrefChangeCallback(show
);
578 * Called whenever tiles should be re-arranging themselves out of the way
579 * of a moving or insert tile.
581 function enterRearrangeMode() {
582 newTabView
.enterRearrangeMode();
586 * Callback invoked by chrome with the apps available.
588 * Note that calls to this function can occur at any time, not just in
589 * response to a getApps request. For example, when a user
590 * installs/uninstalls an app on another synchronized devices.
591 * @param {Object} data An object with all the data on available
594 function getAppsCallback(data
) {
595 newTabView
.getAppsCallback(data
);
599 * Return the index of the given apps page.
600 * @param {ntp.AppsPage} page The AppsPage we wish to find.
601 * @return {number} The index of |page| or -1 if it is not in the collection.
603 function getAppsPageIndex(page
) {
604 return newTabView
.getAppsPageIndex(page
);
607 function getCardSlider() {
608 return newTabView
.cardSlider
;
612 * Invoked whenever some app is released
614 function leaveRearrangeMode() {
615 newTabView
.leaveRearrangeMode();
619 * Save the name of an apps page.
620 * Store the apps page name into the preferences store.
621 * @param {ntp.AppsPage} appPage The app page for which we wish to save.
622 * @param {string} name The name of the page.
624 function saveAppPageName(appPage
, name
) {
625 newTabView
.saveAppPageName(appPage
, name
);
628 function setAppToBeHighlighted(appId
) {
629 newTabView
.highlightAppId
= appId
;
632 // Return an object with all the exports
636 appRemoved
: appRemoved
,
637 appsPrefChangeCallback
: appsPrefChangeCallback
,
638 appLauncherPromoPrefChangeCallback
: appLauncherPromoPrefChangeCallback
,
639 enterRearrangeMode
: enterRearrangeMode
,
640 getAppsCallback
: getAppsCallback
,
641 getAppsPageIndex
: getAppsPageIndex
,
642 getCardSlider
: getCardSlider
,
644 leaveRearrangeMode
: leaveRearrangeMode
,
645 NtpFollowAction
: NtpFollowAction
,
646 saveAppPageName
: saveAppPageName
,
647 setAppToBeHighlighted
: setAppToBeHighlighted
,
648 setBookmarkBarAttached
: setBookmarkBarAttached
,
649 setFaviconDominantColor
: setFaviconDominantColor
,
650 showNotification
: showNotification
,
651 themeChanged
: themeChanged
,
652 updateLogin
: updateLogin
656 document
.addEventListener('DOMContentLoaded', ntp
.onLoad
);
658 var toCssPx
= cr
.ui
.toCssPx
;