Give names to all utility processes.
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / local_ntp_fast.js
blob42c646d84a5f5886e085a80e61e56845db31102a
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 /**
7 * @fileoverview The local InstantExtended NTP.
8 */
11 /**
12 * Controls rendering the new tab page for InstantExtended.
13 * @return {Object} A limited interface for testing the local NTP.
15 function LocalNTP() {
16 'use strict';
18 <include src="../../../../ui/webui/resources/js/util.js">
19 <include src="local_ntp_design.js">
21 /**
22 * Enum for classnames.
23 * @enum {string}
24 * @const
26 var CLASSES = {
27 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
28 DARK: 'dark',
29 DEFAULT_THEME: 'default-theme',
30 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
31 FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive
32 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox
33 // Applies drag focus style to the fakebox
34 FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
35 HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo',
36 HIDE_NOTIFICATION: 'mv-notice-hide',
37 // Vertically centers the most visited section for a non-Google provided page.
38 NON_GOOGLE_PAGE: 'non-google-page',
39 RTL: 'rtl' // Right-to-left language text.
43 /**
44 * Enum for HTML element ids.
45 * @enum {string}
46 * @const
48 var IDS = {
49 ATTRIBUTION: 'attribution',
50 ATTRIBUTION_TEXT: 'attribution-text',
51 CUSTOM_THEME_STYLE: 'ct-style',
52 FAKEBOX: 'fakebox',
53 FAKEBOX_INPUT: 'fakebox-input',
54 FAKEBOX_TEXT: 'fakebox-text',
55 LOGO: 'logo',
56 NOTIFICATION: 'mv-notice',
57 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x',
58 NOTIFICATION_MESSAGE: 'mv-msg',
59 NTP_CONTENTS: 'ntp-contents',
60 RESTORE_ALL_LINK: 'mv-restore',
61 TILES: 'mv-tiles',
62 UNDO_LINK: 'mv-undo'
66 /**
67 * Enum for keycodes.
68 * @enum {number}
69 * @const
71 var KEYCODE = {
72 ENTER: 13
76 /**
77 * Enum for the state of the NTP when it is disposed.
78 * @enum {number}
79 * @const
81 var NTP_DISPOSE_STATE = {
82 NONE: 0, // Preserve the NTP appearance and functionality
83 DISABLE_FAKEBOX: 1,
84 HIDE_FAKEBOX_AND_LOGO: 2
88 /**
89 * The notification displayed when a page is blacklisted.
90 * @type {Element}
92 var notification;
95 /**
96 * The container for the theme attribution.
97 * @type {Element}
99 var attribution;
103 * The "fakebox" - an input field that looks like a regular searchbox. When it
104 * is focused, any text the user types goes directly into the omnibox.
105 * @type {Element}
107 var fakebox;
111 * The container for NTP elements.
112 * @type {Element}
114 var ntpContents;
118 * The last blacklisted tile rid if any, which by definition should not be
119 * filler.
120 * @type {?number}
122 var lastBlacklistedTile = null;
126 * Current number of tiles columns shown based on the window width, including
127 * those that just contain filler.
128 * @type {number}
130 var numColumnsShown = 0;
134 * The browser embeddedSearch.newTabPage object.
135 * @type {Object}
137 var ntpApiHandle;
141 * The browser embeddedSearch.searchBox object.
142 * @type {Object}
144 var searchboxApiHandle;
148 * The state of the NTP when a query is entered into the Omnibox.
149 * @type {NTP_DISPOSE_STATE}
151 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
155 * The state of the NTP when a query is entered into the Fakebox.
156 * @type {NTP_DISPOSE_STATE}
158 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
161 /** @type {number} @const */
162 var MAX_NUM_TILES_TO_SHOW = 8;
165 /** @type {number} @const */
166 var MIN_NUM_COLUMNS = 2;
169 /** @type {number} @const */
170 var MAX_NUM_COLUMNS = 4;
173 /** @type {number} @const */
174 var NUM_ROWS = 2;
178 * Minimum total padding to give to the left and right of the most visited
179 * section. Used to determine how many tiles to show.
180 * @type {number}
181 * @const
183 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
187 * Heuristic to determine whether a theme should be considered to be dark, so
188 * the colors of various UI elements can be adjusted.
189 * @param {ThemeBackgroundInfo|undefined} info Theme background information.
190 * @return {boolean} Whether the theme is dark.
191 * @private
193 function getIsThemeDark(info) {
194 if (!info)
195 return false;
196 // Heuristic: light text implies dark theme.
197 var rgba = info.textColorRgba;
198 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
199 return luminance >= 128;
204 * Updates the NTP based on the current theme.
205 * @private
207 function renderTheme() {
208 var fakeboxText = $(IDS.FAKEBOX_TEXT);
209 if (fakeboxText) {
210 fakeboxText.innerHTML = '';
211 if (configData.translatedStrings.searchboxPlaceholder) {
212 fakeboxText.textContent =
213 configData.translatedStrings.searchboxPlaceholder;
217 var info = ntpApiHandle.themeBackgroundInfo;
218 var isThemeDark = getIsThemeDark(info);
219 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
220 if (!info) {
221 return;
224 var background = [convertToRGBAColor(info.backgroundColorRgba),
225 info.imageUrl,
226 info.imageTiling,
227 info.imageHorizontalAlignment,
228 info.imageVerticalAlignment].join(' ').trim();
230 document.body.style.background = background;
231 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
232 updateThemeAttribution(info.attributionUrl);
233 setCustomThemeStyle(info);
235 var themeinfo = {cmd: 'updateTheme'};
236 if (!info.usingDefaultTheme) {
237 themeinfo.tileBorderColor = convertToRGBAColor(info.sectionBorderColorRgba);
238 themeinfo.tileHoverBorderColor = convertToRGBAColor(info.headerColorRgba);
240 themeinfo.isThemeDark = isThemeDark;
242 var titleColor = NTP_DESIGN.titleColor;
243 if (!info.usingDefaultTheme && info.textColorRgba) {
244 titleColor = info.textColorRgba;
245 } else if (isThemeDark) {
246 titleColor = NTP_DESIGN.titleColorAgainstDark;
248 themeinfo.tileTitleColor = convertToRGBAColor(titleColor);
250 $('mv-single').contentWindow.postMessage(themeinfo, '*');
255 * Updates the NTP based on the current theme, then rerenders all tiles.
256 * @private
258 function onThemeChange() {
259 renderTheme();
264 * Updates the NTP style according to theme.
265 * @param {Object=} opt_themeInfo The information about the theme. If it is
266 * omitted the style will be reverted to the default.
267 * @private
269 function setCustomThemeStyle(opt_themeInfo) {
270 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
271 var head = document.head;
272 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
273 ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
274 var themeStyle =
275 '#attribution {' +
276 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
277 '}' +
278 '#mv-msg {' +
279 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
280 '}' +
281 '#mv-notice-links span {' +
282 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
283 '}' +
284 '#mv-notice-x {' +
285 ' -webkit-filter: drop-shadow(0 0 0 ' +
286 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
287 '}' +
288 '.mv-page-ready .mv-mask {' +
289 ' border: 1px solid ' +
290 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
291 '}' +
292 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
293 ' border-color: ' +
294 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
295 '}';
297 if (customStyleElement) {
298 customStyleElement.textContent = themeStyle;
299 } else {
300 customStyleElement = document.createElement('style');
301 customStyleElement.type = 'text/css';
302 customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
303 customStyleElement.textContent = themeStyle;
304 head.appendChild(customStyleElement);
307 } else {
308 ntpContents.classList.add(CLASSES.DEFAULT_THEME);
309 if (customStyleElement)
310 head.removeChild(customStyleElement);
316 * Renders the attribution if the URL is present, otherwise hides it.
317 * @param {string} url The URL of the attribution image, if any.
318 * @private
320 function updateThemeAttribution(url) {
321 if (!url) {
322 setAttributionVisibility_(false);
323 return;
326 var attributionImage = attribution.querySelector('img');
327 if (!attributionImage) {
328 attributionImage = new Image();
329 attribution.appendChild(attributionImage);
331 attributionImage.style.content = url;
332 setAttributionVisibility_(true);
337 * Sets the visibility of the theme attribution.
338 * @param {boolean} show True to show the attribution.
339 * @private
341 function setAttributionVisibility_(show) {
342 if (attribution) {
343 attribution.style.display = show ? '' : 'none';
349 * Converts an Array of color components into RRGGBBAA format.
350 * @param {Array<number>} color Array of rgba color components.
351 * @return {string} Color string in RRGGBBAA format.
352 * @private
354 function convertToRRGGBBAAColor(color) {
355 return color.map(function(t) {
356 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex.
357 }).join('');
362 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
363 * @param {Array<number>} color Array of rgba color components.
364 * @return {string} CSS color in RGBA format.
365 * @private
367 function convertToRGBAColor(color) {
368 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
369 color[3] / 255 + ')';
374 * Called when page data change.
376 function onMostVisitedChange() {
377 reloadTiles();
382 * Fetches new data, creates, and renders tiles.
384 function reloadTiles() {
385 var pages = ntpApiHandle.mostVisited;
386 var cmds = [];
387 for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) {
388 cmds.push({cmd: 'tile', rid: pages[i].rid});
390 cmds.push({cmd: 'show', maxVisible: numColumnsShown * NUM_ROWS});
392 $('mv-single').contentWindow.postMessage(cmds, '*');
397 * Shows the blacklist notification and triggers a delay to hide it.
399 function showNotification() {
400 notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
401 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
402 notification.scrollTop;
403 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
408 * Hides the blacklist notification.
410 function hideNotification() {
411 notification.classList.add(CLASSES.HIDE_NOTIFICATION);
412 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
417 * Handles a click on the notification undo link by hiding the notification and
418 * informing Chrome.
420 function onUndo() {
421 hideNotification();
422 if (lastBlacklistedTile != null) {
423 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile);
429 * Handles a click on the restore all notification link by hiding the
430 * notification and informing Chrome.
432 function onRestoreAll() {
433 hideNotification();
434 ntpApiHandle.undoAllMostVisitedDeletions();
439 * Recomputes the number of tile columns, and width of various contents based
440 * on the width of the window.
441 * @return {boolean} Whether the number of tile columns has changed.
443 function updateContentWidth() {
444 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
445 // If innerWidth is zero, then use the maximum snap size.
446 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
447 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
448 var innerWidth = window.innerWidth || maxSnapSize;
449 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
450 var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
451 NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING;
452 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
453 if (newNumColumns < MIN_NUM_COLUMNS)
454 newNumColumns = MIN_NUM_COLUMNS;
455 else if (newNumColumns > MAX_NUM_COLUMNS)
456 newNumColumns = MAX_NUM_COLUMNS;
458 if (numColumnsShown === newNumColumns)
459 return false;
461 numColumnsShown = newNumColumns;
462 var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
463 $(IDS.TILES).style.width = tilesContainerWidth + 'px';
464 if (fakebox) {
465 // -2 to account for border.
466 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
467 fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2;
468 fakebox.style.width = fakeboxWidth + 'px';
470 return true;
475 * Resizes elements because the number of tile columns may need to change in
476 * response to resizing. Also shows or hides extra tiles tiles according to the
477 * new width of the page.
479 function onResize() {
480 updateContentWidth();
481 $('mv-single').contentWindow.postMessage(
482 {cmd: 'tilesVisible', maxVisible: numColumnsShown * NUM_ROWS}, '*');
487 * Handles new input by disposing the NTP, according to where the input was
488 * entered.
490 function onInputStart() {
491 if (fakebox && isFakeboxFocused()) {
492 setFakeboxFocus(false);
493 setFakeboxDragFocus(false);
494 disposeNtp(true);
495 } else if (!isFakeboxFocused()) {
496 disposeNtp(false);
502 * Disposes the NTP, according to where the input was entered.
503 * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
505 function disposeNtp(wasFakeboxInput) {
506 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
507 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
508 setFakeboxActive(false);
509 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
510 setFakeboxAndLogoVisibility(false);
515 * Restores the NTP (re-enables the fakebox and unhides the logo.)
517 function restoreNtp() {
518 setFakeboxActive(true);
519 setFakeboxAndLogoVisibility(true);
524 * @param {boolean} focus True to focus the fakebox.
526 function setFakeboxFocus(focus) {
527 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
531 * @param {boolean} focus True to show a dragging focus to the fakebox.
533 function setFakeboxDragFocus(focus) {
534 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
538 * @return {boolean} True if the fakebox has focus.
540 function isFakeboxFocused() {
541 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
542 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
547 * @param {boolean} enable True to enable the fakebox.
549 function setFakeboxActive(enable) {
550 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
555 * @param {!Event} event The click event.
556 * @return {boolean} True if the click occurred in an enabled fakebox.
558 function isFakeboxClick(event) {
559 return fakebox.contains(event.target) &&
560 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
565 * @param {boolean} show True to show the fakebox and logo.
567 function setFakeboxAndLogoVisibility(show) {
568 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
573 * Shortcut for document.getElementById.
574 * @param {string} id of the element.
575 * @return {HTMLElement} with the id.
577 function $(id) {
578 return document.getElementById(id);
583 * Utility function which creates an element with an optional classname and
584 * appends it to the specified parent.
585 * @param {Element} parent The parent to append the new element.
586 * @param {string} name The name of the new element.
587 * @param {string=} opt_class The optional classname of the new element.
588 * @return {Element} The new element.
590 function createAndAppendElement(parent, name, opt_class) {
591 var child = document.createElement(name);
592 if (opt_class)
593 child.classList.add(opt_class);
594 parent.appendChild(child);
595 return child;
600 * @param {!Element} element The element to register the handler for.
601 * @param {number} keycode The keycode of the key to register.
602 * @param {!Function} handler The key handler to register.
604 function registerKeyHandler(element, keycode, handler) {
605 element.addEventListener('keydown', function(event) {
606 if (event.keyCode == keycode)
607 handler(event);
613 * @return {Object} the handle to the embeddedSearch API.
615 function getEmbeddedSearchApiHandle() {
616 if (window.cideb)
617 return window.cideb;
618 if (window.chrome && window.chrome.embeddedSearch)
619 return window.chrome.embeddedSearch;
620 return null;
625 * Event handler for the focus changed and blacklist messages on link elements.
626 * Used to toggle visual treatment on the tiles (depending on the message).
627 * @param {Event} event Event received.
629 function handlePostMessage(event) {
630 var cmd = event.data.cmd;
631 var args = event.data;
632 if (cmd == 'tileBlacklisted') {
633 showNotification();
634 lastBlacklistedTile = args.tid;
636 ntpApiHandle.deleteMostVisitedItem(args.tid);
642 * Prepares the New Tab Page by adding listeners, rendering the current
643 * theme, the most visited pages section, and Google-specific elements for a
644 * Google-provided page.
646 function init() {
647 notification = $(IDS.NOTIFICATION);
648 attribution = $(IDS.ATTRIBUTION);
649 ntpContents = $(IDS.NTP_CONTENTS);
651 if (configData.isGooglePage) {
652 var logo = document.createElement('div');
653 logo.id = IDS.LOGO;
654 logo.title = 'Google';
656 fakebox = document.createElement('div');
657 fakebox.id = IDS.FAKEBOX;
658 var fakeboxHtml = [];
659 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
660 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
661 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
662 fakeboxHtml.push('<div id="cursor"></div>');
663 fakebox.innerHTML = fakeboxHtml.join('');
665 ntpContents.insertBefore(fakebox, ntpContents.firstChild);
666 ntpContents.insertBefore(logo, ntpContents.firstChild);
667 } else {
668 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
671 // Modify design for experimental icon NTP, if specified.
672 if (configData.useIcons)
673 modifyNtpDesignForIcons();
674 document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass);
676 // Hide notifications after fade out, so we can't focus on links via keyboard.
677 notification.addEventListener('webkitTransitionEnd', hideNotification);
679 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
680 notificationMessage.textContent =
681 configData.translatedStrings.thumbnailRemovedNotification;
683 var undoLink = $(IDS.UNDO_LINK);
684 undoLink.addEventListener('click', onUndo);
685 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
686 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
688 var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
689 restoreAllLink.addEventListener('click', onRestoreAll);
690 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
691 restoreAllLink.textContent =
692 configData.translatedStrings.restoreThumbnailsShort;
694 $(IDS.ATTRIBUTION_TEXT).textContent =
695 configData.translatedStrings.attributionIntro;
697 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
698 createAndAppendElement(
699 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
700 notificationCloseButton.addEventListener('click', hideNotification);
702 window.addEventListener('resize', onResize);
703 updateContentWidth();
705 var topLevelHandle = getEmbeddedSearchApiHandle();
707 ntpApiHandle = topLevelHandle.newTabPage;
708 ntpApiHandle.onthemechange = onThemeChange;
709 ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
711 ntpApiHandle.oninputstart = onInputStart;
712 ntpApiHandle.oninputcancel = restoreNtp;
714 if (ntpApiHandle.isInputInProgress)
715 onInputStart();
717 searchboxApiHandle = topLevelHandle.searchBox;
719 if (fakebox) {
720 // Listener for updating the key capture state.
721 document.body.onmousedown = function(event) {
722 if (isFakeboxClick(event))
723 searchboxApiHandle.startCapturingKeyStrokes();
724 else if (isFakeboxFocused())
725 searchboxApiHandle.stopCapturingKeyStrokes();
727 searchboxApiHandle.onkeycapturechange = function() {
728 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
730 var inputbox = $(IDS.FAKEBOX_INPUT);
731 if (inputbox) {
732 inputbox.onpaste = function(event) {
733 event.preventDefault();
734 // Send pasted text to Omnibox.
735 var text = event.clipboardData.getData('text/plain');
736 if (text)
737 searchboxApiHandle.paste(text);
739 inputbox.ondrop = function(event) {
740 event.preventDefault();
741 var text = event.dataTransfer.getData('text/plain');
742 if (text) {
743 searchboxApiHandle.paste(text);
745 setFakeboxDragFocus(false);
747 inputbox.ondragenter = function() {
748 setFakeboxDragFocus(true);
750 inputbox.ondragleave = function() {
751 setFakeboxDragFocus(false);
755 // Update the fakebox style to match the current key capturing state.
756 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
759 if (searchboxApiHandle.rtl) {
760 $(IDS.NOTIFICATION).dir = 'rtl';
761 // Grabbing the root HTML element.
762 document.documentElement.setAttribute('dir', 'rtl');
763 // Add class for setting alignments based on language directionality.
764 document.documentElement.classList.add(CLASSES.RTL);
767 var iframe = document.createElement('iframe');
768 iframe.id = 'mv-single';
769 var args = [];
771 if (searchboxApiHandle.rtl)
772 args.push('rtl=1');
773 if (window.configData.useIcons)
774 args.push('icons=1');
775 if (NTP_DESIGN.numTitleLines > 1)
776 args.push('ntl=' + NTP_DESIGN.numTitleLines);
778 args.push('removeTooltip=' +
779 encodeURIComponent(configData.translatedStrings.removeThumbnailTooltip));
781 iframe.src = '//most-visited/single.html?' + args.join('&');
782 $(IDS.TILES).appendChild(iframe);
784 iframe.onload = function() {
785 reloadTiles();
786 renderTheme();
789 window.addEventListener('message', handlePostMessage);
794 * Binds event listeners.
796 function listen() {
797 document.addEventListener('DOMContentLoaded', init);
800 return {
801 init: init,
802 listen: listen
806 if (!window.localNTPUnitTest) {
807 LocalNTP().listen();