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 * Measure the width of a nav dot with a given title. 345 * @param {string} id The loadTimeData ID of the desired title. 346 * @return {number} The width of the nav dot. 347 */ 348 function measureNavDot(id) { 349 var measuringDiv = $('fontMeasuringDiv'); 350 measuringDiv.textContent = loadTimeData.getString(id); 351 // The 4 is for border and padding. 352 return Math.max(measuringDiv.clientWidth * 1.15 + 4, 80); 353 } 354 355 /** 356 * Fills in an invisible div with the longest dot title string so that 357 * its length may be measured and the nav dots sized accordingly. 358 */ 359 function measureNavDots() { 360 var pxWidth = measureNavDot('appDefaultPageName'); 361 if (loadTimeData.getBoolean('showMostvisited')) 362 pxWidth = Math.max(measureNavDot('mostvisited'), pxWidth); 363 364 var styleElement = document.createElement('style'); 365 styleElement.type = 'text/css'; 366 // max-width is used because if we run out of space, the nav dots will be 367 // shrunk. 368 styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }'; 369 document.querySelector('head').appendChild(styleElement); 370 } 371 372 /** 373 * Layout the footer so that the nav dots stay centered. 374 */ 375 function layoutFooter() { 376 // We need the image to be loaded. 377 var logo = $('logo-img'); 378 var logoImg = logo.querySelector('img'); 379 if (!logoImg.complete) { 380 logoImg.onload = layoutFooter; 381 return; 382 } 383 384 var menu = $('footer-menu-container'); 385 if (menu.clientWidth > logoImg.width) 386 logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px'; 387 else 388 menu.style.WebkitFlex = '0 1 ' + logoImg.width + 'px'; 389 } 390 391 function themeChanged(opt_hasAttribution) { 392 $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now(); 393 394 if (typeof opt_hasAttribution != 'undefined') { 395 document.documentElement.setAttribute('hasattribution', 396 opt_hasAttribution); 397 } 398 399 updateAttribution(); 400 } 401 402 function setBookmarkBarAttached(attached) { 403 document.documentElement.setAttribute('bookmarkbarattached', attached); 404 } 405 406 /** 407 * Attributes the attribution image at the bottom left. 408 */ 409 function updateAttribution() { 410 var attribution = $('attribution'); 411 if (document.documentElement.getAttribute('hasattribution') == 'true') { 412 attribution.hidden = false; 413 } else { 414 attribution.hidden = true; 415 } 416 } 417 418 /** 419 * Timeout ID. 420 * @type {number} 421 */ 422 var notificationTimeout = 0; 423 424 /** 425 * Shows the notification bubble. 426 * @param {string|Node} message The notification message or node to use as 427 * message. 428 * @param {Array.<{text: string, action: function()}>} links An array of 429 * records describing the links in the notification. Each record should 430 * have a 'text' attribute (the display string) and an 'action' attribute 431 * (a function to run when the link is activated). 432 * @param {Function} opt_closeHandler The callback invoked if the user 433 * manually dismisses the notification. 434 */ 435 function showNotification(message, links, opt_closeHandler, opt_timeout) { 436 window.clearTimeout(notificationTimeout); 437 438 var span = document.querySelector('#notification > span'); 439 if (typeof message == 'string') { 440 span.textContent = message; 441 } else { 442 span.textContent = ''; // Remove all children. 443 span.appendChild(message); 444 } 445 446 var linksBin = $('notificationLinks'); 447 linksBin.textContent = ''; 448 for (var i = 0; i < links.length; i++) { 449 var link = linksBin.ownerDocument.createElement('div'); 450 link.textContent = links[i].text; 451 link.action = links[i].action; 452 link.onclick = function() { 453 this.action(); 454 hideNotification(); 455 }; 456 link.setAttribute('role', 'button'); 457 link.setAttribute('tabindex', 0); 458 link.className = 'link-button'; 459 linksBin.appendChild(link); 460 } 461 462 function closeFunc(e) { 463 if (opt_closeHandler) 464 opt_closeHandler(); 465 hideNotification(); 466 } 467 468 document.querySelector('#notification button').onclick = closeFunc; 469 document.addEventListener('dragstart', closeFunc); 470 471 notificationContainer.hidden = false; 472 showNotificationOnCurrentPage(); 473 474 newTabView.cardSlider.frame.addEventListener( 475 'cardSlider:card_change_ended', onCardChangeEnded); 476 477 var timeout = opt_timeout || 10000; 478 notificationTimeout = window.setTimeout(hideNotification, timeout); 479 } 480 481 /** 482 * Hide the notification bubble. 483 */ 484 function hideNotification() { 485 notificationContainer.classList.add('inactive'); 486 487 newTabView.cardSlider.frame.removeEventListener( 488 'cardSlider:card_change_ended', onCardChangeEnded); 489 } 490 491 /** 492 * Happens when 1 or more consecutive card changes end. 493 * @param {Event} e The cardSlider:card_change_ended event. 494 */ 495 function onCardChangeEnded(e) { 496 // If we ended on the same page as we started, ignore. 497 if (newTabView.cardSlider.currentCardValue.notification) 498 return; 499 500 // Hide the notification the old page. 501 notificationContainer.classList.add('card-changed'); 502 503 showNotificationOnCurrentPage(); 504 } 505 506 /** 507 * Move and show the notification on the current page. 508 */ 509 function showNotificationOnCurrentPage() { 510 var page = newTabView.cardSlider.currentCardValue; 511 doWhenAllSectionsReady(function() { 512 if (page != newTabView.cardSlider.currentCardValue) 513 return; 514 515 // NOTE: This moves the notification to inside of the current page. 516 page.notification = notificationContainer; 517 518 // Reveal the notification and instruct it to hide itself if ignored. 519 notificationContainer.classList.remove('inactive'); 520 521 // Gives the browser time to apply this rule before we remove it (causing 522 // a transition). 523 window.setTimeout(function() { 524 notificationContainer.classList.remove('card-changed'); 525 }, 0); 526 }); 527 } 528 529 /** 530 * When done fading out, set hidden to true so the notification can't be 531 * tabbed to or clicked. 532 * @param {Event} e The webkitTransitionEnd event. 533 */ 534 function onNotificationTransitionEnd(e) { 535 if (notificationContainer.classList.contains('inactive')) 536 notificationContainer.hidden = true; 537 } 538 539 function setRecentlyClosedTabs(dataItems) { 540 $('recently-closed-menu-button').dataItems = dataItems; 541 layoutFooter(); 542 } 543 544 function setMostVisitedPages(data, hasBlacklistedUrls) { 545 newTabView.mostVisitedPage.data = data; 546 cr.dispatchSimpleEvent(document, 'sectionready', true, true); 547 } 548 549 function setSuggestionsPages(data, hasBlacklistedUrls) { 550 newTabView.suggestionsPage.data = data; 551 } 552 553 /** 554 * Set the dominant color for a node. This will be called in response to 555 * getFaviconDominantColor. The node represented by |id| better have a setter 556 * for stripeColor. 557 * @param {string} id The ID of a node. 558 * @param {string} color The color represented as a CSS string. 559 */ 560 function setFaviconDominantColor(id, color) { 561 var node = $(id); 562 if (node) 563 node.stripeColor = color; 564 } 565 566 /** 567 * Updates the text displayed in the login container. If there is no text then 568 * the login container is hidden. 569 * @param {string} loginHeader The first line of text. 570 * @param {string} loginSubHeader The second line of text. 571 * @param {string} iconURL The url for the login status icon. If this is null 572 then the login status icon is hidden. 573 * @param {boolean} isUserSignedIn Indicates if the user is signed in or not. 574 */ 575 function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) { 576 if (loginHeader || loginSubHeader) { 577 $('login-container').hidden = false; 578 $('login-status-header').innerHTML = loginHeader; 579 $('login-status-sub-header').innerHTML = loginSubHeader; 580 $('card-slider-frame').classList.add('showing-login-area'); 581 582 if (iconURL) { 583 $('login-status-header-container').style.backgroundImage = url(iconURL); 584 $('login-status-header-container').classList.add('login-status-icon'); 585 } else { 586 $('login-status-header-container').style.backgroundImage = 'none'; 587 $('login-status-header-container').classList.remove( 588 'login-status-icon'); 589 } 590 } else { 591 $('login-container').hidden = true; 592 $('card-slider-frame').classList.remove('showing-login-area'); 593 } 594 if (shouldShowLoginBubble) { 595 window.setTimeout(loginBubble.show.bind(loginBubble), 0); 596 chrome.send('loginMessageSeen'); 597 shouldShowLoginBubble = false; 598 } else if (loginBubble) { 599 loginBubble.reposition(); 600 } 601 if (otherSessionsButton) { 602 otherSessionsButton.updateSignInState(isUserSignedIn); 603 layoutFooter(); 604 } 605 } 606 607 /** 608 * Show the sync login UI. 609 * @param {Event} e The click event. 610 */ 611 function showSyncLoginUI(e) { 612 var rect = e.currentTarget.getBoundingClientRect(); 613 chrome.send('showSyncLoginUI', 614 [rect.left, rect.top, rect.width, rect.height]); 615 } 616 617 /** 618 * Logs the time to click for the specified item. 619 * @param {string} item The item to log the time-to-click. 620 */ 621 function logTimeToClick(item) { 622 var timeToClick = Date.now() - startTime; 623 chrome.send('logTimeToClick', 624 ['NewTabPage.TimeToClick' + item, timeToClick]); 625 } 626 627 /** 628 * Wrappers to forward the callback to corresponding PageListView member. 629 */ 630 function appAdded() { 631 return newTabView.appAdded.apply(newTabView, arguments); 632 } 633 634 function appMoved() { 635 return newTabView.appMoved.apply(newTabView, arguments); 636 } 637 638 function appRemoved() { 639 return newTabView.appRemoved.apply(newTabView, arguments); 640 } 641 642 function appsPrefChangeCallback() { 643 return newTabView.appsPrefChangedCallback.apply(newTabView, arguments); 644 } 645 646 function appLauncherPromoPrefChangeCallback() { 647 return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView, 648 arguments); 649 } 650 651 function appsReordered() { 652 return newTabView.appsReordered.apply(newTabView, arguments); 653 } 654 655 function enterRearrangeMode() { 656 return newTabView.enterRearrangeMode.apply(newTabView, arguments); 657 } 658 659 function setForeignSessions(sessionList, isTabSyncEnabled) { 660 if (otherSessionsButton) { 661 otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled); 662 layoutFooter(); 663 } 664 } 665 666 function getAppsCallback() { 667 return newTabView.getAppsCallback.apply(newTabView, arguments); 668 } 669 670 function getAppsPageIndex() { 671 return newTabView.getAppsPageIndex.apply(newTabView, arguments); 672 } 673 674 function getCardSlider() { 675 return newTabView.cardSlider; 676 } 677 678 function leaveRearrangeMode() { 679 return newTabView.leaveRearrangeMode.apply(newTabView, arguments); 680 } 681 682 function saveAppPageName() { 683 return newTabView.saveAppPageName.apply(newTabView, arguments); 684 } 685 686 function setAppToBeHighlighted(appId) { 687 newTabView.highlightAppId = appId; 688 } 689 690 // Return an object with all the exports 691 return { 692 appAdded: appAdded, 693 appMoved: appMoved, 694 appRemoved: appRemoved, 695 appsPrefChangeCallback: appsPrefChangeCallback, 696 appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback, 697 enterRearrangeMode: enterRearrangeMode, 698 getAppsCallback: getAppsCallback, 699 getAppsPageIndex: getAppsPageIndex, 700 getCardSlider: getCardSlider, 701 onLoad: onLoad, 702 leaveRearrangeMode: leaveRearrangeMode, 703 logTimeToClick: logTimeToClick, 704 NtpFollowAction: NtpFollowAction, 705 saveAppPageName: saveAppPageName, 706 setAppToBeHighlighted: setAppToBeHighlighted, 707 setBookmarkBarAttached: setBookmarkBarAttached, 708 setForeignSessions: setForeignSessions, 709 setMostVisitedPages: setMostVisitedPages, 710 setSuggestionsPages: setSuggestionsPages, 711 setRecentlyClosedTabs: setRecentlyClosedTabs, 712 setFaviconDominantColor: setFaviconDominantColor, 713 showNotification: showNotification, 714 themeChanged: themeChanged, 715 updateLogin: updateLogin 716 }; 717 }); 718 719 document.addEventListener('DOMContentLoaded', ntp.onLoad); 720 721 var toCssPx = cr.ui.toCssPx; 722