| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview New tab page |
| * This is the main code for the new tab page used by touch-enabled Chrome |
| * browsers. For now this is still a prototype. |
| */ |
| |
| // Use an anonymous function to enable strict mode just for this file (which |
| // will be concatenated with other files when embedded in Chrome |
| cr.define('ntp', function() { |
| 'use strict'; |
| |
| /** |
| * NewTabView instance. |
| * @type {!Object|undefined} |
| */ |
| var newTabView; |
| |
| /** |
| * The 'notification-container' element. |
| * @type {!Element|undefined} |
| */ |
| var notificationContainer; |
| |
| /** |
| * If non-null, an info bubble for showing messages to the user. It points at |
| * the Most Visited label, and is used to draw more attention to the |
| * navigation dot UI. |
| * @type {!Element|undefined} |
| */ |
| var promoBubble; |
| |
| /** |
| * If non-null, an bubble confirming that the user has signed into sync. It |
| * points at the login status at the top of the page. |
| * @type {!Element|undefined} |
| */ |
| var loginBubble; |
| |
| /** |
| * true if |loginBubble| should be shown. |
| * @type {boolean} |
| */ |
| var shouldShowLoginBubble = false; |
| |
| /** |
| * The 'other-sessions-menu-button' element. |
| * @type {!Element|undefined} |
| */ |
| var otherSessionsButton; |
| |
| /** |
| * The time when all sections are ready. |
| * @type {number|undefined} |
| * @private |
| */ |
| var startTime; |
| |
| /** |
| * The time in milliseconds for most transitions. This should match what's |
| * in new_tab.css. Unfortunately there's no better way to try to time |
| * something to occur until after a transition has completed. |
| * @type {number} |
| * @const |
| */ |
| var DEFAULT_TRANSITION_TIME = 500; |
| |
| /** |
| * See description for these values in ntp_stats.h. |
| * @enum {number} |
| */ |
| var NtpFollowAction = { |
| CLICKED_TILE: 11, |
| CLICKED_OTHER_NTP_PANE: 12, |
| OTHER: 13 |
| }; |
| |
| /** |
| * Creates a NewTabView object. NewTabView extends PageListView with |
| * new tab UI specific logics. |
| * @constructor |
| * @extends {PageListView} |
| */ |
| function NewTabView() { |
| var pageSwitcherStart = null; |
| var pageSwitcherEnd = null; |
| if (loadTimeData.getValue('showApps')) { |
| pageSwitcherStart = getRequiredElement('page-switcher-start'); |
| pageSwitcherEnd = getRequiredElement('page-switcher-end'); |
| } |
| this.initialize(getRequiredElement('page-list'), |
| getRequiredElement('dot-list'), |
| getRequiredElement('card-slider-frame'), |
| getRequiredElement('trash'), |
| pageSwitcherStart, pageSwitcherEnd); |
| } |
| |
| NewTabView.prototype = { |
| __proto__: ntp.PageListView.prototype, |
| |
| /** @override */ |
| appendTilePage: function(page, title, titleIsEditable, opt_refNode) { |
| ntp.PageListView.prototype.appendTilePage.apply(this, arguments); |
| |
| if (promoBubble) |
| window.setTimeout(promoBubble.reposition.bind(promoBubble), 0); |
| } |
| }; |
| |
| /** |
| * Invoked at startup once the DOM is available to initialize the app. |
| */ |
| function onLoad() { |
| sectionsToWaitFor = 0; |
| if (loadTimeData.getBoolean('showMostvisited')) |
| sectionsToWaitFor++; |
| if (loadTimeData.getBoolean('showApps')) { |
| sectionsToWaitFor++; |
| if (loadTimeData.getBoolean('showAppLauncherPromo')) { |
| $('app-launcher-promo-close-button').addEventListener('click', |
| function() { chrome.send('stopShowingAppLauncherPromo'); }); |
| $('apps-promo-learn-more').addEventListener('click', |
| function() { chrome.send('onLearnMore'); }); |
| } |
| } |
| if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled')) |
| sectionsToWaitFor++; |
| measureNavDots(); |
| |
| // Load the current theme colors. |
| themeChanged(); |
| |
| newTabView = new NewTabView(); |
| |
| notificationContainer = getRequiredElement('notification-container'); |
| notificationContainer.addEventListener( |
| 'webkitTransitionEnd', onNotificationTransitionEnd); |
| |
| if (loadTimeData.getBoolean('showRecentlyClosed')) { |
| cr.ui.decorate($('recently-closed-menu-button'), ntp.RecentMenuButton); |
| chrome.send('getRecentlyClosedTabs'); |
| } else { |
| $('recently-closed-menu-button').hidden = true; |
| } |
| |
| if (loadTimeData.getBoolean('showOtherSessionsMenu')) { |
| otherSessionsButton = getRequiredElement('other-sessions-menu-button'); |
| cr.ui.decorate(otherSessionsButton, ntp.OtherSessionsMenuButton); |
| otherSessionsButton.initialize(loadTimeData.getBoolean('isUserSignedIn')); |
| } else { |
| getRequiredElement('other-sessions-menu-button').hidden = true; |
| } |
| |
| if (loadTimeData.getBoolean('showMostvisited')) { |
| var mostVisited = new ntp.MostVisitedPage(); |
| // Move the footer into the most visited page if we are in "bare minimum" |
| // mode. |
| if (document.body.classList.contains('bare-minimum')) |
| mostVisited.appendFooter(getRequiredElement('footer')); |
| newTabView.appendTilePage(mostVisited, |
| loadTimeData.getString('mostvisited'), |
| false); |
| chrome.send('getMostVisited'); |
| } |
| |
| if (loadTimeData.getBoolean('isDiscoveryInNTPEnabled')) { |
| var suggestionsScript = document.createElement('script'); |
| suggestionsScript.src = 'suggestions_page.js'; |
| suggestionsScript.onload = function() { |
| newTabView.appendTilePage(new ntp.SuggestionsPage(), |
| loadTimeData.getString('suggestions'), |
| false, |
| (newTabView.appsPages.length > 0) ? |
| newTabView.appsPages[0] : null); |
| chrome.send('getSuggestions'); |
| cr.dispatchSimpleEvent(document, 'sectionready', true, true); |
| }; |
| document.querySelector('head').appendChild(suggestionsScript); |
| } |
| |
| if (!loadTimeData.getBoolean('showWebStoreIcon')) { |
| var webStoreIcon = $('chrome-web-store-link'); |
| // Not all versions of the NTP have a footer, so this may not exist. |
| if (webStoreIcon) |
| webStoreIcon.hidden = true; |
| } else { |
| var webStoreLink = loadTimeData.getString('webStoreLink'); |
| var url = appendParam(webStoreLink, 'utm_source', 'chrome-ntp-launcher'); |
| $('chrome-web-store-link').href = url; |
| $('chrome-web-store-link').addEventListener('click', |
| onChromeWebStoreButtonClick); |
| } |
| |
| // We need to wait for all the footer menu setup to be completed before |
| // we can compute its layout. |
| layoutFooter(); |
| |
| if (loadTimeData.getString('login_status_message')) { |
| loginBubble = new cr.ui.Bubble; |
| loginBubble.anchorNode = $('login-container'); |
| loginBubble.arrowLocation = cr.ui.ArrowLocation.TOP_END; |
| loginBubble.bubbleAlignment = |
| cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE; |
| loginBubble.deactivateToDismissDelay = 2000; |
| loginBubble.closeButtonVisible = false; |
| |
| $('login-status-advanced').onclick = function() { |
| chrome.send('showAdvancedLoginUI'); |
| }; |
| $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble); |
| |
| var bubbleContent = $('login-status-bubble-contents'); |
| loginBubble.content = bubbleContent; |
| |
| // The anchor node won't be updated until updateLogin is called so don't |
| // show the bubble yet. |
| shouldShowLoginBubble = true; |
| } |
| |
| if (loadTimeData.valueExists('bubblePromoText')) { |
| promoBubble = new cr.ui.Bubble; |
| promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor'); |
| promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START; |
| promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE; |
| promoBubble.deactivateToDismissDelay = 2000; |
| promoBubble.content = parseHtmlSubset( |
| loadTimeData.getString('bubblePromoText'), ['BR']); |
| |
| var bubbleLink = promoBubble.querySelector('a'); |
| if (bubbleLink) { |
| bubbleLink.addEventListener('click', function(e) { |
| chrome.send('bubblePromoLinkClicked'); |
| }); |
| } |
| |
| promoBubble.handleCloseEvent = function() { |
| promoBubble.hide(); |
| chrome.send('bubblePromoClosed'); |
| }; |
| promoBubble.show(); |
| chrome.send('bubblePromoViewed'); |
| } |
| |
| var loginContainer = getRequiredElement('login-container'); |
| loginContainer.addEventListener('click', showSyncLoginUI); |
| if (loadTimeData.getBoolean('shouldShowSyncLogin')) |
| chrome.send('initializeSyncLogin'); |
| |
| doWhenAllSectionsReady(function() { |
| // Tell the slider about the pages. |
| newTabView.updateSliderCards(); |
| // Mark the current page. |
| newTabView.cardSlider.currentCardValue.navigationDot.classList.add( |
| 'selected'); |
| |
| if (loadTimeData.valueExists('notificationPromoText')) { |
| var promoText = loadTimeData.getString('notificationPromoText'); |
| var tags = ['IMG']; |
| var attrs = { |
| src: function(node, value) { |
| return node.tagName == 'IMG' && |
| /^data\:image\/(?:png|gif|jpe?g)/.test(value); |
| }, |
| }; |
| |
| var promo = parseHtmlSubset(promoText, tags, attrs); |
| var promoLink = promo.querySelector('a'); |
| if (promoLink) { |
| promoLink.addEventListener('click', function(e) { |
| chrome.send('notificationPromoLinkClicked'); |
| }); |
| } |
| |
| showNotification(promo, [], function() { |
| chrome.send('notificationPromoClosed'); |
| }, 60000); |
| chrome.send('notificationPromoViewed'); |
| } |
| |
| cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true); |
| document.documentElement.classList.remove('starting-up'); |
| |
| startTime = Date.now(); |
| }); |
| |
| preventDefaultOnPoundLinkClicks(); // From webui/js/util.js. |
| cr.ui.FocusManager.disableMouseFocusOnButtons(); |
| } |
| |
| /** |
| * Launches the chrome web store app with the chrome-ntp-launcher |
| * source. |
| * @param {Event} e The click event. |
| */ |
| function onChromeWebStoreButtonClick(e) { |
| chrome.send('recordAppLaunchByURL', |
| [encodeURIComponent(this.href), |
| ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]); |
| } |
| |
| /* |
| * The number of sections to wait on. |
| * @type {number} |
| */ |
| var sectionsToWaitFor = -1; |
| |
| /** |
| * Queued callbacks which lie in wait for all sections to be ready. |
| * @type {array} |
| */ |
| var readyCallbacks = []; |
| |
| /** |
| * Fired as each section of pages becomes ready. |
| * @param {Event} e Each page's synthetic DOM event. |
| */ |
| document.addEventListener('sectionready', function(e) { |
| if (--sectionsToWaitFor <= 0) { |
| while (readyCallbacks.length) { |
| readyCallbacks.shift()(); |
| } |
| } |
| }); |
| |
| /** |
| * This is used to simulate a fire-once event (i.e. $(document).ready() in |
| * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback |
| * is fired right away. If all pages are not ready yet, the function is queued |
| * for later execution. |
| * @param {function} callback The work to be done when ready. |
| */ |
| function doWhenAllSectionsReady(callback) { |
| assert(typeof callback == 'function'); |
| if (sectionsToWaitFor > 0) |
| readyCallbacks.push(callback); |
| else |
| window.setTimeout(callback, 0); // Do soon after, but asynchronously. |
| } |
| |
| /** |
| * Fills in an invisible div with the 'Most Visited' string so that |
| * its length may be measured and the nav dots sized accordingly. |
| */ |
| function measureNavDots() { |
| var measuringDiv = $('fontMeasuringDiv'); |
| if (loadTimeData.getBoolean('showMostvisited')) |
| measuringDiv.textContent = loadTimeData.getString('mostvisited'); |
| |
| // The 4 is for border and padding. |
| var pxWidth = Math.max(measuringDiv.clientWidth * 1.15 + 4, 80); |
| |
| var styleElement = document.createElement('style'); |
| styleElement.type = 'text/css'; |
| // max-width is used because if we run out of space, the nav dots will be |
| // shrunk. |
| styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }'; |
| document.querySelector('head').appendChild(styleElement); |
| } |
| |
| /** |
| * Layout the footer so that the nav dots stay centered. |
| */ |
| function layoutFooter() { |
| var menu = $('footer-menu-container'); |
| var logo = $('logo-img'); |
| if (menu.clientWidth > logo.clientWidth) |
| logo.style.WebkitFlex = '0 1 ' + menu.clientWidth + 'px'; |
| else |
| menu.style.WebkitFlex = '0 1 ' + logo.clientWidth + 'px'; |
| } |
| |
| function themeChanged(opt_hasAttribution) { |
| $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now(); |
| |
| if (typeof opt_hasAttribution != 'undefined') { |
| document.documentElement.setAttribute('hasattribution', |
| opt_hasAttribution); |
| } |
| |
| updateAttribution(); |
| } |
| |
| function setBookmarkBarAttached(attached) { |
| document.documentElement.setAttribute('bookmarkbarattached', attached); |
| } |
| |
| /** |
| * Attributes the attribution image at the bottom left. |
| */ |
| function updateAttribution() { |
| var attribution = $('attribution'); |
| if (document.documentElement.getAttribute('hasattribution') == 'true') { |
| attribution.hidden = false; |
| } else { |
| attribution.hidden = true; |
| } |
| } |
| |
| /** |
| * Timeout ID. |
| * @type {number} |
| */ |
| var notificationTimeout = 0; |
| |
| /** |
| * Shows the notification bubble. |
| * @param {string|Node} message The notification message or node to use as |
| * message. |
| * @param {Array.<{text: string, action: function()}>} links An array of |
| * records describing the links in the notification. Each record should |
| * have a 'text' attribute (the display string) and an 'action' attribute |
| * (a function to run when the link is activated). |
| * @param {Function} opt_closeHandler The callback invoked if the user |
| * manually dismisses the notification. |
| */ |
| function showNotification(message, links, opt_closeHandler, opt_timeout) { |
| window.clearTimeout(notificationTimeout); |
| |
| var span = document.querySelector('#notification > span'); |
| if (typeof message == 'string') { |
| span.textContent = message; |
| } else { |
| span.textContent = ''; // Remove all children. |
| span.appendChild(message); |
| } |
| |
| var linksBin = $('notificationLinks'); |
| linksBin.textContent = ''; |
| for (var i = 0; i < links.length; i++) { |
| var link = linksBin.ownerDocument.createElement('div'); |
| link.textContent = links[i].text; |
| link.action = links[i].action; |
| link.onclick = function() { |
| this.action(); |
| hideNotification(); |
| }; |
| link.setAttribute('role', 'button'); |
| link.setAttribute('tabindex', 0); |
| link.className = 'link-button'; |
| linksBin.appendChild(link); |
| } |
| |
| function closeFunc(e) { |
| if (opt_closeHandler) |
| opt_closeHandler(); |
| hideNotification(); |
| } |
| |
| document.querySelector('#notification button').onclick = closeFunc; |
| document.addEventListener('dragstart', closeFunc); |
| |
| notificationContainer.hidden = false; |
| showNotificationOnCurrentPage(); |
| |
| newTabView.cardSlider.frame.addEventListener( |
| 'cardSlider:card_change_ended', onCardChangeEnded); |
| |
| var timeout = opt_timeout || 10000; |
| notificationTimeout = window.setTimeout(hideNotification, timeout); |
| } |
| |
| /** |
| * Hide the notification bubble. |
| */ |
| function hideNotification() { |
| notificationContainer.classList.add('inactive'); |
| |
| newTabView.cardSlider.frame.removeEventListener( |
| 'cardSlider:card_change_ended', onCardChangeEnded); |
| } |
| |
| /** |
| * Happens when 1 or more consecutive card changes end. |
| * @param {Event} e The cardSlider:card_change_ended event. |
| */ |
| function onCardChangeEnded(e) { |
| // If we ended on the same page as we started, ignore. |
| if (newTabView.cardSlider.currentCardValue.notification) |
| return; |
| |
| // Hide the notification the old page. |
| notificationContainer.classList.add('card-changed'); |
| |
| showNotificationOnCurrentPage(); |
| } |
| |
| /** |
| * Move and show the notification on the current page. |
| */ |
| function showNotificationOnCurrentPage() { |
| var page = newTabView.cardSlider.currentCardValue; |
| doWhenAllSectionsReady(function() { |
| if (page != newTabView.cardSlider.currentCardValue) |
| return; |
| |
| // NOTE: This moves the notification to inside of the current page. |
| page.notification = notificationContainer; |
| |
| // Reveal the notification and instruct it to hide itself if ignored. |
| notificationContainer.classList.remove('inactive'); |
| |
| // Gives the browser time to apply this rule before we remove it (causing |
| // a transition). |
| window.setTimeout(function() { |
| notificationContainer.classList.remove('card-changed'); |
| }, 0); |
| }); |
| } |
| |
| /** |
| * When done fading out, set hidden to true so the notification can't be |
| * tabbed to or clicked. |
| * @param {Event} e The webkitTransitionEnd event. |
| */ |
| function onNotificationTransitionEnd(e) { |
| if (notificationContainer.classList.contains('inactive')) |
| notificationContainer.hidden = true; |
| } |
| |
| function setRecentlyClosedTabs(dataItems) { |
| $('recently-closed-menu-button').dataItems = dataItems; |
| layoutFooter(); |
| } |
| |
| function setMostVisitedPages(data, hasBlacklistedUrls) { |
| newTabView.mostVisitedPage.data = data; |
| cr.dispatchSimpleEvent(document, 'sectionready', true, true); |
| } |
| |
| function setSuggestionsPages(data, hasBlacklistedUrls) { |
| newTabView.suggestionsPage.data = data; |
| } |
| |
| /** |
| * Set the dominant color for a node. This will be called in response to |
| * getFaviconDominantColor. The node represented by |id| better have a setter |
| * for stripeColor. |
| * @param {string} id The ID of a node. |
| * @param {string} color The color represented as a CSS string. |
| */ |
| function setFaviconDominantColor(id, color) { |
| var node = $(id); |
| if (node) |
| node.stripeColor = color; |
| } |
| |
| /** |
| * Updates the text displayed in the login container. If there is no text then |
| * the login container is hidden. |
| * @param {string} loginHeader The first line of text. |
| * @param {string} loginSubHeader The second line of text. |
| * @param {string} iconURL The url for the login status icon. If this is null |
| then the login status icon is hidden. |
| * @param {boolean} isUserSignedIn Indicates if the user is signed in or not. |
| */ |
| function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) { |
| if (loginHeader || loginSubHeader) { |
| $('login-container').hidden = false; |
| $('login-status-header').innerHTML = loginHeader; |
| $('login-status-sub-header').innerHTML = loginSubHeader; |
| $('card-slider-frame').classList.add('showing-login-area'); |
| |
| if (iconURL) { |
| $('login-status-header-container').style.backgroundImage = url(iconURL); |
| $('login-status-header-container').classList.add('login-status-icon'); |
| } else { |
| $('login-status-header-container').style.backgroundImage = 'none'; |
| $('login-status-header-container').classList.remove( |
| 'login-status-icon'); |
| } |
| } else { |
| $('login-container').hidden = true; |
| $('card-slider-frame').classList.remove('showing-login-area'); |
| } |
| if (shouldShowLoginBubble) { |
| window.setTimeout(loginBubble.show.bind(loginBubble), 0); |
| chrome.send('loginMessageSeen'); |
| shouldShowLoginBubble = false; |
| } else if (loginBubble) { |
| loginBubble.reposition(); |
| } |
| if (otherSessionsButton) { |
| otherSessionsButton.updateSignInState(isUserSignedIn); |
| layoutFooter(); |
| } |
| } |
| |
| /** |
| * Show the sync login UI. |
| * @param {Event} e The click event. |
| */ |
| function showSyncLoginUI(e) { |
| var rect = e.currentTarget.getBoundingClientRect(); |
| chrome.send('showSyncLoginUI', |
| [rect.left, rect.top, rect.width, rect.height]); |
| } |
| |
| /** |
| * Logs the time to click for the specified item. |
| * @param {string} item The item to log the time-to-click. |
| */ |
| function logTimeToClick(item) { |
| var timeToClick = Date.now() - startTime; |
| chrome.send('logTimeToClick', |
| ['NewTabPage.TimeToClick' + item, timeToClick]); |
| } |
| |
| /** |
| * Wrappers to forward the callback to corresponding PageListView member. |
| */ |
| function appAdded() { |
| return newTabView.appAdded.apply(newTabView, arguments); |
| } |
| |
| function appMoved() { |
| return newTabView.appMoved.apply(newTabView, arguments); |
| } |
| |
| function appRemoved() { |
| return newTabView.appRemoved.apply(newTabView, arguments); |
| } |
| |
| function appsPrefChangeCallback() { |
| return newTabView.appsPrefChangedCallback.apply(newTabView, arguments); |
| } |
| |
| function appLauncherPromoPrefChangeCallback() { |
| return newTabView.appLauncherPromoPrefChangeCallback.apply(newTabView, |
| arguments); |
| } |
| |
| function appsReordered() { |
| return newTabView.appsReordered.apply(newTabView, arguments); |
| } |
| |
| function enterRearrangeMode() { |
| return newTabView.enterRearrangeMode.apply(newTabView, arguments); |
| } |
| |
| function setForeignSessions(sessionList, isTabSyncEnabled) { |
| if (otherSessionsButton) { |
| otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled); |
| layoutFooter(); |
| } |
| } |
| |
| function getAppsCallback() { |
| return newTabView.getAppsCallback.apply(newTabView, arguments); |
| } |
| |
| function getAppsPageIndex() { |
| return newTabView.getAppsPageIndex.apply(newTabView, arguments); |
| } |
| |
| function getCardSlider() { |
| return newTabView.cardSlider; |
| } |
| |
| function leaveRearrangeMode() { |
| return newTabView.leaveRearrangeMode.apply(newTabView, arguments); |
| } |
| |
| function saveAppPageName() { |
| return newTabView.saveAppPageName.apply(newTabView, arguments); |
| } |
| |
| function setAppToBeHighlighted(appId) { |
| newTabView.highlightAppId = appId; |
| } |
| |
| // Return an object with all the exports |
| return { |
| appAdded: appAdded, |
| appMoved: appMoved, |
| appRemoved: appRemoved, |
| appsPrefChangeCallback: appsPrefChangeCallback, |
| appLauncherPromoPrefChangeCallback: appLauncherPromoPrefChangeCallback, |
| enterRearrangeMode: enterRearrangeMode, |
| getAppsCallback: getAppsCallback, |
| getAppsPageIndex: getAppsPageIndex, |
| getCardSlider: getCardSlider, |
| onLoad: onLoad, |
| leaveRearrangeMode: leaveRearrangeMode, |
| logTimeToClick: logTimeToClick, |
| NtpFollowAction: NtpFollowAction, |
| saveAppPageName: saveAppPageName, |
| setAppToBeHighlighted: setAppToBeHighlighted, |
| setBookmarkBarAttached: setBookmarkBarAttached, |
| setForeignSessions: setForeignSessions, |
| setMostVisitedPages: setMostVisitedPages, |
| setSuggestionsPages: setSuggestionsPages, |
| setRecentlyClosedTabs: setRecentlyClosedTabs, |
| setFaviconDominantColor: setFaviconDominantColor, |
| showNotification: showNotification, |
| themeChanged: themeChanged, |
| updateLogin: updateLogin |
| }; |
| }); |
| |
| document.addEventListener('DOMContentLoaded', ntp.onLoad); |
| |
| var toCssPx = cr.ui.toCssPx; |