// Copyright 2013 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. */ /** * Controls rendering the new tab page for InstantExtended. * @return {Object} A limited interface for testing the local NTP. */ function LocalNTP() { /** * Enum for classnames. * @enum {string} * @const */ var CLASSES = { ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation BLACKLIST_BUTTON: 'mv-x', BLACKLIST_BUTTON_INNER: 'mv-x-inner', DARK: 'dark', DEFAULT_THEME: 'default-theme', DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', DOT: 'dot', FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox // Applies drag focus style to the fakebox FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', FAVICON: 'mv-favicon', FAVICON_FALLBACK: 'mv-favicon-fallback', FOCUSED: 'mv-focused', HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo', HIDE_NOTIFICATION: 'mv-notice-hide', // Vertically centers the most visited section for a non-Google provided page. NON_GOOGLE_PAGE: 'non-google-page', PAGE: 'mv-page', // page tiles PAGE_READY: 'mv-page-ready', // page tile when ready RTL: 'rtl', // Right-to-left language text. THUMBNAIL: 'mv-thumb', THUMBNAIL_FALLBACK: 'mv-thumb-fallback', THUMBNAIL_MASK: 'mv-mask', TILE: 'mv-tile', TILE_INNER: 'mv-tile-inner', TITLE: 'mv-title' }; /** * Enum for HTML element ids. * @enum {string} * @const */ var IDS = { ATTRIBUTION: 'attribution', ATTRIBUTION_TEXT: 'attribution-text', CUSTOM_THEME_STYLE: 'ct-style', FAKEBOX: 'fakebox', FAKEBOX_INPUT: 'fakebox-input', FAKEBOX_TEXT: 'fakebox-text', LOGO: 'logo', NOTIFICATION: 'mv-notice', NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', NOTIFICATION_MESSAGE: 'mv-msg', NTP_CONTENTS: 'ntp-contents', RESTORE_ALL_LINK: 'mv-restore', TILES: 'mv-tiles', UNDO_LINK: 'mv-undo' }; /** * Enum for keycodes. * @enum {number} * @const */ var KEYCODE = { ENTER: 13 }; /** * Enum for the state of the NTP when it is disposed. * @enum {number} * @const */ var NTP_DISPOSE_STATE = { NONE: 0, // Preserve the NTP appearance and functionality DISABLE_FAKEBOX: 1, HIDE_FAKEBOX_AND_LOGO: 2 }; /** * The JavaScript button event value for a middle click. * @type {number} * @const */ var MIDDLE_MOUSE_BUTTON = 1; /** * Specifications for the NTP design. * @const {NtpDesign} */ var NTP_DESIGN = getNtpDesign(configData.ntpDesignName); /** * The container for the tile elements. * @type {Element} */ var tilesContainer; /** * The notification displayed when a page is blacklisted. * @type {Element} */ var notification; /** * The container for the theme attribution. * @type {Element} */ var attribution; /** * The "fakebox" - an input field that looks like a regular searchbox. When it * is focused, any text the user types goes directly into the omnibox. * @type {Element} */ var fakebox; /** * The container for NTP elements. * @type {Element} */ var ntpContents; /** * The array of rendered tiles, ordered by appearance. * @type {!Array.} */ var tiles = []; /** * The last blacklisted tile if any, which by definition should not be filler. * @type {?Tile} */ var lastBlacklistedTile = null; /** * The iframe element which is currently keyboard focused, or null. * @type {?Element} */ var focusedIframe = null; /** * True if a page has been blacklisted and we're waiting on the * onmostvisitedchange callback. See onMostVisitedChange() for how this * is used. * @type {boolean} */ var isBlacklisting = false; /** * Current number of tiles columns shown based on the window width, including * those that just contain filler. * @type {number} */ var numColumnsShown = 0; /** * A flag to indicate Most Visited changed caused by user action. If true, then * in onMostVisitedChange() tiles remain visible so no flickering occurs. * @type {boolean} */ var userInitiatedMostVisitedChange = false; /** * The browser embeddedSearch.newTabPage object. * @type {Object} */ var ntpApiHandle; /** * The browser embeddedSearch.searchBox object. * @type {Object} */ var searchboxApiHandle; /** * The state of the NTP when a query is entered into the Omnibox. * @type {NTP_DISPOSE_STATE} */ var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE; /** * The state of the NTP when a query is entered into the Fakebox. * @type {NTP_DISPOSE_STATE} */ var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO; /** @type {number} @const */ var MAX_NUM_TILES_TO_SHOW = 8; /** @type {number} @const */ var MIN_NUM_COLUMNS = 2; /** @type {number} @const */ var MAX_NUM_COLUMNS = 4; /** @type {number} @const */ var NUM_ROWS = 2; /** * Minimum total padding to give to the left and right of the most visited * section. Used to determine how many tiles to show. * @type {number} * @const */ var MIN_TOTAL_HORIZONTAL_PADDING = 200; /** * The filename for a most visited iframe src which shows a page title. * @type {string} * @const */ var MOST_VISITED_TITLE_IFRAME = 'title.html'; /** * The filename for a most visited iframe src which shows a thumbnail image. * @type {string} * @const */ var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html'; /** * The color of the title in RRGGBBAA format. * @type {?string} */ var titleColor = null; /** * Hide most visited tiles for at most this many milliseconds while painting. * @type {number} * @const */ var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500; /** * A Tile is either a rendering of a Most Visited page or "filler" used to * pad out the section when not enough pages exist. * * @param {Element} elem The element for rendering the tile. * @param {Element=} opt_innerElem The element for contents of tile. * @param {Element=} opt_titleElem The element for rendering the title. * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail. * @param {number=} opt_rid The RID for the corresponding Most Visited page. * Should only be left unspecified when creating a filler tile. * @constructor */ function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) { /** @type {Element} */ this.elem = elem; /** @type {Element|undefined} */ this.innerElem = opt_innerElem; /** @type {Element|undefined} */ this.titleElem = opt_titleElem; /** @type {Element|undefined} */ this.thumbnailElem = opt_thumbnailElem; /** @type {number|undefined} */ this.rid = opt_rid; } /** * 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(info) { 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() { var fakeboxText = $(IDS.FAKEBOX_TEXT); if (fakeboxText) { fakeboxText.innerHTML = ''; if (NTP_DESIGN.showFakeboxHint && configData.translatedStrings.searchboxPlaceholder) { fakeboxText.textContent = configData.translatedStrings.searchboxPlaceholder; } } var info = ntpApiHandle.themeBackgroundInfo; var isThemeDark = getIsThemeDark(info); ntpContents.classList.toggle(CLASSES.DARK, isThemeDark); if (!info) { titleColor = NTP_DESIGN.titleColor; return; } if (!info.usingDefaultTheme && info.textColorRgba) { titleColor = convertToRRGGBBAAColor(info.textColorRgba); } else { titleColor = isThemeDark ? NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor; } 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); updateThemeAttribution(info.attributionUrl); setCustomThemeStyle(info); } /** * Updates the NTP based on the current theme, then rerenders all tiles. * @private */ function onThemeChange() { renderTheme(); tilesContainer.innerHTML = ''; renderAndShowTiles(); } /** * Updates the NTP style according to theme. * @param {Object=} opt_themeInfo The information about the theme. If it is * omitted the style will be reverted to the default. * @private */ function setCustomThemeStyle(opt_themeInfo) { var customStyleElement = $(IDS.CUSTOM_THEME_STYLE); var head = document.head; if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) { ntpContents.classList.remove(CLASSES.DEFAULT_THEME); var themeStyle = '#attribution {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + '}' + '#mv-msg {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' + '}' + '#mv-notice-links span {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + '}' + '#mv-notice-x {' + ' -webkit-filter: drop-shadow(0 0 0 ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' + '}' + '.mv-page-ready .mv-mask {' + ' border: 1px solid ' + convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' + '}' + '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' + ' border-color: ' + convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' + '}'; if (customStyleElement) { customStyleElement.textContent = themeStyle; } else { customStyleElement = document.createElement('style'); customStyleElement.type = 'text/css'; customStyleElement.id = IDS.CUSTOM_THEME_STYLE; customStyleElement.textContent = themeStyle; head.appendChild(customStyleElement); } } else { ntpContents.classList.add(CLASSES.DEFAULT_THEME); if (customStyleElement) head.removeChild(customStyleElement); } } /** * Renders the attribution if the URL is present, otherwise hides it. * @param {string} url The URL of the attribution image, if any. * @private */ function updateThemeAttribution(url) { if (!url) { setAttributionVisibility_(false); return; } var attributionImage = attribution.querySelector('img'); if (!attributionImage) { attributionImage = new Image(); attribution.appendChild(attributionImage); } attributionImage.style.content = url; setAttributionVisibility_(true); } /** * Sets the visibility of the theme attribution. * @param {boolean} show True to show the attribution. * @private */ function setAttributionVisibility_(show) { if (attribution) { attribution.style.display = show ? '' : 'none'; } } /** * Converts an Array of color components into RRGGBBAA format. * @param {Array.} color Array of rgba color components. * @return {string} Color string in RRGGBBAA format. * @private */ function convertToRRGGBBAAColor(color) { return color.map(function(t) { return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex. }).join(''); } /** * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". * @param {Array.} 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 + ')'; } /** * Handles a new set of Most Visited page data. */ function onMostVisitedChange() { if (isBlacklisting) { // Trigger the blacklist animation, which then triggers reloadAllTiles(). var lastBlacklistedTileElem = lastBlacklistedTile.elem; lastBlacklistedTileElem.addEventListener( 'webkitTransitionEnd', blacklistAnimationDone); lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST); } else { reloadAllTiles(); } } /** * Handles the end of the blacklist animation by showing the notification and * re-rendering the new set of tiles. */ function blacklistAnimationDone() { showNotification(); isBlacklisting = false; tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON); lastBlacklistedTile.elem.removeEventListener( 'webkitTransitionEnd', blacklistAnimationDone); // Need to call explicitly to re-render the tiles, since the initial // onmostvisitedchange issued by the blacklist function only triggered // the animation. reloadAllTiles(); } /** * Fetches new data, creates, and renders tiles. */ function reloadAllTiles() { var pages = ntpApiHandle.mostVisited; tiles = []; for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) tiles.push(createTile(pages[i], i)); tilesContainer.innerHTML = ''; renderAndShowTiles(); } /** * Binds onload events for a tile's internal