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); |