- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / ntp4 / new_tab.js
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.
4
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  */
10
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';
15
16   /**
17    * NewTabView instance.
18    * @type {!Object|undefined}
19    */
20   var newTabView;
21
22   /**
23    * The 'notification-container' element.
24    * @type {!Element|undefined}
25    */
26   var notificationContainer;
27
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;
35
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;
42
43   /**
44    * true if |loginBubble| should be shown.
45    * @type {boolean}
46    */
47   var shouldShowLoginBubble = false;
48
49   /**
50    * The 'other-sessions-menu-button' element.
51    * @type {!Element|undefined}
52    */
53   var otherSessionsButton;
54
55   /**
56    * The time when all sections are ready.
57    * @type {number|undefined}
58    * @private
59    */
60   var startTime;
61
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;
70
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   };
80
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   }
100
101   NewTabView.prototype = {
102     __proto__: ntp.PageListView.prototype,
103
104     /** @override */
105     appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
106       ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
107
108       if (promoBubble)
109         window.setTimeout(promoBubble.reposition.bind(promoBubble), 0);
110     }
111   };
112
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();
132
133     // Load the current theme colors.
134     themeChanged();
135
136     newTabView = new NewTabView();
137
138     notificationContainer = getRequiredElement('notification-container');
139     notificationContainer.addEventListener(
140         'webkitTransitionEnd', onNotificationTransitionEnd);
141
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     }
148
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     }
156
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     }
168
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     }
183
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     }
196
197     // We need to wait for all the footer menu setup to be completed before
198     // we can compute its layout.
199     layoutFooter();
200
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;
209
210       $('login-status-advanced').onclick = function() {
211         chrome.send('showAdvancedLoginUI');
212       };
213       $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
214
215       var bubbleContent = $('login-status-bubble-contents');
216       loginBubble.content = bubbleContent;
217
218       // The anchor node won't be updated until updateLogin is called so don't
219       // show the bubble yet.
220       shouldShowLoginBubble = true;
221     }
222
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']);
231
232       var bubbleLink = promoBubble.querySelector('a');
233       if (bubbleLink) {
234         bubbleLink.addEventListener('click', function(e) {
235           chrome.send('bubblePromoLinkClicked');
236         });
237       }
238
239       promoBubble.handleCloseEvent = function() {
240         promoBubble.hide();
241         chrome.send('bubblePromoClosed');
242       };
243       promoBubble.show();
244       chrome.send('bubblePromoViewed');
245     }
246
247     var loginContainer = getRequiredElement('login-container');
248     loginContainer.addEventListener('click', showSyncLoginUI);
249     if (loadTimeData.getBoolean('shouldShowSyncLogin'))
250       chrome.send('initializeSyncLogin');
251
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');
258
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         };
268
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         }
276
277         showNotification(promo, [], function() {
278           chrome.send('notificationPromoClosed');
279         }, 60000);
280         chrome.send('notificationPromoViewed');
281       }
282
283       cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
284       document.documentElement.classList.remove('starting-up');
285
286       startTime = Date.now();
287     });
288
289     preventDefaultOnPoundLinkClicks();  // From webui/js/util.js.
290     cr.ui.FocusManager.disableMouseFocusOnButtons();
291   }
292
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   }
303
304   /*
305    * The number of sections to wait on.
306    * @type {number}
307    */
308   var sectionsToWaitFor = -1;
309
310   /**
311    * Queued callbacks which lie in wait for all sections to be ready.
312    * @type {array}
313    */
314   var readyCallbacks = [];
315
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   });
327
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   }
342
343   /**
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.
346    */
347   function measureNavDots() {
348     var measuringDiv = $('fontMeasuringDiv');
349     if (loadTimeData.getBoolean('showMostvisited'))
350       measuringDiv.textContent = loadTimeData.getString('mostvisited');
351
352     // The 4 is for border and padding.
353     var pxWidth = Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
354
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
358     // shrunk.
359     styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
360     document.querySelector('head').appendChild(styleElement);
361   }
362
363   /**
364    * Layout the footer so that the nav dots stay centered.
365    */
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';
371     else
372       menu.style.WebkitFlex = '0 1 ' + logo.clientWidth + 'px';
373   }
374
375   function themeChanged(opt_hasAttribution) {
376     $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
377
378     if (typeof opt_hasAttribution != 'undefined') {
379       document.documentElement.setAttribute('hasattribution',
380                                             opt_hasAttribution);
381     }
382
383     updateAttribution();
384   }
385
386   function setBookmarkBarAttached(attached) {
387     document.documentElement.setAttribute('bookmarkbarattached', attached);
388   }
389
390   /**
391    * Attributes the attribution image at the bottom left.
392    */
393   function updateAttribution() {
394     var attribution = $('attribution');
395     if (document.documentElement.getAttribute('hasattribution') == 'true') {
396       attribution.hidden = false;
397     } else {
398       attribution.hidden = true;
399     }
400   }
401
402   /**
403    * Timeout ID.
404    * @type {number}
405    */
406   var notificationTimeout = 0;
407
408   /**
409    * Shows the notification bubble.
410    * @param {string|Node} message The notification message or node to use as
411    *     message.
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.
418    */
419   function showNotification(message, links, opt_closeHandler, opt_timeout) {
420     window.clearTimeout(notificationTimeout);
421
422     var span = document.querySelector('#notification > span');
423     if (typeof message == 'string') {
424       span.textContent = message;
425     } else {
426       span.textContent = '';  // Remove all children.
427       span.appendChild(message);
428     }
429
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() {
437         this.action();
438         hideNotification();
439       };
440       link.setAttribute('role', 'button');
441       link.setAttribute('tabindex', 0);
442       link.className = 'link-button';
443       linksBin.appendChild(link);
444     }
445
446     function closeFunc(e) {
447       if (opt_closeHandler)
448         opt_closeHandler();
449       hideNotification();
450     }
451
452     document.querySelector('#notification button').onclick = closeFunc;
453     document.addEventListener('dragstart', closeFunc);
454
455     notificationContainer.hidden = false;
456     showNotificationOnCurrentPage();
457
458     newTabView.cardSlider.frame.addEventListener(
459         'cardSlider:card_change_ended', onCardChangeEnded);
460
461     var timeout = opt_timeout || 10000;
462     notificationTimeout = window.setTimeout(hideNotification, timeout);
463   }
464
465   /**
466    * Hide the notification bubble.
467    */
468   function hideNotification() {
469     notificationContainer.classList.add('inactive');
470
471     newTabView.cardSlider.frame.removeEventListener(
472         'cardSlider:card_change_ended', onCardChangeEnded);
473   }
474
475   /**
476    * Happens when 1 or more consecutive card changes end.
477    * @param {Event} e The cardSlider:card_change_ended event.
478    */
479   function onCardChangeEnded(e) {
480     // If we ended on the same page as we started, ignore.
481     if (newTabView.cardSlider.currentCardValue.notification)
482       return;
483
484     // Hide the notification the old page.
485     notificationContainer.classList.add('card-changed');
486
487     showNotificationOnCurrentPage();
488   }
489
490   /**
491    * Move and show the notification on the current page.
492    */
493   function showNotificationOnCurrentPage() {
494     var page = newTabView.cardSlider.currentCardValue;
495     doWhenAllSectionsReady(function() {
496       if (page != newTabView.cardSlider.currentCardValue)
497         return;
498
499       // NOTE: This moves the notification to inside of the current page.
500       page.notification = notificationContainer;
501
502       // Reveal the notification and instruct it to hide itself if ignored.
503       notificationContainer.classList.remove('inactive');
504
505       // Gives the browser time to apply this rule before we remove it (causing
506       // a transition).
507       window.setTimeout(function() {
508         notificationContainer.classList.remove('card-changed');
509       }, 0);
510     });
511   }
512
513   /**
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.
517    */
518   function onNotificationTransitionEnd(e) {
519     if (notificationContainer.classList.contains('inactive'))
520       notificationContainer.hidden = true;
521   }
522
523   function setRecentlyClosedTabs(dataItems) {
524     $('recently-closed-menu-button').dataItems = dataItems;
525     layoutFooter();
526   }
527
528   function setMostVisitedPages(data, hasBlacklistedUrls) {
529     newTabView.mostVisitedPage.data = data;
530     cr.dispatchSimpleEvent(document, 'sectionready', true, true);
531   }
532
533   function setSuggestionsPages(data, hasBlacklistedUrls) {
534     newTabView.suggestionsPage.data = data;
535   }
536
537   /**
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
540    * for stripeColor.
541    * @param {string} id The ID of a node.
542    * @param {string} color The color represented as a CSS string.
543    */
544   function setFaviconDominantColor(id, color) {
545     var node = $(id);
546     if (node)
547       node.stripeColor = color;
548   }
549
550   /**
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.
558    */
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');
565
566       if (iconURL) {
567         $('login-status-header-container').style.backgroundImage = url(iconURL);
568         $('login-status-header-container').classList.add('login-status-icon');
569       } else {
570         $('login-status-header-container').style.backgroundImage = 'none';
571         $('login-status-header-container').classList.remove(
572             'login-status-icon');
573       }
574     } else {
575       $('login-container').hidden = true;
576       $('card-slider-frame').classList.remove('showing-login-area');
577     }
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();
584     }
585     if (otherSessionsButton) {
586       otherSessionsButton.updateSignInState(isUserSignedIn);
587       layoutFooter();
588     }
589   }
590
591   /**
592    * Show the sync login UI.
593    * @param {Event} e The click event.
594    */
595   function showSyncLoginUI(e) {
596     var rect = e.currentTarget.getBoundingClientRect();
597     chrome.send('showSyncLoginUI',
598                 [rect.left, rect.top, rect.width, rect.height]);
599   }
600
601   /**
602    * Logs the time to click for the specified item.
603    * @param {string} item The item to log the time-to-click.
604    */
605   function logTimeToClick(item) {
606     var timeToClick = Date.now() - startTime;
607     chrome.send('logTimeToClick',
608         ['NewTabPage.TimeToClick' + item, timeToClick]);
609   }
610
611   /**
612    * Wrappers to forward the callback to corresponding PageListView member.
613    */
614   function appAdded() {
615     return newTabView.appAdded.apply(newTabView, arguments);
616   }
617
618   function appMoved() {
619     return newTabView.appMoved.apply(newTabView, arguments);
620   }
621
622   function appRemoved() {
623     return newTabView.appRemoved.apply(newTabView, arguments);
624   }
625
626   function appsPrefChangeCallback() {
627     return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
628   }
629
630   function appLauncherPromoPrefChangeCallback() {
631     return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView,
632                                                                arguments);
633   }
634
635   function appsReordered() {
636     return newTabView.appsReordered.apply(newTabView, arguments);
637   }
638
639   function enterRearrangeMode() {
640     return newTabView.enterRearrangeMode.apply(newTabView, arguments);
641   }
642
643   function setForeignSessions(sessionList, isTabSyncEnabled) {
644     if (otherSessionsButton) {
645       otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
646       layoutFooter();
647     }
648   }
649
650   function getAppsCallback() {
651     return newTabView.getAppsCallback.apply(newTabView, arguments);
652   }
653
654   function getAppsPageIndex() {
655     return newTabView.getAppsPageIndex.apply(newTabView, arguments);
656   }
657
658   function getCardSlider() {
659     return newTabView.cardSlider;
660   }
661
662   function leaveRearrangeMode() {
663     return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
664   }
665
666   function saveAppPageName() {
667     return newTabView.saveAppPageName.apply(newTabView, arguments);
668   }
669
670   function setAppToBeHighlighted(appId) {
671     newTabView.highlightAppId = appId;
672   }
673
674   // Return an object with all the exports
675   return {
676     appAdded: appAdded,
677     appMoved: appMoved,
678     appRemoved: appRemoved,
679     appsPrefChangeCallback: appsPrefChangeCallback,
680     appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback,
681     enterRearrangeMode: enterRearrangeMode,
682     getAppsCallback: getAppsCallback,
683     getAppsPageIndex: getAppsPageIndex,
684     getCardSlider: getCardSlider,
685     onLoad: onLoad,
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
700   };
701 });
702
703 document.addEventListener('DOMContentLoaded', ntp.onLoad);
704
705 var toCssPx = cr.ui.toCssPx;