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.
7 * @fileoverview The local InstantExtended NTP.
12 * Controls rendering the new tab page for InstantExtended.
13 * @return {Object} A limited interface for testing the local NTP.
18 <include src="../../../../ui/webui/resources/js/util.js">
19 <include src='local_ntp_design.js'>
22 * Enum for classnames.
27 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
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.
44 * Enum for HTML element ids.
49 ATTRIBUTION: 'attribution',
50 ATTRIBUTION_TEXT: 'attribution-text',
51 CUSTOM_THEME_STYLE: 'ct-style',
53 FAKEBOX_INPUT: 'fakebox-input',
54 FAKEBOX_TEXT: 'fakebox-text',
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',
77 * Enum for the state of the NTP when it is disposed.
81 var NTP_DISPOSE_STATE = {
82 NONE: 0, // Preserve the NTP appearance and functionality
84 HIDE_FAKEBOX_AND_LOGO: 2
89 * The notification displayed when a page is blacklisted.
96 * The container for the theme 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.
111 * The container for NTP elements.
118 * The last blacklisted tile rid if any, which by definition should not be
122 var lastBlacklistedTile = null;
126 * Current number of tiles columns shown based on the window width, including
127 * those that just contain filler.
130 var numColumnsShown = 0;
134 * The browser embeddedSearch.newTabPage object.
141 * The browser embeddedSearch.searchBox 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 */
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.
183 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
187 * The color of the title in RRGGBBAA format.
190 var titleColor = null;
194 * Heuristic to determine whether a theme should be considered to be dark, so
195 * the colors of various UI elements can be adjusted.
196 * @param {ThemeBackgroundInfo|undefined} info Theme background information.
197 * @return {boolean} Whether the theme is dark.
200 function getIsThemeDark(info) {
203 // Heuristic: light text implies dark theme.
204 var rgba = info.textColorRgba;
205 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
206 return luminance >= 128;
211 * Updates the NTP based on the current theme.
214 function renderTheme() {
215 var fakeboxText = $(IDS.FAKEBOX_TEXT);
217 fakeboxText.innerHTML = '';
218 if (configData.translatedStrings.searchboxPlaceholder) {
219 fakeboxText.textContent =
220 configData.translatedStrings.searchboxPlaceholder;
224 var info = ntpApiHandle.themeBackgroundInfo;
225 var isThemeDark = getIsThemeDark(info);
226 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
228 titleColor = NTP_DESIGN.titleColor;
232 if (!info.usingDefaultTheme && info.textColorRgba) {
233 titleColor = convertToRRGGBBAAColor(info.textColorRgba);
235 titleColor = isThemeDark ?
236 NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor;
239 var background = [convertToRGBAColor(info.backgroundColorRgba),
242 info.imageHorizontalAlignment,
243 info.imageVerticalAlignment].join(' ').trim();
245 document.body.style.background = background;
246 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
247 updateThemeAttribution(info.attributionUrl);
248 setCustomThemeStyle(info);
253 * Updates the NTP based on the current theme, then rerenders all tiles.
256 function onThemeChange() {
262 * Updates the NTP style according to theme.
263 * @param {Object=} opt_themeInfo The information about the theme. If it is
264 * omitted the style will be reverted to the default.
267 function setCustomThemeStyle(opt_themeInfo) {
268 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
269 var head = document.head;
270 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
271 ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
274 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
277 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
279 '#mv-notice-links span {' +
280 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
283 ' -webkit-filter: drop-shadow(0 0 0 ' +
284 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
286 '.mv-page-ready .mv-mask {' +
287 ' border: 1px solid ' +
288 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
290 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
292 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
295 if (customStyleElement) {
296 customStyleElement.textContent = themeStyle;
298 customStyleElement = document.createElement('style');
299 customStyleElement.type = 'text/css';
300 customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
301 customStyleElement.textContent = themeStyle;
302 head.appendChild(customStyleElement);
306 ntpContents.classList.add(CLASSES.DEFAULT_THEME);
307 if (customStyleElement)
308 head.removeChild(customStyleElement);
314 * Renders the attribution if the URL is present, otherwise hides it.
315 * @param {string} url The URL of the attribution image, if any.
318 function updateThemeAttribution(url) {
320 setAttributionVisibility_(false);
324 var attributionImage = attribution.querySelector('img');
325 if (!attributionImage) {
326 attributionImage = new Image();
327 attribution.appendChild(attributionImage);
329 attributionImage.style.content = url;
330 setAttributionVisibility_(true);
335 * Sets the visibility of the theme attribution.
336 * @param {boolean} show True to show the attribution.
339 function setAttributionVisibility_(show) {
341 attribution.style.display = show ? '' : 'none';
347 * Converts an Array of color components into RRGGBBAA format.
348 * @param {Array<number>} color Array of rgba color components.
349 * @return {string} Color string in RRGGBBAA format.
352 function convertToRRGGBBAAColor(color) {
353 return color.map(function(t) {
354 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex.
360 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
361 * @param {Array<number>} color Array of rgba color components.
362 * @return {string} CSS color in RGBA format.
365 function convertToRGBAColor(color) {
366 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
367 color[3] / 255 + ')';
372 * Called when page data change.
374 function onMostVisitedChange() {
380 * Fetches new data, creates, and renders tiles.
382 function reloadTiles() {
383 var pages = ntpApiHandle.mostVisited;
384 var iframe = $('mv-single').contentWindow;
387 for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) {
388 iframe.postMessage({cmd: 'tile', rid: pages[i].rid}, '*');
390 iframe.postMessage({cmd: 'show'}, '*');
395 * Shows the blacklist notification and triggers a delay to hide it.
397 function showNotification() {
398 notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
399 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
400 notification.scrollTop;
401 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
406 * Hides the blacklist notification.
408 function hideNotification() {
409 notification.classList.add(CLASSES.HIDE_NOTIFICATION);
410 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
415 * Handles a click on the notification undo link by hiding the notification and
420 if (lastBlacklistedTile != null) {
421 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile);
427 * Handles a click on the restore all notification link by hiding the
428 * notification and informing Chrome.
430 function onRestoreAll() {
432 ntpApiHandle.undoAllMostVisitedDeletions();
437 * Recomputes the number of tile columns, and width of various contents based
438 * on the width of the window.
439 * @return {boolean} Whether the number of tile columns has changed.
441 function updateContentWidth() {
442 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
443 // If innerWidth is zero, then use the maximum snap size.
444 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
445 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
446 var innerWidth = window.innerWidth || maxSnapSize;
447 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
448 var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
449 MIN_TOTAL_HORIZONTAL_PADDING;
450 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
451 if (newNumColumns < MIN_NUM_COLUMNS)
452 newNumColumns = MIN_NUM_COLUMNS;
453 else if (newNumColumns > MAX_NUM_COLUMNS)
454 newNumColumns = MAX_NUM_COLUMNS;
456 if (numColumnsShown === newNumColumns)
459 numColumnsShown = newNumColumns;
460 var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
461 $(IDS.TILES).style.width = tilesContainerWidth + 'px';
463 // -2 to account for border.
464 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
465 fakebox.style.width = fakeboxWidth + 'px';
472 * Resizes elements because the number of tile columns may need to change in
473 * response to resizing. Also shows or hides extra tiles tiles according to the
474 * new width of the page.
476 function onResize() {
477 updateContentWidth();
482 * Handles new input by disposing the NTP, according to where the input was
485 function onInputStart() {
486 if (fakebox && isFakeboxFocused()) {
487 setFakeboxFocus(false);
488 setFakeboxDragFocus(false);
490 } else if (!isFakeboxFocused()) {
497 * Disposes the NTP, according to where the input was entered.
498 * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
500 function disposeNtp(wasFakeboxInput) {
501 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
502 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
503 setFakeboxActive(false);
504 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
505 setFakeboxAndLogoVisibility(false);
510 * Restores the NTP (re-enables the fakebox and unhides the logo.)
512 function restoreNtp() {
513 setFakeboxActive(true);
514 setFakeboxAndLogoVisibility(true);
519 * @param {boolean} focus True to focus the fakebox.
521 function setFakeboxFocus(focus) {
522 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
526 * @param {boolean} focus True to show a dragging focus to the fakebox.
528 function setFakeboxDragFocus(focus) {
529 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
533 * @return {boolean} True if the fakebox has focus.
535 function isFakeboxFocused() {
536 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
537 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
542 * @param {boolean} enable True to enable the fakebox.
544 function setFakeboxActive(enable) {
545 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
550 * @param {!Event} event The click event.
551 * @return {boolean} True if the click occurred in an enabled fakebox.
553 function isFakeboxClick(event) {
554 return fakebox.contains(event.target) &&
555 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
560 * @param {boolean} show True to show the fakebox and logo.
562 function setFakeboxAndLogoVisibility(show) {
563 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
568 * Shortcut for document.getElementById.
569 * @param {string} id of the element.
570 * @return {HTMLElement} with the id.
573 return document.getElementById(id);
578 * Utility function which creates an element with an optional classname and
579 * appends it to the specified parent.
580 * @param {Element} parent The parent to append the new element.
581 * @param {string} name The name of the new element.
582 * @param {string=} opt_class The optional classname of the new element.
583 * @return {Element} The new element.
585 function createAndAppendElement(parent, name, opt_class) {
586 var child = document.createElement(name);
588 child.classList.add(opt_class);
589 parent.appendChild(child);
595 * @param {!Element} element The element to register the handler for.
596 * @param {number} keycode The keycode of the key to register.
597 * @param {!Function} handler The key handler to register.
599 function registerKeyHandler(element, keycode, handler) {
600 element.addEventListener('keydown', function(event) {
601 if (event.keyCode == keycode)
608 * @return {Object} the handle to the embeddedSearch API.
610 function getEmbeddedSearchApiHandle() {
613 if (window.chrome && window.chrome.embeddedSearch)
614 return window.chrome.embeddedSearch;
620 * Event handler for the focus changed and blacklist messages on link elements.
621 * Used to toggle visual treatment on the tiles (depending on the message).
622 * @param {Event} event Event received.
624 function handlePostMessage(event) {
625 var cmd = event.data.cmd;
626 var args = event.data;
627 if (cmd == 'tileBlacklisted') {
629 lastBlacklistedTile = args.rid;
631 ntpApiHandle.deleteMostVisitedItem(args.rid);
637 * Prepares the New Tab Page by adding listeners, rendering the current
638 * theme, the most visited pages section, and Google-specific elements for a
639 * Google-provided page.
642 notification = $(IDS.NOTIFICATION);
643 attribution = $(IDS.ATTRIBUTION);
644 ntpContents = $(IDS.NTP_CONTENTS);
646 if (configData.isGooglePage) {
647 var logo = document.createElement('div');
649 logo.title = 'Google';
651 fakebox = document.createElement('div');
652 fakebox.id = IDS.FAKEBOX;
653 var fakeboxHtml = [];
654 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
655 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
656 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
657 fakeboxHtml.push('<div id="cursor"></div>');
658 fakebox.innerHTML = fakeboxHtml.join('');
660 ntpContents.insertBefore(fakebox, ntpContents.firstChild);
661 ntpContents.insertBefore(logo, ntpContents.firstChild);
663 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
666 // Hide notifications after fade out, so we can't focus on links via keyboard.
667 notification.addEventListener('webkitTransitionEnd', hideNotification);
669 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
670 notificationMessage.textContent =
671 configData.translatedStrings.thumbnailRemovedNotification;
673 var undoLink = $(IDS.UNDO_LINK);
674 undoLink.addEventListener('click', onUndo);
675 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
676 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
678 var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
679 restoreAllLink.addEventListener('click', onRestoreAll);
680 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
681 restoreAllLink.textContent =
682 configData.translatedStrings.restoreThumbnailsShort;
684 $(IDS.ATTRIBUTION_TEXT).textContent =
685 configData.translatedStrings.attributionIntro;
687 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
688 createAndAppendElement(
689 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
690 notificationCloseButton.addEventListener('click', hideNotification);
692 window.addEventListener('resize', onResize);
693 updateContentWidth();
695 var topLevelHandle = getEmbeddedSearchApiHandle();
697 ntpApiHandle = topLevelHandle.newTabPage;
698 ntpApiHandle.onthemechange = onThemeChange;
699 ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
701 ntpApiHandle.oninputstart = onInputStart;
702 ntpApiHandle.oninputcancel = restoreNtp;
704 if (ntpApiHandle.isInputInProgress)
709 searchboxApiHandle = topLevelHandle.searchBox;
712 // Listener for updating the key capture state.
713 document.body.onmousedown = function(event) {
714 if (isFakeboxClick(event))
715 searchboxApiHandle.startCapturingKeyStrokes();
716 else if (isFakeboxFocused())
717 searchboxApiHandle.stopCapturingKeyStrokes();
719 searchboxApiHandle.onkeycapturechange = function() {
720 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
722 var inputbox = $(IDS.FAKEBOX_INPUT);
724 inputbox.onpaste = function(event) {
725 event.preventDefault();
726 // Send pasted text to Omnibox.
727 var text = event.clipboardData.getData('text/plain');
729 searchboxApiHandle.paste(text);
731 inputbox.ondrop = function(event) {
732 event.preventDefault();
733 var text = event.dataTransfer.getData('text/plain');
735 searchboxApiHandle.paste(text);
738 inputbox.ondragenter = function() {
739 setFakeboxDragFocus(true);
741 inputbox.ondragleave = function() {
742 setFakeboxDragFocus(false);
746 // Update the fakebox style to match the current key capturing state.
747 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
750 if (searchboxApiHandle.rtl) {
751 $(IDS.NOTIFICATION).dir = 'rtl';
752 // Grabbing the root HTML element.
753 document.documentElement.setAttribute('dir', 'rtl');
754 // Add class for setting alignments based on language directionality.
755 document.documentElement.classList.add(CLASSES.RTL);
758 var iframe = document.createElement('iframe');
759 iframe.id = 'mv-single';
762 if (searchboxApiHandle.rtl) {
766 iframe.src = '//most-visited/single.html?' + args.join('&');
767 $(IDS.TILES).appendChild(iframe);
769 iframe.onload = function() {
773 window.addEventListener('message', handlePostMessage);
778 * Binds event listeners.
781 document.addEventListener('DOMContentLoaded', init);
790 if (!window.localNTPUnitTest) {