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..378e70c655b05243102d4ea11533c7a2a3e1d983 |
| --- /dev/null |
| +++ b/chrome/browser/resources/local_ntp/local_ntp.js |
| @@ -0,0 +1,545 @@ |
| +// 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. |
| + |
| +(function() { |
| +/** |
| + * 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} |
|
Dan Beam
2013/03/25 18:13:49
@type {number}
jeremycho
2013/03/25 21:27:16
Done.
|
| + */ |
| +var notificationTimer = null; |
|
Dan Beam
2013/03/25 18:13:49
s/null/0/
jeremycho
2013/03/25 21:27:16
Done.
|
| + |
| +/** |
| + * 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 |
| + * 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)']; |
| + |
| +/** |
| + * 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} |
| + * @const |
| + */ |
| +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 |
| + */ |
| +var MIN_TOTAL_HORIZONTAL_PADDING = 188; |
| + |
| +/** |
| + * Enum for classnames. |
| + * @enum {string} |
| + * @const |
| + */ |
| +var CLASSES = { |
| + TILE: 'mv-tile', |
| + PAGE: 'mv-page', // page tiles |
| + TITLE: 'mv-title', |
| + THUMBNAIL: 'mv-thumb', |
| + DOMAIN: 'mv-domain', |
| + BLACKLIST_BUTTON: 'mv-x', |
| + FAVICON: 'mv-favicon', |
| + FILLER: 'mv-filler', // filler tiles |
| + BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation |
| + HIDE_TILE: 'mv-tile-hide', // hides tiles on small browser width |
| + HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation |
| + HIDE_NOTIFICATION: 'mv-notice-hide' |
| +}; |
| + |
| +/** |
| + * Enum for ids. |
| + * @enum {string} |
| + * @const |
| + */ |
| +var IDS = { |
| + TOP_MARGIN: 'mv-top-margin', |
| + TILES: 'mv-tiles', |
| + NOTIFICATION: 'mv-notice', |
| + NOTIFICATION_MESSAGE: 'mv-msg', |
| + UNDO_LINK: 'mv-undo', |
| + RESTORE_ALL_LINK: 'mv-restore', |
| + NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x' |
| +}; |
| + |
| +/** |
| + * 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 |
| + */ |
| +function Tile(elem, opt_rid) { |
| + /** @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(CLASSES.BLACKLIST); |
| + // 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) { |
|
Dan Beam
2013/03/25 18:13:49
} else if (isUndoing) {
// If this was called as
jeremycho
2013/03/25 21:27:16
Done.
|
| + tiles.splice( |
| + lastBlacklistedIndex, 0, lastBlacklistedTile); |
| + var lastBlacklistedTileElement = lastBlacklistedTile.elem; |
| + tilesContainer.insertBefore( |
| + lastBlacklistedTileElement, |
| + tilesContainer.childNodes[lastBlacklistedIndex]); |
| + lastBlacklistedTileElement.addEventListener( |
| + 'webkitTransitionEnd', undoAnimationDone); |
| + // Force the removal to happen synchronously. |
| + lastBlacklistedTileElement.scrollTop; |
| + lastBlacklistedTileElement.classList.remove(CLASSES.BLACKLIST); |
| + // Otherwise render the tiles using the new data without animation. |
| + } else { |
|
Dan Beam
2013/03/25 18:13:49
} else {
// Otherwise render the tiles using the
jeremycho
2013/03/25 21:27:16
Done.
|
| + 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(CLASSES.TILE); |
| + |
| + if (page) { |
| + var rid = page.rid; |
| + tileElement.classList.add(CLASSES.PAGE); |
| + |
| + // 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(CLASSES.TITLE); |
| + 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(CLASSES.DOMAIN); |
| + tileElement.appendChild(domainElement); |
| + } |
| + }; |
| + if (thumbnailUrl) { |
| + var image = new Image(); |
| + image.onload = function() { |
| + var thumbnailElement = createAndAppendElement( |
| + tileElement, 'div', CLASSES.THUMBNAIL); |
| + thumbnailElement.style.backgroundImage = 'url(' + thumbnailUrl + ')'; |
| + }; |
| + |
| + image.onerror = showDomainElement; |
| + image.src = thumbnailUrl; |
| + } else { |
| + showDomainElement(); |
| + } |
| + |
| + // The button used to blacklist this page. |
| + var blacklistButton = createAndAppendElement( |
| + tileElement, 'div', CLASSES.BLACKLIST_BUTTON); |
| + blacklistButton.addEventListener('click', generateBlacklistFunction(rid)); |
| + // TODO(jeremycho): i18n. See crbug/190223. |
|
Dan Beam
2013/03/25 18:13:49
1 \s between sentences, why not use real URL here?
jeremycho
2013/03/25 21:27:16
Done.
|
| + blacklistButton.title = "Don't show on this page"; |
| + |
| + // The page favicon, if any. |
| + var faviconUrl = page.faviconUrl; |
| + if (faviconUrl) { |
| + var favicon = createAndAppendElement( |
| + tileElement, 'div', CLASSES.FAVICON); |
| + favicon.style['background-image'] = 'url(' + faviconUrl + ')'; |
|
Dan Beam
2013/03/25 18:13:49
always use style.styleProperty rather than style['
jeremycho
2013/03/25 21:27:16
Done.
|
| + } |
| + return new Tile(tileElement, rid); |
| + } else { |
| + tileElement.classList.add(CLASSES.FILLER); |
| + 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(CLASSES.HIDE_BLACKLIST_BUTTON); |
| + 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(CLASSES.HIDE_NOTIFICATION); |
| + if (notificationTimer) |
| + window.clearTimeout(notificationTimer); |
| + notificationTimer = window.setTimeout( |
|
Dan Beam
2013/03/25 18:13:49
^ you might be able to use CSS to do this, i.e.
jeremycho
2013/03/25 21:27:16
Done.
On 2013/03/25 18:13:49, Dan Beam wrote:
|
| + hideNotification, NOTIFICATION_TIMEOUT); |
| +} |
| + |
| +/** |
| + * Hides the blacklist notification. |
| + */ |
| +function hideNotification() { |
| + notification.classList.add(CLASSES.HIDE_NOTIFICATION); |
|
Dan Beam
2013/03/25 18:13:49
notificationTimer = 0;
jeremycho
2013/03/25 21:27:16
Removed timer.
On 2013/03/25 18:13:49, Dan Beam w
|
| +} |
| + |
| +/** |
| + * 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(CLASSES.HIDE_BLACKLIST_BUTTON); |
| + 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 != 'undefined') { |
|
Dan Beam
2013/03/25 18:13:49
anytime I see a comparison to the string 'undefine
jeremycho
2013/03/25 21:27:16
Done, to allow lastBlacklistedRID == 0.
|
| + 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.marginTop = |
| + 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, CLASSES.HIDE_TILE, 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); |
|
Dan Beam
2013/03/25 18:13:49
if (node && node.parentNode)
node.parentNode.rem
Dan Beam
2013/03/25 18:13:49
why is this being called when disconnected or null
jeremycho
2013/03/25 21:27:16
It shouldn't be. Removed check.
jeremycho
2013/03/25 21:27:16
Removed check.
|
| +} |
| + |
| +/** |
| + * Removes all the child nodes on a DOM node. |
| + * @param {Node} node Node to remove children from. |
| + */ |
| +function removeChildren(node) { |
| + node.innerHTML = ''; |
| +} |
| + |
| +/** |
| + * 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) { |
|
Dan Beam
2013/03/25 18:13:49
^ what's the benefit of this method
jeremycho
2013/03/25 21:27:16
Removed.
|
| + element.classList.toggle(className, enabled); |
|
Dan Beam
2013/03/25 18:13:49
^ if you already have this?
jeremycho
2013/03/25 21:27:16
Done.
|
| +} |
| + |
| +/** |
| + * @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; |
|
Dan Beam
2013/03/25 18:13:49
add this when it works
jeremycho
2013/03/25 21:27:16
Done.
|
| + 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(IDS.TOP_MARGIN); |
| + tilesContainer = document.getElementById(IDS.TILES); |
| + notification = document.getElementById(IDS.NOTIFICATION); |
| + |
| + // TODO(jeremycho): i18n. |
| + var notificationMessage = document.getElementById(IDS.NOTIFICATION_MESSAGE); |
| + notificationMessage.innerText = 'Thumbnail removed.'; |
| + var undoLink = document.getElementById(IDS.UNDO_LINK); |
| + undoLink.addEventListener('click', onUndo); |
| + undoLink.innerText = 'Undo'; |
| + var restoreAllLink = document.getElementById(IDS.RESTORE_ALL_LINK); |
| + restoreAllLink.addEventListener('click', onRestoreAll); |
| + restoreAllLink.innerText = 'Restore all'; |
| + var notificationCloseButton = |
| + document.getElementById(IDS.NOTIFICATION_CLOSE_BUTTON); |
| + notificationCloseButton.addEventListener('click', hideNotification); |
| + |
| + window.addEventListener('resize', onResize); |
| + onResize(); |
| + |
| + var topLevelHandle = getEmbeddedSearchApiHandle(); |
| + // This is to inform Chrome that the NTP is instant-extended capable i.e. |
| + // it should fire events like onmostvisitedchange. |
| + topLevelHandle.searchBox.onsubmit = function() {}; |
| + |
| + apiHandle = topLevelHandle.newTabPage; |
| + apiHandle.onthemechange = onThemeChange; |
| + apiHandle.onmostvisitedchange = onMostVisitedChange; |
| + |
| + onThemeChange(); |
| + onMostVisitedChange(); |
| +} |
| + |
| +document.addEventListener('DOMContentLoaded', init); |
| +})(); |