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 13cr.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 703document.addEventListener('DOMContentLoaded', ntp.onLoad); 704 705var toCssPx = cr.ui.toCssPx; 706