blob: da4de3450ddb96c263ce3a6b7252d75c98e813fa [file] [log] [blame]
// Copyright 2015 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 The local InstantExtended NTP.
*/
/**
* Whether the most visited tiles have finished loading, i.e. we've received the
* 'loaded' postMessage from the iframe. Used by tests to detect that loading
* has completed.
* @type {boolean}
*/
var tilesAreLoaded = false;
var numDdllogResponsesReceived = 0;
var lastDdllogResponse = '';
var onDdllogResponse = null;
/**
* Controls rendering the new tab page for InstantExtended.
* @return {Object} A limited interface for testing the local NTP.
*/
function LocalNTP() {
'use strict';
/**
* Alias for document.getElementById.
* @param {string} id The ID of the element to find.
* @return {HTMLElement} The found element or null if not found.
*/
function $(id) {
// eslint-disable-next-line no-restricted-properties
return document.getElementById(id);
}
/**
* Specifications for an NTP design (not comprehensive).
*
* numTitleLines: Number of lines to display in titles.
* titleColor: The 4-component color of title text.
* titleColorAgainstDark: The 4-component color of title text against a dark
* theme.
*
* @type {{
* numTitleLines: number,
* titleColor: string,
* titleColorAgainstDark: string,
* }}
*/
var NTP_DESIGN = {
numTitleLines: 1,
titleColor: [50, 50, 50, 255],
titleColorAgainstDark: [210, 210, 210, 255],
};
/**
* Enum for classnames.
* @enum {string}
* @const
*/
var CLASSES = {
ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
DARK: 'dark',
DEFAULT_THEME: 'default-theme',
DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
FADE: 'fade', // Enables opacity transition on logo and doodle.
FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox
SHOW_EDIT_DIALOG: 'show', // Displays the edit custom link dialog.
HIDE_BODY_OVERFLOW: 'hidden', // Prevents scrolling while the edit custom
// link dialog is open.
// Applies float animations to the Most Visited notification
FLOAT_UP: 'float-up',
// Applies ripple animation to the element on click
RIPPLE: 'ripple',
RIPPLE_CONTAINER: 'ripple-container',
RIPPLE_EFFECT: 'ripple-effect',
// Applies drag focus style to the fakebox
FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
HIDE_FAKEBOX: 'hide-fakebox',
HIDE_NOTIFICATION: 'mv-notice-hide',
INITED: 'inited', // Reveals the <body> once init() is done.
LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution',
MATERIAL_DESIGN: 'md', // Applies Material Design styles to the page
MATERIAL_DESIGN_ICONS:
'md-icons', // Applies Material Design styles to Most Visited.
// Vertically centers the most visited section for a non-Google provided page.
NON_GOOGLE_PAGE: 'non-google-page',
NON_WHITE_BG: 'non-white-bg',
RTL: 'rtl', // Right-to-left language text.
SHOW_LOGO: 'show-logo', // Marks logo/doodle that should be shown.
};
/**
* Enum for HTML element ids.
* @enum {string}
* @const
*/
var IDS = {
ATTRIBUTION: 'attribution',
ATTRIBUTION_TEXT: 'attribution-text',
CUSTOM_LINKS_EDIT_IFRAME: 'custom-links-edit',
FAKEBOX: 'fakebox',
FAKEBOX_INPUT: 'fakebox-input',
FAKEBOX_TEXT: 'fakebox-text',
FAKEBOX_MICROPHONE: 'fakebox-microphone',
LOGO: 'logo',
LOGO_DEFAULT: 'logo-default',
LOGO_DOODLE: 'logo-doodle',
LOGO_DOODLE_IMAGE: 'logo-doodle-image',
LOGO_DOODLE_IFRAME: 'logo-doodle-iframe',
LOGO_DOODLE_BUTTON: 'logo-doodle-button',
LOGO_DOODLE_NOTIFIER: 'logo-doodle-notifier',
MOST_VISITED: 'most-visited',
NOTIFICATION: 'mv-notice',
NOTIFICATION_CONTAINER: 'mv-notice-container',
NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x',
NOTIFICATION_MESSAGE: 'mv-msg',
NTP_CONTENTS: 'ntp-contents',
RESTORE_ALL_LINK: 'mv-restore',
TILES: 'mv-tiles',
TILES_IFRAME: 'mv-single',
UNDO_LINK: 'mv-undo',
};
/**
* Counterpart of search_provider_logos::LogoType.
* @enum {string}
* @const
*/
var LOGO_TYPE = {
SIMPLE: 'SIMPLE',
ANIMATED: 'ANIMATED',
INTERACTIVE: 'INTERACTIVE',
};
/**
* The different types of events that are logged from the NTP. This enum is
* used to transfer information from the NTP JavaScript to the renderer and is
* not used as a UMA enum histogram's logged value.
* Note: Keep in sync with common/ntp_logging_events.h
* @enum {number}
* @const
*/
var LOG_TYPE = {
// A static Doodle was shown, coming from cache.
NTP_STATIC_LOGO_SHOWN_FROM_CACHE: 30,
// A static Doodle was shown, coming from the network.
NTP_STATIC_LOGO_SHOWN_FRESH: 31,
// A call-to-action Doodle image was shown, coming from cache.
NTP_CTA_LOGO_SHOWN_FROM_CACHE: 32,
// A call-to-action Doodle image was shown, coming from the network.
NTP_CTA_LOGO_SHOWN_FRESH: 33,
// A static Doodle was clicked.
NTP_STATIC_LOGO_CLICKED: 34,
// A call-to-action Doodle was clicked.
NTP_CTA_LOGO_CLICKED: 35,
// An animated Doodle was clicked.
NTP_ANIMATED_LOGO_CLICKED: 36,
// The One Google Bar was shown.
NTP_ONE_GOOGLE_BAR_SHOWN: 37,
};
/**
* The maximum number of tiles to show in the Most Visited section.
* @type {number}
* @const
*/
const MAX_NUM_TILES_MOST_VISITED = 8;
/**
* The maximum number of tiles to show in the Most Visited section if custom
* links is enabled.
* @type {number}
* @const
*/
const MAX_NUM_TILES_CUSTOM_LINKS = 10;
/**
* Background colors considered "white". Used to determine if it is possible
* to display a Google Doodle, or if the notifier should be used instead.
* @type {Array<string>}
* @const
*/
var WHITE_BACKGROUND_COLORS = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)'];
/**
* Enum for keycodes.
* @enum {number}
* @const
*/
var KEYCODE = {ENTER: 13, SPACE: 32};
/**
* The period of time (ms) before the Most Visited notification is hidden.
* @type {number}
*/
const NOTIFICATION_TIMEOUT = 10000;
/**
* The duration of the ripple animation.
* @type {number}
*/
const RIPPLE_DURATION_MS = 800;
/**
* The max size of the ripple animation.
* @type {number}
*/
const RIPPLE_MAX_RADIUS_PX = 300;
/**
* The last blacklisted tile rid if any, which by definition should not be
* filler.
* @type {?number}
*/
var lastBlacklistedTile = null;
/**
* The timeout id for automatically hiding the Most Visited notification.
* Invalid ids will silently do nothing.
* @type {number}
*/
let delayedHideNotification = -1;
/**
* The browser embeddedSearch.newTabPage object.
* @type {Object}
*/
var ntpApiHandle;
/**
* Returns theme background info, first checking for history.state.notheme. If
* the page has notheme set, returns a fallback light-colored theme.
*/
function getThemeBackgroundInfo() {
if (history.state && history.state.notheme) {
return {
alternateLogo: false,
backgroundColorRgba: [255, 255, 255, 255],
colorRgba: [255, 255, 255, 255],
headerColorRgba: [150, 150, 150, 255],
linkColorRgba: [6, 55, 116, 255],
sectionBorderColorRgba: [150, 150, 150, 255],
textColorLightRgba: [102, 102, 102, 255],
textColorRgba: [0, 0, 0, 255],
usingDefaultTheme: true,
};
}
return ntpApiHandle.themeBackgroundInfo;
}
/**
* Heuristic to determine whether a theme should be considered to be dark, so
* the colors of various UI elements can be adjusted.
* @param {ThemeBackgroundInfo|undefined} info Theme background information.
* @return {boolean} Whether the theme is dark.
* @private
*/
function getIsThemeDark() {
var info = getThemeBackgroundInfo();
if (!info)
return false;
// Heuristic: light text implies dark theme.
var rgba = info.textColorRgba;
var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
return luminance >= 128;
}
/**
* Updates the NTP based on the current theme.
* @private
*/
function renderTheme() {
$(IDS.NTP_CONTENTS).classList.toggle(CLASSES.DARK, getIsThemeDark());
var info = getThemeBackgroundInfo();
if (!info)
return;
var background = [convertToRGBAColor(info.backgroundColorRgba),
info.imageUrl,
info.imageTiling,
info.imageHorizontalAlignment,
info.imageVerticalAlignment].join(' ').trim();
document.body.style.background = background;
document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
var isNonWhiteBackground = !WHITE_BACKGROUND_COLORS.includes(background);
document.body.classList.toggle(CLASSES.NON_WHITE_BG, isNonWhiteBackground);
updateThemeAttribution(info.attributionUrl, info.imageHorizontalAlignment);
setCustomThemeStyle(info);
if (info.customBackgroundConfigured) {
var imageWithOverlay = [
customBackgrounds.CUSTOM_BACKGROUND_OVERLAY, info.imageUrl
].join(',').trim();
if (imageWithOverlay != document.body.style.backgroundImage) {
customBackgrounds.closeCustomizationDialog();
customBackgrounds.clearAttribution();
}
document.body.style.setProperty('background-image', imageWithOverlay);
customBackgrounds.setAttribution(
info.attribution1, info.attribution2, info.attributionActionUrl);
}
$(customBackgrounds.IDS.RESTORE_DEFAULT)
.classList.toggle(
customBackgrounds.CLASSES.OPTION_DISABLED,
!info.customBackgroundConfigured);
if (configData.isGooglePage) {
// Hide the settings menu if the user has a theme. Do not hide if custom
// links is enabled.
$('edit-bg').hidden =
(!configData.isCustomBackgroundsEnabled || !info.usingDefaultTheme) &&
!configData.isCustomLinksEnabled;
}
}
/**
* Sends the current theme info to the most visited iframe.
* @private
*/
function sendThemeInfoToMostVisitedIframe() {
var info = getThemeBackgroundInfo();
if (!info)
return;
var isThemeDark = getIsThemeDark();
var message = {cmd: 'updateTheme'};
message.isThemeDark = isThemeDark;
message.isUsingTheme =
!info.customBackgroundConfigured && !info.usingDefaultTheme;
var titleColor = NTP_DESIGN.titleColor;
if (!info.usingDefaultTheme && info.textColorRgba) {
titleColor = info.textColorRgba;
} else if (isThemeDark) {
titleColor = NTP_DESIGN.titleColorAgainstDark;
}
message.tileTitleColor = convertToRGBAColor(titleColor);
$(IDS.TILES_IFRAME).contentWindow.postMessage(message, '*');
}
/**
* Updates the OneGoogleBar (if it is loaded) based on the current theme.
* @private
*/
function renderOneGoogleBarTheme() {
if (!window.gbar) {
return;
}
try {
var oneGoogleBarApi = window.gbar.a;
var oneGoogleBarPromise = oneGoogleBarApi.bf();
oneGoogleBarPromise.then(function(oneGoogleBar) {
var isThemeDark = getIsThemeDark();
var setForegroundStyle = oneGoogleBar.pc.bind(oneGoogleBar);
setForegroundStyle(isThemeDark ? 1 : 0);
});
} catch (err) {
console.log('Failed setting OneGoogleBar theme:\n' + err);
}
}
/**
* Callback for embeddedSearch.newTabPage.onthemechange.
* @private
*/
function onThemeChange() {
renderTheme();
renderOneGoogleBarTheme();
sendThemeInfoToMostVisitedIframe();
}
/**
* Updates the NTP style according to theme.
* @param {Object} themeInfo The information about the theme.
* @private
*/
function setCustomThemeStyle(themeInfo) {
var textColor = null;
var textColorLight = null;
var mvxFilter = null;
if (!themeInfo.usingDefaultTheme) {
textColor = convertToRGBAColor(themeInfo.textColorRgba);
textColorLight = convertToRGBAColor(themeInfo.textColorLightRgba);
mvxFilter = 'drop-shadow(0 0 0 ' + textColor + ')';
}
$(IDS.NTP_CONTENTS)
.classList.toggle(CLASSES.DEFAULT_THEME, themeInfo.usingDefaultTheme);
document.body.style.setProperty('--text-color', textColor);
document.body.style.setProperty('--text-color-light', textColorLight);
// Themes reuse the "light" text color for links too.
document.body.style.setProperty('--text-color-link', textColorLight);
$(IDS.NOTIFICATION_CLOSE_BUTTON)
.style.setProperty('--theme-filter', mvxFilter);
}
/**
* Renders the attribution if the URL is present, otherwise hides it.
* @param {string} url The URL of the attribution image, if any.
* @param {string} themeBackgroundAlignment The alignment of the theme
* background image. This is used to compute the attribution's alignment.
* @private
*/
function updateThemeAttribution(url, themeBackgroundAlignment) {
if (!url) {
setAttributionVisibility_(false);
return;
}
var attribution = $(IDS.ATTRIBUTION);
var attributionImage = attribution.querySelector('img');
if (!attributionImage) {
attributionImage = new Image();
attribution.appendChild(attributionImage);
}
attributionImage.style.content = url;
// To avoid conflicts, place the attribution on the left for themes that
// right align their background images.
attribution.classList.toggle(CLASSES.LEFT_ALIGN_ATTRIBUTION,
themeBackgroundAlignment == 'right');
setAttributionVisibility_(true);
}
/**
* Sets the visibility of the theme attribution.
* @param {boolean} show True to show the attribution.
* @private
*/
function setAttributionVisibility_(show) {
$(IDS.ATTRIBUTION).style.display = show ? '' : 'none';
}
/**
* Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
* @param {Array<number>} color Array of rgba color components.
* @return {string} CSS color in RGBA format.
* @private
*/
function convertToRGBAColor(color) {
return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
color[3] / 255 + ')';
}
/**
* Callback for embeddedSearch.newTabPage.onmostvisitedchange. Called when the
* NTP tiles are updated.
*/
function onMostVisitedChange() {
reloadTiles();
}
/**
* Fetches new data (RIDs) from the embeddedSearch.newTabPage API and passes
* them to the iframe.
*/
function reloadTiles() {
// Don't attempt to load tiles if the MV data isn't available yet - this can
// happen occasionally, see https://crbug.com/794942. In that case, we should
// get an onMostVisitedChange call once they are available.
// Note that MV data being available is different from having > 0 tiles. There
// can legitimately be 0 tiles, e.g. if the user blacklisted them all.
if (!ntpApiHandle.mostVisitedAvailable) {
return;
}
var pages = ntpApiHandle.mostVisited;
var cmds = [];
let maxNumTiles = configData.isCustomLinksEnabled ?
MAX_NUM_TILES_CUSTOM_LINKS :
MAX_NUM_TILES_MOST_VISITED;
for (var i = 0; i < Math.min(maxNumTiles, pages.length); ++i) {
cmds.push({cmd: 'tile', rid: pages[i].rid});
}
cmds.push({cmd: 'show'});
$(IDS.TILES_IFRAME).contentWindow.postMessage(cmds, '*');
}
/**
* Shows the blacklist notification and triggers a delay to hide it.
*/
function showNotification() {
if (configData.isMDIconsEnabled || configData.isMDUIEnabled) {
$(IDS.NOTIFICATION).classList.remove(CLASSES.HIDE_NOTIFICATION);
// Timeout is required for the "float up" transition to work. Modifying the
// "display" property prevents transitions from activating.
window.setTimeout(() => {
$(IDS.NOTIFICATION_CONTAINER).classList.add(CLASSES.FLOAT_UP);
}, 20);
// Automatically hide the notification after a period of time.
delayedHideNotification = window.setTimeout(() => {
hideNotification();
}, NOTIFICATION_TIMEOUT);
} else {
var notification = $(IDS.NOTIFICATION);
notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
notification.scrollTop;
notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
}
}
/**
* Hides the blacklist notification.
*/
function hideNotification() {
if (configData.isMDIconsEnabled || configData.isMDUIEnabled) {
let notification = $(IDS.NOTIFICATION_CONTAINER);
if (!notification.classList.contains(CLASSES.FLOAT_UP)) {
return;
}
window.clearTimeout(delayedHideNotification);
notification.classList.remove(CLASSES.FLOAT_UP);
let afterHide = (event) => {
if (event.propertyName == 'bottom') {
$(IDS.NOTIFICATION).classList.add(CLASSES.HIDE_NOTIFICATION);
notification.removeEventListener('transitionend', afterHide);
}
};
notification.addEventListener('transitionend', afterHide);
} else {
var notification = $(IDS.NOTIFICATION);
notification.classList.add(CLASSES.HIDE_NOTIFICATION);
notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
}
}
/**
* Handles a click on the notification undo link by hiding the notification and
* informing Chrome.
*/
function onUndo() {
hideNotification();
if (lastBlacklistedTile != null) {
ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile);
}
}
/**
* Handles a click on the restore all notification link by hiding the
* notification and informing Chrome.
*/
function onRestoreAll() {
hideNotification();
ntpApiHandle.undoAllMostVisitedDeletions();
}
/**
* Callback for embeddedSearch.newTabPage.oninputstart. Handles new input by
* disposing the NTP, according to where the input was entered.
*/
function onInputStart() {
if (isFakeboxFocused()) {
setFakeboxFocus(false);
setFakeboxDragFocus(false);
setFakeboxVisibility(false);
}
}
/**
* Callback for embeddedSearch.newTabPage.oninputcancel. Restores the NTP
* (re-enables the fakebox and unhides the logo.)
*/
function onInputCancel() {
setFakeboxVisibility(true);
}
/**
* @param {boolean} focus True to focus the fakebox.
*/
function setFakeboxFocus(focus) {
document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
}
/**
* @param {boolean} focus True to show a dragging focus on the fakebox.
*/
function setFakeboxDragFocus(focus) {
document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
}
/**
* @return {boolean} True if the fakebox has focus.
*/
function isFakeboxFocused() {
return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
}
/**
* @param {!Event} event The click event.
* @return {boolean} True if the click occurred in an enabled fakebox.
*/
function isFakeboxClick(event) {
return $(IDS.FAKEBOX).contains(event.target) &&
!$(IDS.FAKEBOX_MICROPHONE).contains(event.target);
}
/**
* @param {boolean} show True to show the fakebox and logo.
*/
function setFakeboxVisibility(show) {
document.body.classList.toggle(CLASSES.HIDE_FAKEBOX, !show);
}
/**
* @param {boolean} show True if do show the edit custom link dialog and disable
* scrolling.
*/
function setEditCustomLinkDialogVisibility(show) {
$(IDS.CUSTOM_LINKS_EDIT_IFRAME)
.classList.toggle(CLASSES.SHOW_EDIT_DIALOG, show);
document.body.classList.toggle(CLASSES.HIDE_BODY_OVERFLOW, show);
}
/**
* @param {!Element} element The element to register the handler for.
* @param {number} keycode The keycode of the key to register.
* @param {!Function} handler The key handler to register.
*/
function registerKeyHandler(element, keycode, handler) {
element.addEventListener('keydown', function(event) {
if (event.keyCode == keycode)
handler(event);
});
}
/**
* Event handler for messages from the most visited and edit custom link iframe.
* @param {Event} event Event received.
*/
function handlePostMessage(event) {
var cmd = event.data.cmd;
var args = event.data;
if (cmd === 'loaded') {
tilesAreLoaded = true;
if (configData.isGooglePage && !$('one-google-loader')) {
// Load the OneGoogleBar script. It'll create a global variable name "og"
// which is a dict corresponding to the native OneGoogleBarData type.
// We do this only after all the tiles have loaded, to avoid slowing down
// the main page load.
var ogScript = document.createElement('script');
ogScript.id = 'one-google-loader';
ogScript.src = 'chrome-search://local-ntp/one-google.js';
document.body.appendChild(ogScript);
ogScript.onload = function() {
injectOneGoogleBar(og);
};
}
if (configData.isCustomLinksEnabled) {
$(customBackgrounds.IDS.CUSTOM_LINKS_RESTORE_DEFAULT)
.classList.toggle(
customBackgrounds.CLASSES.OPTION_DISABLED,
!args.showRestoreDefault);
}
} else if (cmd === 'tileBlacklisted') {
showNotification();
lastBlacklistedTile = args.tid;
ntpApiHandle.deleteMostVisitedItem(args.tid);
} else if (cmd === 'resizeDoodle') {
let width = args.width || null;
let height = args.height || null;
let duration = args.duration || '0s';
let iframe = $(IDS.LOGO_DOODLE_IFRAME);
document.body.style.setProperty('--logo-iframe-height', height);
document.body.style.setProperty('--logo-iframe-width', width);
document.body.style.setProperty('--logo-iframe-resize-duration', duration);
} else if (cmd === 'startEditLink') {
$(IDS.CUSTOM_LINKS_EDIT_IFRAME)
.contentWindow.postMessage({cmd: 'linkData', tid: args.tid}, '*');
setEditCustomLinkDialogVisibility(true);
} else if (cmd === 'closeDialog') {
setEditCustomLinkDialogVisibility(false);
}
}
/**
* Enables Material Design styles for all of NTP.
*/
function enableMD() {
document.body.classList.add(CLASSES.MATERIAL_DESIGN);
enableMDIcons();
}
/**
* Enables Material Design styles for the Most Visited section.
*/
function enableMDIcons() {
$(IDS.MOST_VISITED).classList.add(CLASSES.MATERIAL_DESIGN_ICONS);
$(IDS.TILES).classList.add(CLASSES.MATERIAL_DESIGN_ICONS);
addRippleAnimations();
}
/**
* Enables ripple animations for elements with CLASSES.RIPPLE.
* TODO(kristipark): Remove after migrating to WebUI.
*/
function addRippleAnimations() {
let ripple = (event) => {
let target = event.target;
const rect = target.getBoundingClientRect();
const x = Math.round(event.clientX - rect.left);
const y = Math.round(event.clientY - rect.top);
// Calculate radius
const corners = [
{x: 0, y: 0},
{x: rect.width, y: 0},
{x: 0, y: rect.height},
{x: rect.width, y: rect.height},
];
let distance = (x1, y1, x2, y2) => {
var xDelta = x1 - x2;
var yDelta = y1 - y2;
return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
};
let cornerDistances = corners.map(function(corner) {
return Math.round(distance(x, y, corner.x, corner.y));
});
const radius =
Math.min(RIPPLE_MAX_RADIUS_PX, Math.max.apply(Math, cornerDistances));
let ripple = document.createElement('div');
let rippleContainer = document.createElement('div');
ripple.classList.add(CLASSES.RIPPLE_EFFECT);
rippleContainer.classList.add(CLASSES.RIPPLE_CONTAINER);
rippleContainer.appendChild(ripple);
target.appendChild(rippleContainer);
// Ripple start location
ripple.style.marginLeft = x + 'px';
ripple.style.marginTop = y + 'px';
rippleContainer.style.left = rect.left + 'px';
rippleContainer.style.top = rect.top + 'px';
rippleContainer.style.width = target.offsetWidth + 'px';
rippleContainer.style.height = target.offsetHeight + 'px';
rippleContainer.style.borderRadius =
window.getComputedStyle(target).borderRadius;
rippleContainer.style.position = 'fixed';
// Start transition/ripple
ripple.style.width = radius * 2 + 'px';
ripple.style.height = radius * 2 + 'px';
ripple.style.marginLeft = x - radius + 'px';
ripple.style.marginTop = y - radius + 'px';
ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)';
window.setTimeout(function() {
ripple.remove();
rippleContainer.remove();
}, RIPPLE_DURATION_MS);
};
let rippleElements = document.querySelectorAll('.' + CLASSES.RIPPLE);
for (let i = 0; i < rippleElements.length; i++) {
rippleElements[i].addEventListener('mousedown', ripple);
}
}
/**
* Prepares the New Tab Page by adding listeners, the most visited pages
* section, and Google-specific elements for a Google-provided page.
*/
function init() {
// If an accessibility tool is in use, increase the time for which the
// "tile was blacklisted" notification is shown.
if (configData.isAccessibleBrowser) {
document.body.style.setProperty('--mv-notice-time', '30s');
}
// Hide notifications after fade out, so we can't focus on links via keyboard.
$(IDS.NOTIFICATION).addEventListener('transitionend', (event) => {
if (event.properyName === 'opacity') {
hideNotification();
}
});
$(IDS.NOTIFICATION_MESSAGE).textContent =
configData.translatedStrings.thumbnailRemovedNotification;
var undoLink = $(IDS.UNDO_LINK);
undoLink.addEventListener('click', onUndo);
registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
registerKeyHandler(undoLink, KEYCODE.SPACE, onUndo);
undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
restoreAllLink.addEventListener('click', onRestoreAll);
registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onRestoreAll);
registerKeyHandler(restoreAllLink, KEYCODE.SPACE, onRestoreAll);
restoreAllLink.textContent =
configData.translatedStrings.restoreThumbnailsShort;
$(IDS.ATTRIBUTION_TEXT).textContent =
configData.translatedStrings.attributionIntro;
$(IDS.NOTIFICATION_CLOSE_BUTTON).addEventListener('click', hideNotification);
var embeddedSearchApiHandle = window.chrome.embeddedSearch;
ntpApiHandle = embeddedSearchApiHandle.newTabPage;
ntpApiHandle.onthemechange = onThemeChange;
ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
renderTheme();
var searchboxApiHandle = embeddedSearchApiHandle.searchBox;
if (configData.isGooglePage) {
if (configData.isMDUIEnabled) {
enableMD();
} else if (configData.isMDIconsEnabled) {
enableMDIcons();
}
if (configData.isCustomBackgroundsEnabled ||
configData.isCustomLinksEnabled) {
customBackgrounds.init();
// Hide items in the settings menu if a feature is disabled.
if (configData.isCustomBackgroundsEnabled &&
!configData.isCustomLinksEnabled) {
// Only custom backgrounds enabled, hide all custom link options.
$(customBackgrounds.IDS.CUSTOM_LINKS_RESTORE_DEFAULT).hidden = true;
} else if (
!configData.isCustomBackgroundsEnabled &&
configData.isCustomLinksEnabled) {
// Only custom links enabled, hide all custom background options.
$(customBackgrounds.IDS.DEFAULT_WALLPAPERS).hidden = true;
$(customBackgrounds.IDS.UPLOAD_IMAGE).hidden = true;
$(customBackgrounds.IDS.RESTORE_DEFAULT).hidden = true;
$(customBackgrounds.IDS.EDIT_BG_DIVIDER).hidden = true;
}
}
// Set up the fakebox (which only exists on the Google NTP).
ntpApiHandle.oninputstart = onInputStart;
ntpApiHandle.oninputcancel = onInputCancel;
if (ntpApiHandle.isInputInProgress) {
onInputStart();
}
$(IDS.FAKEBOX_TEXT).textContent =
configData.translatedStrings.searchboxPlaceholder;
if (configData.isVoiceSearchEnabled) {
speech.init(
configData.googleBaseUrl, configData.translatedStrings,
$(IDS.FAKEBOX_MICROPHONE), searchboxApiHandle);
}
// Listener for updating the key capture state.
document.body.onmousedown = function(event) {
if (isFakeboxClick(event))
searchboxApiHandle.startCapturingKeyStrokes();
else if (isFakeboxFocused())
searchboxApiHandle.stopCapturingKeyStrokes();
};
searchboxApiHandle.onkeycapturechange = function() {
setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
};
var inputbox = $(IDS.FAKEBOX_INPUT);
inputbox.onpaste = function(event) {
event.preventDefault();
// Send pasted text to Omnibox.
var text = event.clipboardData.getData('text/plain');
if (text)
searchboxApiHandle.paste(text);
};
inputbox.ondrop = function(event) {
event.preventDefault();
var text = event.dataTransfer.getData('text/plain');
if (text) {
searchboxApiHandle.paste(text);
}
setFakeboxDragFocus(false);
};
inputbox.ondragenter = function() {
setFakeboxDragFocus(true);
};
inputbox.ondragleave = function() {
setFakeboxDragFocus(false);
};
// Update the fakebox style to match the current key capturing state.
setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
// Also tell the browser that we're capturing, otherwise it's possible that
// both fakebox and Omnibox have visible focus at the same time, see
// crbug.com/792850.
if (searchboxApiHandle.isKeyCaptureEnabled) {
searchboxApiHandle.startCapturingKeyStrokes();
}
// Load the Doodle. After the first request completes (getting cached
// data), issue a second request for fresh Doodle data.
loadDoodle(/*v=*/null, function(ddl) {
if (ddl === null) {
// Got no ddl object at all, the feature is probably disabled. Just show
// the logo.
showLogoOrDoodle(/*fromCache=*/true);
return;
}
// Got a (possibly empty) ddl object. Show logo or doodle.
targetDoodle.image = ddl.image || null;
targetDoodle.metadata = ddl.metadata || null;
showLogoOrDoodle(/*fromCache=*/true);
// Never hide an interactive doodle if it was already shown.
if (ddl.metadata && (ddl.metadata.type === LOGO_TYPE.INTERACTIVE))
return;
// If we got a valid ddl object (from cache), load a fresh one.
if (ddl.v !== null) {
loadDoodle(ddl.v, function(ddl2) {
if (ddl2.usable) {
targetDoodle.image = ddl2.image || null;
targetDoodle.metadata = ddl2.metadata || null;
fadeToLogoOrDoodle();
}
});
}
});
// Set up doodle notifier (but it may be invisible).
var doodleNotifier = $(IDS.LOGO_DOODLE_NOTIFIER);
doodleNotifier.title = configData.translatedStrings.clickToViewDoodle;
doodleNotifier.addEventListener('click', function(e) {
e.preventDefault();
var state = window.history.state || {};
state.notheme = true;
window.history.replaceState(state, document.title);
onThemeChange();
if (e.detail === 0) { // Activated by keyboard.
$(IDS.LOGO_DOODLE_BUTTON).focus();
}
});
} else {
document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
}
if (searchboxApiHandle.rtl) {
$(IDS.NOTIFICATION).dir = 'rtl';
// Grabbing the root HTML element.
document.documentElement.setAttribute('dir', 'rtl');
// Add class for setting alignments based on language directionality.
document.documentElement.classList.add(CLASSES.RTL);
}
// Collect arguments for the most visited iframe.
var args = [];
if (searchboxApiHandle.rtl)
args.push('rtl=1');
if (NTP_DESIGN.numTitleLines > 1)
args.push('ntl=' + NTP_DESIGN.numTitleLines);
args.push('removeTooltip=' +
encodeURIComponent(configData.translatedStrings.removeThumbnailTooltip));
if (configData.isMDIconsEnabled) {
args.push('enableMD=1');
}
if (configData.isCustomLinksEnabled) {
args.push('enableCustomLinks=1');
args.push(
'addLink=' + encodeURIComponent(configData.translatedStrings.addLink));
args.push(
'addLinkTooltip=' +
encodeURIComponent(configData.translatedStrings.addLinkTooltip));
args.push(
'editLinkTooltip=' +
encodeURIComponent(configData.translatedStrings.editLinkTooltip));
}
// Create the most visited iframe.
var iframe = document.createElement('iframe');
iframe.id = IDS.TILES_IFRAME;
iframe.name = IDS.TILES_IFRAME;
iframe.title = configData.translatedStrings.mostVisitedTitle;
iframe.src = 'chrome-search://most-visited/single.html?' + args.join('&');
$(IDS.TILES).appendChild(iframe);
iframe.onload = function() {
reloadTiles();
sendThemeInfoToMostVisitedIframe();
};
if (configData.isCustomLinksEnabled) {
// Collect arguments for the edit custom link iframe.
let clArgs = [];
if (searchboxApiHandle.rtl)
clArgs.push('rtl=1');
clArgs.push(
'title=' +
encodeURIComponent(configData.translatedStrings.editLinkTitle));
clArgs.push(
'nameField=' +
encodeURIComponent(configData.translatedStrings.nameField));
clArgs.push(
'urlField=' +
encodeURIComponent(configData.translatedStrings.urlField));
clArgs.push(
'linkRemove=' +
encodeURIComponent(configData.translatedStrings.linkRemove));
clArgs.push(
'linkCancel=' +
encodeURIComponent(configData.translatedStrings.linkCancel));
clArgs.push(
'linkDone=' +
encodeURIComponent(configData.translatedStrings.linkDone));
clArgs.push(
'invalidUrl=' +
encodeURIComponent(configData.translatedStrings.invalidUrl));
// Create the edit custom link iframe.
let clIframe = document.createElement('iframe');
clIframe.id = IDS.CUSTOM_LINKS_EDIT_IFRAME;
clIframe.name = IDS.CUSTOM_LINKS_EDIT_IFRAME;
clIframe.title = configData.translatedStrings.editLinkTitle;
clIframe.src = 'chrome-search://most-visited/edit.html?' + clArgs.join('&');
document.body.appendChild(clIframe);
}
window.addEventListener('message', handlePostMessage);
document.body.classList.add(CLASSES.INITED);
}
/**
* Binds event listeners.
*/
function listen() {
document.addEventListener('DOMContentLoaded', init);
}
/**
* Injects the One Google Bar into the page. Called asynchronously, so that it
* doesn't block the main page load.
*/
function injectOneGoogleBar(ogb) {
var inHeadStyle = document.createElement('style');
inHeadStyle.type = 'text/css';
inHeadStyle.appendChild(document.createTextNode(ogb.inHeadStyle));
document.head.appendChild(inHeadStyle);
var inHeadScript = document.createElement('script');
inHeadScript.type = 'text/javascript';
inHeadScript.appendChild(document.createTextNode(ogb.inHeadScript));
document.head.appendChild(inHeadScript);
renderOneGoogleBarTheme();
var ogElem = $('one-google');
ogElem.innerHTML = ogb.barHtml;
ogElem.classList.remove('hidden');
var afterBarScript = document.createElement('script');
afterBarScript.type = 'text/javascript';
afterBarScript.appendChild(document.createTextNode(ogb.afterBarScript));
ogElem.parentNode.insertBefore(afterBarScript, ogElem.nextSibling);
$('one-google-end-of-body').innerHTML = ogb.endOfBodyHtml;
var endOfBodyScript = document.createElement('script');
endOfBodyScript.type = 'text/javascript';
endOfBodyScript.appendChild(document.createTextNode(ogb.endOfBodyScript));
document.body.appendChild(endOfBodyScript);
ntpApiHandle.logEvent(LOG_TYPE.NTP_ONE_GOOGLE_BAR_SHOWN);
}
/** Loads the Doodle. On success, the loaded script declares a global variable
* ddl, which onload() receives as its single argument. On failure, onload() is
* called with null as the argument. If v is null, then the call requests a
* cached logo. If non-null, it must be the ddl.v of a previous request for a
* cached logo, and the corresponding fresh logo is returned.
* @param {?number} v
* @param {function(?{v, usable, image, metadata})} onload
*/
var loadDoodle = function(v, onload) {
var ddlScript = document.createElement('script');
ddlScript.src = 'chrome-search://local-ntp/doodle.js';
if (v !== null)
ddlScript.src += '?v=' + v;
ddlScript.onload = function() {
onload(ddl);
};
ddlScript.onerror = function() {
onload(null);
};
// TODO(treib,sfiera): Add a timeout in case something goes wrong?
document.body.appendChild(ddlScript);
};
/** Handles the response of a doodle impression ping, i.e. stores the
* appropriate interactionLogUrl or onClickUrlExtraParams.
*
* @param {!Object} ddllog Response object from the ddllog ping.
* @param {!boolean} isAnimated
*/
var handleDdllogResponse = function(ddllog, isAnimated) {
if (ddllog && ddllog.interaction_log_url) {
let interactionLogUrl =
new URL(ddllog.interaction_log_url, configData.googleBaseUrl);
if (isAnimated) {
targetDoodle.animatedInteractionLogUrl = interactionLogUrl;
} else {
targetDoodle.staticInteractionLogUrl = interactionLogUrl;
}
lastDdllogResponse = 'interaction_log_url ' + ddllog.interaction_log_url;
} else if (ddllog && ddllog.target_url_params) {
targetDoodle.onClickUrlExtraParams =
new URLSearchParams(ddllog.target_url_params);
lastDdllogResponse = 'target_url_params ' + ddllog.target_url_params;
} else {
console.log('Invalid or missing ddllog response:');
console.log(ddllog);
}
};
/** Logs a doodle impression at the given logUrl, and handles the response via
* handleDdllogResponse.
*
* @param {!string} logUrl
* @param {!boolean} isAnimated
*/
var logDoodleImpression = function(logUrl, isAnimated) {
lastDdllogResponse = '';
fetch(logUrl, {credentials: 'omit'})
.then(function(response) {
return response.text();
})
.then(function(text) {
// Remove the optional XSS preamble.
const preamble = ')]}\'';
if (text.startsWith(preamble)) {
text = text.substr(preamble.length);
}
try {
var json = JSON.parse(text);
} catch (error) {
console.log('Failed to parse doodle impression response as JSON:');
console.log(error);
return;
}
handleDdllogResponse(json.ddllog, isAnimated);
})
.catch(function(error) {
console.log('Error logging doodle impression to "' + logUrl + '":');
console.log(error);
})
.finally(function() {
++numDdllogResponsesReceived;
if (onDdllogResponse !== null) {
onDdllogResponse();
}
});
};
/** Returns true if the target doodle is currently visible. If |image| is null,
* returns true when the default logo is visible; if non-null, checks that it
* matches the doodle that is currently visible. Here, "visible" means
* fully-visible or fading in.
*
* @returns {boolean}
*/
var isDoodleCurrentlyVisible = function() {
var haveDoodle = ($(IDS.LOGO_DOODLE).classList.contains(CLASSES.SHOW_LOGO));
var wantDoodle =
(targetDoodle.image !== null) && (targetDoodle.metadata !== null);
if (!haveDoodle || !wantDoodle) {
return haveDoodle === wantDoodle;
}
// Have a visible doodle and a target doodle. Test that they match.
if (targetDoodle.metadata.type === LOGO_TYPE.INTERACTIVE) {
var logoDoodleIframe = $(IDS.LOGO_DOODLE_IFRAME);
return logoDoodleIframe.classList.contains(CLASSES.SHOW_LOGO) &&
(logoDoodleIframe.src === targetDoodle.metadata.fullPageUrl);
} else {
var logoDoodleImage = $(IDS.LOGO_DOODLE_IMAGE);
var logoDoodleButton = $(IDS.LOGO_DOODLE_BUTTON);
return logoDoodleButton.classList.contains(CLASSES.SHOW_LOGO) &&
((logoDoodleImage.src === targetDoodle.image) ||
(logoDoodleImage.src === targetDoodle.metadata.animatedUrl));
}
};
/** The image and metadata that should be shown, according to the latest fetch.
* After a logo fades out, onDoodleFadeOutComplete fades in a logo according to
* targetDoodle.
*/
var targetDoodle = {
image: null,
metadata: null,
// The log URLs and params may be filled with the response from the
// corresponding impression log URL.
staticInteractionLogUrl: null,
animatedInteractionLogUrl: null,
onClickUrlExtraParams: null,
};
var getDoodleTargetUrl = function() {
let url = new URL(targetDoodle.metadata.onClickUrl);
if (targetDoodle.onClickUrlExtraParams) {
for (var param of targetDoodle.onClickUrlExtraParams) {
url.searchParams.append(param[0], param[1]);
}
}
return url;
};
var showLogoOrDoodle = function(fromCache) {
if (targetDoodle.metadata !== null) {
applyDoodleMetadata();
if (targetDoodle.metadata.type === LOGO_TYPE.INTERACTIVE) {
$(IDS.LOGO_DOODLE_BUTTON).classList.remove(CLASSES.SHOW_LOGO);
$(IDS.LOGO_DOODLE_IFRAME).classList.add(CLASSES.SHOW_LOGO);
} else {
$(IDS.LOGO_DOODLE_IMAGE).src = targetDoodle.image;
$(IDS.LOGO_DOODLE_BUTTON).classList.add(CLASSES.SHOW_LOGO);
$(IDS.LOGO_DOODLE_IFRAME).classList.remove(CLASSES.SHOW_LOGO);
// Log the impression in Chrome metrics.
var isCta = !!targetDoodle.metadata.animatedUrl;
var eventType = isCta ?
(fromCache ? LOG_TYPE.NTP_CTA_LOGO_SHOWN_FROM_CACHE :
LOG_TYPE.NTP_CTA_LOGO_SHOWN_FRESH) :
(fromCache ? LOG_TYPE.NTP_STATIC_LOGO_SHOWN_FROM_CACHE :
LOG_TYPE.NTP_STATIC_LOGO_SHOWN_FRESH);
ntpApiHandle.logEvent(eventType);
// Ping the proper impression logging URL if it exists.
var logUrl = isCta ? targetDoodle.metadata.ctaLogUrl :
targetDoodle.metadata.logUrl;
if (logUrl) {
logDoodleImpression(logUrl, /*isAnimated=*/false);
}
}
$(IDS.LOGO_DOODLE).classList.add(CLASSES.SHOW_LOGO);
} else {
// No doodle. Just show the default logo.
$(IDS.LOGO_DEFAULT).classList.add(CLASSES.SHOW_LOGO);
}
};
/**
* Starts fading out the given element, which should be either the default logo
* or the doodle.
*
* @param {HTMLElement} element
*/
var startFadeOut = function(element) {
if (!element.classList.contains(CLASSES.SHOW_LOGO)) {
return;
}
// Compute style now, to ensure that the transition from 1 -> 0 is properly
// recognized. Otherwise, if a 0 -> 1 -> 0 transition is too fast, the
// element might stay invisible instead of appearing then fading out.
window.getComputedStyle(element).opacity;
element.classList.add(CLASSES.FADE);
element.classList.remove(CLASSES.SHOW_LOGO);
element.addEventListener('transitionend', onDoodleFadeOutComplete);
};
/**
* Integrates a fresh doodle into the page as appropriate. If the correct logo
* or doodle is already shown, just updates the metadata. Otherwise, initiates
* a fade from the currently-shown logo/doodle to the new one.
*/
var fadeToLogoOrDoodle = function() {
// If the image is already visible, there's no need to start a fade-out.
// However, metadata may have changed, so update the doodle's alt text and
// href, if applicable.
if (isDoodleCurrentlyVisible()) {
if (targetDoodle.metadata !== null) {
applyDoodleMetadata();
}
return;
}
// It's not the same doodle. Clear any loging URLs/params we might have.
targetDoodle.staticInteractionLogUrl = null;
targetDoodle.animatedInteractionLogUrl = null;
targetDoodle.onClickUrlExtraParams = null;
// Start fading out the current logo or doodle. onDoodleFadeOutComplete will
// apply the change when the fade-out finishes.
startFadeOut($(IDS.LOGO_DEFAULT));
startFadeOut($(IDS.LOGO_DOODLE));
};
var onDoodleFadeOutComplete = function(e) {
// Fade-out finished. Start fading in the appropriate logo.
$(IDS.LOGO_DOODLE).classList.add(CLASSES.FADE);
$(IDS.LOGO_DEFAULT).classList.add(CLASSES.FADE);
showLogoOrDoodle(/*fromCache=*/false);
this.removeEventListener('transitionend', onDoodleFadeOutComplete);
};
var applyDoodleMetadata = function() {
var logoDoodleButton = $(IDS.LOGO_DOODLE_BUTTON);
var logoDoodleImage = $(IDS.LOGO_DOODLE_IMAGE);
var logoDoodleIframe = $(IDS.LOGO_DOODLE_IFRAME);
switch (targetDoodle.metadata.type) {
case LOGO_TYPE.SIMPLE:
logoDoodleImage.title = targetDoodle.metadata.altText;
// On click, navigate to the target URL.
logoDoodleButton.onclick = function() {
// Log the click in Chrome metrics.
ntpApiHandle.logEvent(LOG_TYPE.NTP_STATIC_LOGO_CLICKED);
// Ping the static interaction_log_url if there is one.
if (targetDoodle.staticInteractionLogUrl) {
navigator.sendBeacon(targetDoodle.staticInteractionLogUrl);
targetDoodle.staticInteractionLogUrl = null;
}
window.location = getDoodleTargetUrl();
};
break;
case LOGO_TYPE.ANIMATED:
logoDoodleImage.title = targetDoodle.metadata.altText;
// The CTA image is currently shown; on click, show the animated one.
logoDoodleButton.onclick = function(e) {
e.preventDefault();
// Log the click in Chrome metrics.
ntpApiHandle.logEvent(LOG_TYPE.NTP_CTA_LOGO_CLICKED);
// Ping the static interaction_log_url if there is one.
if (targetDoodle.staticInteractionLogUrl) {
navigator.sendBeacon(targetDoodle.staticInteractionLogUrl);
targetDoodle.staticInteractionLogUrl = null;
}
// Once the animated image loads, ping the impression log URL.
if (targetDoodle.metadata.logUrl) {
logoDoodleImage.onload = function() {
logDoodleImpression(
targetDoodle.metadata.logUrl, /*isAnimated=*/true);
};
}
logoDoodleImage.src = targetDoodle.metadata.animatedUrl;
// When the animated image is clicked, navigate to the target URL.
logoDoodleButton.onclick = function() {
// Log the click in Chrome metrics.
ntpApiHandle.logEvent(LOG_TYPE.NTP_ANIMATED_LOGO_CLICKED);
// Ping the animated interaction_log_url if there is one.
if (targetDoodle.animatedInteractionLogUrl) {
navigator.sendBeacon(targetDoodle.animatedInteractionLogUrl);
targetDoodle.animatedInteractionLogUrl = null;
}
window.location = getDoodleTargetUrl();
};
};
break;
case LOGO_TYPE.INTERACTIVE:
logoDoodleIframe.title = targetDoodle.metadata.altText;
logoDoodleIframe.src = targetDoodle.metadata.fullPageUrl;
document.body.style.setProperty(
'--logo-iframe-width', targetDoodle.metadata.iframeWidthPx + 'px');
document.body.style.setProperty(
'--logo-iframe-height', targetDoodle.metadata.iframeHeightPx + 'px');
document.body.style.setProperty(
'--logo-iframe-initial-height',
targetDoodle.metadata.iframeHeightPx + 'px');
break;
}
};
return {
init: init, // Exposed for testing.
listen: listen
};
}
if (!window.localNTPUnitTest) {
LocalNTP().listen();
}