Chromium Code Reviews| Index: chrome/browser/resources/local_ntp/local_ntp.js |
| diff --git a/chrome/browser/resources/local_ntp/local_ntp.js b/chrome/browser/resources/local_ntp/local_ntp.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..3f6d2d31fe424e470348bc7f356d3faadf9df5ad |
| --- /dev/null |
| +++ b/chrome/browser/resources/local_ntp/local_ntp.js |
| @@ -0,0 +1,583 @@ |
| +// 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. |
| + |
| +/** |
| + * The element used to vertically position the most visited section on |
| + * window resize. |
| + * @type {Element} |
| + */ |
| +var topMarginElement; |
| + |
| +/** |
| + * The container for the tile elements. |
| + * @type {Element} |
| + */ |
| +var tilesContainer; |
| + |
| +/** |
| + * The notification displayed when a page is blacklisted. |
| + * @type {Element} |
| + */ |
| +var notification; |
| + |
| +/** |
| + * The handle for the timer used to hide the notification. |
| + * @type {?number} |
| + */ |
| +var notificationTimer = null; |
| + |
| +/** |
| + * The array of rendered tiles, ordered by appearance. |
| + * @type {Array.<Tile>} |
| + */ |
| +var tiles = []; |
| + |
| +/** |
| + * The last blacklisted tile if any, which by definition should not be filler. |
| + * @type {?Tile} |
| + */ |
| +var lastBlacklistedTile = null; |
| + |
| +/** |
| + * The index of the last blacklisted tile, if any. Used to determine where to |
| + * re-insert a tile on undo. |
| + * @type {number} |
| + */ |
| +var lastBlacklistedIndex = -1; |
| + |
| +/** |
| + * 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; |
| + |
| +/** |
| + * True if a blacklist has been undone and we're waiting on the |
| + * onmostvisitedchange callback. See onMostVisitedChange() for how this |
| + * is used. |
| + * @type {boolean} |
| + */ |
| +var isUndoing = false; |
| + |
| +/** |
| + * Current number of tiles shown based on the window width, including filler. |
| + * @type {number} |
| + */ |
| +var numTilesShown = 0; |
| + |
| +/** |
| + * The browser embeddedSearch.newTabPage object. |
| + * @type {Object} |
| + */ |
| +var apiHandle; |
| + |
| +/** |
| + * Possible background-colors of a non-custom theme. Used to determine whether |
|
Dan Beam
2013/03/21 02:45:08
1 \s between sentences in comments.
jeremycho
2013/03/21 05:28:42
Done.
|
| + * the homepage should be updated to support custom or non-custom themes. |
| + * @type {Array.<string>} |
| + * @const |
| + */ |
| +var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)']; |
|
Dan Beam
2013/03/21 02:45:08
uh, the second color here is transparent black?
jeremycho
2013/03/21 05:28:42
This gets passed on occasion when a default theme
|
| + |
| +/** |
| + * Should be equal to mv-tile's -webkit-margin-start + width. |
| + * @type {number} |
| + * @const |
| + */ |
| +var TILE_WIDTH = 160; |
| + |
| +/** |
| + * The height of the most visited section. |
| + * @type {number} |
|
Dan Beam
2013/03/21 02:45:08
@const
jeremycho
2013/03/21 05:28:42
Done.
|
| + */ |
| +var MOST_VISITED_HEIGHT = 156; |
| + |
| +/** @type {number} @const */ |
| +var MAX_NUM_TILES_TO_SHOW = 4; |
| + |
| +/** @type {number} @const */ |
| +var MIN_NUM_TILES_TO_SHOW = 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 |
|
Dan Beam
2013/03/21 02:45:08
@const should be on next line if the comment is mu
jeremycho
2013/03/21 05:28:42
Done.
|
| + */ |
| +var MIN_TOTAL_HORIZONTAL_PADDING = 188; |
| + |
| +/** @type {string} @const */ |
| +var TILE_CLASS = 'mv-tile'; |
| + |
| +/** |
| + * Class applied to page tiles. |
| + * @type {string} @const |
| + */ |
| +var PAGE_CLASS = 'mv-page'; |
| + |
| +/** |
| + * Class applied to a page tile's title. |
| + * @type {string} @const |
| + */ |
| +var TITLE_CLASS = 'mv-title'; |
| + |
| +/** |
| + * Class applied to a page tile's thumbnail. |
| + * @type {string} @const |
| + */ |
| +var THUMBNAIL_CLASS = 'mv-thumb'; |
| + |
| +/** |
| + * Class applied to a page tile's domain. |
| + * @type {string} @const |
| + */ |
| +var DOMAIN_CLASS = 'mv-domain'; |
| + |
| +/** |
| + * Class applied to a page tile's blacklist button. |
| + * @type {string} @const |
| + */ |
| +var BLACKLIST_BUTTON_CLASS = 'mv-x'; |
| + |
| +/** |
| + * Class applied to a page tile's favicon. |
| + * @type {string} @const |
| + */ |
| +var FAVICON_CLASS = 'mv-fav'; |
| + |
| +/** |
| + * Class applied to filler tiles. |
| + * @type {string} @const |
| + */ |
| +var FILLER_CLASS = 'mv-filler'; |
| + |
| +/** |
| + * Class applied to tiles to trigger the blacklist animation. |
| + * @type {string} @const |
| + */ |
| +var BLACKLIST_CLASS = 'mv-bl'; |
| + |
| +/** |
| + * Class applied to tiles to hide them on small browser width. |
| + * @type {string} @const |
| + */ |
| +var HIDE_TILE_CLASS = 'mv-tile-hide'; |
| + |
| +/** |
| + * Class applied to hide the blacklist buttons during the blacklist animation. |
| + * @type {string} @const |
| + */ |
| +var HIDE_BLACKLIST_BUTTON_CLASS = 'mv-x-hide'; |
| + |
| +/** |
| + * Class applied to the notification to hide it. |
| + * @type {string} @const |
| + */ |
| +var HIDE_NOTIFICATION_CLASS = 'mv-noti-hide'; |
|
Dan Beam
2013/03/21 02:45:08
var CLASSES = {
HIDE_NOTIFICATION: 'mv-noti-hide
jeremycho
2013/03/21 05:28:42
Done.
|
| + |
| +/** |
| + * Time (in milliseconds) to show the notification. |
| + * @type {number} @const |
| + */ |
| +var NOTIFICATION_TIMEOUT = 10000; |
| + |
| +/** |
| + * 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 {number=} opt_rid The RID for the corresponding Most Visited page. |
| + * Should only be left unspecified when creating a filler tile. |
| + * @constructor |
| + */ |
| +Tile = function(elem, opt_rid) { |
|
Dan Beam
2013/03/21 02:45:08
s/Tile = function(/function Tile(/
jeremycho
2013/03/21 05:28:42
Done.
|
| + /** @type {Element} */ |
| + this.elem = elem; |
| + |
| + /** @type {number|undefined} */ |
| + this.rid = opt_rid; |
| +}; |
| + |
| +/** |
| + * Updates the NTP based on the current theme. |
| + * @private |
| + */ |
| +function onThemeChange() { |
| + var info = apiHandle.themeBackgroundInfo; |
| + if (!info) |
| + return; |
| + var background = [info.colorRgba, |
| + info.imageUrl, |
| + info.imageTiling, |
| + info.imageHorizontalAlignment, |
| + info.imageVerticalAlignment].join(' ').trim(); |
| + document.body.style.background = background; |
| + var isCustom = !!background && WHITE.indexOf(background) == -1; |
| + enable(document.body, 'custom-theme', isCustom); |
| +} |
| + |
| +/** |
| + * Handles a new set of Most Visited page data. |
| + */ |
| +function onMostVisitedChange() { |
| + var pages = apiHandle.mostVisited; |
| + |
| + // If this was called as a result of a blacklist, add a new replacement |
| + // (possibly filler) tile at the end and trigger the blacklist animation. |
| + if (isBlacklisting) { |
| + var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]); |
| + |
| + tiles.push(replacementTile); |
| + tilesContainer.appendChild(replacementTile.elem); |
| + |
| + var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
| + lastBlacklistedTileElement.addEventListener( |
| + 'webkitTransitionEnd', blacklistAnimationDone); |
| + lastBlacklistedTileElement.classList.add(BLACKLIST_CLASS); |
| + // In order to animate the replacement tile sliding into place, it must |
| + // be made visible. |
| + updateTileVisibility(numTilesShown + 1); |
| + |
| + // If this was called as a result of an undo, re-insert the last blacklisted |
| + // tile in its old location and trigger the undo animation. |
| + } else if (isUndoing) { |
| + tiles.splice( |
| + lastBlacklistedIndex, 0, lastBlacklistedTile); |
| + var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
| + tilesContainer.insertBefore( |
| + lastBlacklistedTileElement, |
| + tilesContainer.childNodes[lastBlacklistedIndex]); |
| + lastBlacklistedTileElement.addEventListener( |
| + 'webkitTransitionEnd', undoAnimationDone); |
| + // Yield to ensure the tile is added to the DOM before removing the class. |
| + // Without this, the webkit transition doesn't reliably trigger. |
| + window.setTimeout(function() { |
| + lastBlacklistedTileElement.classList.remove(BLACKLIST_CLASS); |
| + }, 0); |
| + // Otherwise render the tiles using the new data without animation. |
| + } else { |
| + tiles = []; |
| + for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) { |
| + tiles.push(createTile(pages[i])); |
| + } |
| + renderTiles(); |
| + } |
| +} |
| + |
| +/** |
| + * Renders the current set of tiles without animation. |
| + */ |
| +function renderTiles() { |
| + removeChildren(tilesContainer); |
| + for (var i = 0, length = tiles.length; i < length; ++i) { |
| + tilesContainer.appendChild(tiles[i].elem); |
| + } |
| +} |
| + |
| +/** |
| + * Creates a Tile with the specified page data. If no data is provided, a |
| + * filler Tile is created. |
| + * @param {Object} page The page data. |
| + * @return {Tile} The new Tile_. |
| + */ |
| +function createTile(page) { |
| + var tileElement = document.createElement('div'); |
| + tileElement.classList.add(TILE_CLASS); |
| + |
| + if (page) { |
| + var rid = page.rid; |
| + tileElement.classList.add(PAGE_CLASS); |
| + |
| + // The click handler for navigating to the page identified by the RID. |
| + tileElement.addEventListener('click', function() { |
| + apiHandle.navigateContentWindow(rid); |
| + }); |
| + |
| + // The shadow DOM which renders the page title. |
| + var titleElement = page.titleElement; |
| + if (titleElement) { |
| + titleElement.classList.add(TITLE_CLASS); |
| + tileElement.appendChild(titleElement); |
| + } |
| + |
| + // Render the thumbnail if present. Otherwise, fall back to a shadow DOM |
| + // which renders the domain. |
| + var thumbnailUrl = page.thumbnailUrl; |
| + |
| + var showDomainElement = function() { |
| + var domainElement = page.domainElement; |
| + if (domainElement) { |
| + domainElement.classList.add(DOMAIN_CLASS); |
| + tileElement.appendChild(domainElement); |
| + } |
| + }; |
| + if (thumbnailUrl) { |
| + var image = new Image(); |
| + image.onload = function() { |
| + var thumbnailElement = createAndAppendElement( |
| + tileElement, 'div', THUMBNAIL_CLASS); |
| + thumbnailElement.style['background-image'] = |
| + 'url(' + thumbnailUrl + ')'; |
| + }; |
| + |
| + image.onerror = showDomainElement; |
| + image.src = thumbnailUrl; |
| + } else { |
| + showDomainElement(); |
| + } |
| + |
| + // The button used to blacklist this page. |
| + var blacklistButton = createAndAppendElement( |
| + tileElement, 'div', BLACKLIST_BUTTON_CLASS); |
| + blacklistButton.addEventListener('click', generateBlacklistFunction(rid)); |
| + // TODO(jeremycho): i18n. |
| + blacklistButton.title = "Don't show on this page"; |
| + |
| + // The page favicon, if any. |
| + var faviconUrl = page.faviconUrl; |
| + if (faviconUrl) { |
| + var favicon = createAndAppendElement( |
| + tileElement, 'div', FAVICON_CLASS); |
| + favicon.style['background-image'] = 'url(' + faviconUrl + ')'; |
| + } |
| + return new Tile(tileElement, rid); |
| + } else { |
| + tileElement.classList.add(FILLER_CLASS); |
| + return new Tile(tileElement); |
| + } |
| +} |
| + |
| +/** |
| + * Generates a function to be called when the page with the corresponding RID |
| + * is blacklisted. |
| + * @param {number} rid The RID of the page being blacklisted. |
| + * @return {function(Event)} A function which handles the blacklisting of the |
| + * page by displaying the notification, updating state variables, and |
| + * notifying Chrome. |
| + */ |
| +function generateBlacklistFunction(rid) { |
| + return function(e) { |
| + // Prevent navigation when the page is being blacklisted. |
| + e.stopPropagation(); |
| + |
| + showNotification(); |
| + isBlacklisting = true; |
| + tilesContainer.classList.add(HIDE_BLACKLIST_BUTTON_CLASS); |
| + lastBlacklistedTile = getTileByRid(rid); |
| + lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile); |
| + apiHandle.deleteMostVisitedItem(rid); |
| + }; |
| +} |
| + |
| +/** |
| + * Shows the blacklist notification and refreshes the timer to hide it. |
| + */ |
| +function showNotification() { |
| + notification.classList.remove(HIDE_NOTIFICATION_CLASS); |
| + if (notificationTimer) |
| + window.clearTimeout(notificationTimer); |
| + notificationTimer = window.setTimeout( |
| + hideNotification, NOTIFICATION_TIMEOUT); |
| +} |
| + |
| + |
| +/** |
| + * Hides the blacklist notification. |
| + */ |
| +function hideNotification() { |
| + notification.classList.add(HIDE_NOTIFICATION_CLASS); |
| +} |
| + |
| +/** |
| + * Handles the end of the blacklist animation by removing the blacklisted tile. |
| + */ |
| +function blacklistAnimationDone() { |
| + tiles.splice(lastBlacklistedIndex, 1); |
| + removeNode(lastBlacklistedTile.elem); |
| + updateTileVisibility(numTilesShown); |
| + isBlacklisting = false; |
| + tilesContainer.classList.remove(HIDE_BLACKLIST_BUTTON_CLASS); |
| + lastBlacklistedTile.elem.removeEventListener( |
| + 'webkitTransitionEnd', blacklistAnimationDone); |
| +} |
| + |
| +/** |
| + * Handles a click on the notification undo link by hiding the notification and |
| + * informing Chrome. |
| + */ |
| +function onUndo() { |
| + hideNotification(); |
| + var lastBlacklistedRID = lastBlacklistedTile.rid; |
| + if (lastBlacklistedRID != null) { |
| + isUndoing = true; |
| + apiHandle.undoMostVisitedDeletion(lastBlacklistedRID); |
| + } |
| +} |
| + |
| +/** |
| + * Handles the end of the undo animation by removing the extraneous end tile. |
| + */ |
| +function undoAnimationDone() { |
| + isUndoing = false; |
| + tiles.splice(tiles.length - 1, 1); |
| + removeNode(tilesContainer.lastElementChild); |
| + updateTileVisibility(numTilesShown); |
| + lastBlacklistedTile.elem.removeEventListener( |
| + 'webkitTransitionEnd', undoAnimationDone); |
| +} |
| + |
| +/** |
| + * Handles a click on the restore all notification link by hiding the |
| + * notification and informing Chrome. |
| + */ |
| +function onRestoreAll() { |
| + hideNotification(); |
| + apiHandle.undoAllMostVisitedDeletions(); |
| +} |
| + |
| +/** |
| + * Handles a resize by vertically centering the most visited section |
| + * and triggering the tile show/hide animation if necessary. |
| + */ |
| +function onResize() { |
| + var clientHeight = document.documentElement.clientHeight; |
| + topMarginElement.style['margin-top'] = |
| + Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px'; |
| + |
| + var clientWidth = document.documentElement.clientWidth; |
| + var numTilesToShow = Math.floor( |
| + (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH); |
| + numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow); |
| + if (numTilesToShow != numTilesShown) { |
| + updateTileVisibility(numTilesToShow); |
| + numTilesShown = numTilesToShow; |
| + } |
| +} |
| + |
| + |
| +/** |
| + * Triggers an animation to show the first numTilesToShow tiles and hide the |
| + * remaining. |
| + * @param {number} numTilesToShow The number of tiles to show. |
| + */ |
| +function updateTileVisibility(numTilesToShow) { |
| + for (var i = 0, length = tiles.length; i < length; ++i) { |
| + enable(tiles[i].elem, HIDE_TILE_CLASS, i >= numTilesToShow); |
| + } |
| +} |
| + |
| +/** |
| + * Returns the tile corresponding to the specified page RID. |
| + * @param {number} rid The page RID being looked up. |
| + * @return {Tile} The corresponding tile. |
| + */ |
| +function getTileByRid(rid) { |
| + for (var i = 0, length = tiles.length; i < length; ++i) { |
| + var tile = tiles[i]; |
| + if (tile.rid == rid) |
| + return tile; |
| + } |
| + return null; |
| +} |
| + |
| +/** |
| + * Utility function which creates an element with an optional classname and |
| + * appends it to the specified parent. |
| + * @param {Element} parent The parent to append the new element. |
| + * @param {string} name The name of the new element. |
| + * @param {string=} opt_class The optional classname of the new element. |
| + * @return {Element} The new element. |
| + */ |
| +function createAndAppendElement(parent, name, opt_class) { |
| + var child = document.createElement(name); |
| + if (opt_class) |
| + child.classList.add(opt_class); |
| + parent.appendChild(child); |
| + return child; |
| +} |
| + |
| +/** |
| + * Removes a node from its parent. |
| + * @param {Node} node The node to remove. |
| + */ |
| +function removeNode(node) { |
| + node && node.parentNode && node.parentNode.removeChild(node); |
| +} |
| + |
| +/** |
| + * Removes all the child nodes on a DOM node. |
| + * @param {Node} node Node to remove children from. |
| + */ |
| +function removeChildren(node) { |
| + var child; |
| + while ((child = node.firstChild)) { |
| + node.removeChild(child); |
| + } |
| +} |
| + |
| +/** |
| + * Adds or removes a class depending on the enabled argument. |
| + * @param {Element} element DOM node to add or remove the class on. |
| + * @param {string} className Class name to add or remove. |
| + * @param {boolean} enabled Whether to add or remove the class (true adds, |
| + * false removes). |
| + */ |
| +function enable(element, className, enabled) { |
| + if (enabled) |
| + element.classList.add(className); |
| + else |
| + element.classList.remove(className); |
| +} |
| + |
| +/** |
| + * @return {Object} the handle to the embeddedSearch API. |
| + */ |
| +function getEmbeddedSearchApiHandle() { |
| + if (window.cideb) |
| + return window.cideb; |
| + if (window.navigator && window.navigator.embeddedSearch) |
| + return window.navigator.embeddedSearch; |
| + if (window.chrome && window.chrome.embeddedSearch) |
| + return window.chrome.embeddedSearch; |
| + return null; |
| +} |
| + |
| +/** |
| + * Prepares the New Tab Page by adding listeners, rendering the current |
| + * theme, and the most visited pages section. |
| + */ |
| +function init() { |
| + topMarginElement = document.getElementById('top-margin'); |
| + tilesContainer = document.getElementById('mv-tiles'); |
| + notification = document.getElementById('mv-noti'); |
| + |
| + // TODO(jeremycho): i18n. |
| + var notificationMessage = document.getElementById('mv-msg'); |
| + notificationMessage.innerText = 'Thumbnail removed.'; |
| + var undoLink = document.getElementById('mv-undo'); |
| + undoLink.addEventListener('click', onUndo); |
| + undoLink.innerText = 'Undo'; |
| + var restoreAllLink = document.getElementById('mv-restore'); |
| + restoreAllLink.addEventListener('click', onRestoreAll); |
| + restoreAllLink.innerText = 'Restore all'; |
| + var notificationCloseButton = document.getElementById('mv-noti-x'); |
| + notificationCloseButton.addEventListener('click', hideNotification); |
| + |
| + window.addEventListener('resize', onResize); |
| + onResize(); |
| + |
| + var topLevelHandle = getEmbeddedSearchApiHandle(); |
| + // This is to inform Chrome that the NTP is instant-extended capable. |
| + topLevelHandle.searchBox.onsubmit = function() {}; |
| + |
| + apiHandle = topLevelHandle.newTabPage; |
| + apiHandle.onthemechange = onThemeChange; |
| + apiHandle.onmostvisitedchange = onMostVisitedChange; |
| + |
| + onThemeChange(); |
| + onMostVisitedChange(); |
| +} |
| + |
| +document.addEventListener('DOMContentLoaded', init); |