Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / local_ntp / local_ntp.js
index fb8b873..26bf34e 100644 (file)
@@ -14,6 +14,8 @@
  */
 function LocalNTP() {
 <include src="../../../../ui/webui/resources/js/assert.js">
+<include src="local_ntp_design.js">
+<include src="local_ntp_util.js">
 <include src="window_disposition_util.js">
 
 
@@ -26,12 +28,16 @@ var CLASSES = {
   ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
   BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation
   BLACKLIST_BUTTON: 'mv-x',
+  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',
   HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation
   HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo',
   HIDE_NOTIFICATION: 'mv-notice-hide',
@@ -39,11 +45,12 @@ var CLASSES = {
   NON_GOOGLE_PAGE: 'non-google-page',
   PAGE: 'mv-page', // page tiles
   PAGE_READY: 'mv-page-ready',  // page tile when ready
-  ROW: 'mv-row',  // tile row
   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'
 };
 
@@ -59,6 +66,7 @@ var IDS = {
   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',
@@ -102,6 +110,13 @@ 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}
  */
@@ -161,6 +176,13 @@ var isBlacklisting = false;
 
 
 /**
+ * Stores whether the current theme has a dark background.
+ * @type {boolean}
+ */
+var isBackgroundDark = false;
+
+
+/**
  * Current number of tiles columns shown based on the window width, including
  * those that just contain filler.
  * @type {number}
@@ -169,8 +191,8 @@ var numColumnsShown = 0;
 
 
 /**
- * True if the user initiated the current most visited change and false
- * otherwise.
+ * 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;
@@ -204,22 +226,6 @@ var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
 
 
-/**
- * Total tile width. Should be equal to mv-tile's width + 2 * border-width.
- * @private {number}
- * @const
- */
-var TILE_WIDTH = 140;
-
-
-/**
- * Margin between tiles. Should be equal to mv-tile's -webkit-margin-start.
- * @private {number}
- * @const
- */
-var TILE_MARGIN_START = 20;
-
-
 /** @type {number} @const */
 var MAX_NUM_TILES_TO_SHOW = 8;
 
@@ -262,30 +268,6 @@ var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
 
 
 /**
- * The hex color for most visited tile elements.
- * @type {string}
- * @const
- */
-var MOST_VISITED_COLOR = '777777';
-
-
-/**
- * The font family for most visited tile elements.
- * @type {string}
- * @const
- */
-var MOST_VISITED_FONT_FAMILY = 'arial, sans-serif';
-
-
-/**
- * The font size for most visited tile elements.
- * @type {number}
- * @const
- */
-var MOST_VISITED_FONT_SIZE = 11;
-
-
-/**
  * Hide most visited tiles for at most this many milliseconds while painting.
  * @type {number}
  * @const
@@ -298,27 +280,59 @@ var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
  * 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_rid) {
+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;
 }
 
 
 /**
+ * Determines whether a theme should be considered to have dark background.
+ * @param {ThemeBackgroundInfo} info Theme background information.
+ * @return {boolean} Whether the theme has dark background.
+ * @private
+ */
+function getIsBackgroundDark(info) {
+  if (info.imageUrl)
+    return true;
+  var rgba = info.backgroundColorRgba;
+  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 onThemeChange() {
+function renderTheme() {
   var info = ntpApiHandle.themeBackgroundInfo;
-  if (!info)
+  if (!info) {
+    isBackgroundDark = false;
     return;
+  }
+
+  isBackgroundDark = getIsBackgroundDark(info);
+  ntpContents.classList.toggle(CLASSES.DARK, isBackgroundDark);
 
   var background = [convertToRGBAColor(info.backgroundColorRgba),
                     info.imageUrl,
@@ -329,7 +343,17 @@ function onThemeChange() {
   document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
   updateThemeAttribution(info.attributionUrl);
   setCustomThemeStyle(info);
-  renderTiles();
+}
+
+
+/**
+ * Updates the NTP based on the current theme, then rerenders all tiles.
+ * @private
+ */
+function onThemeChange() {
+  renderTheme();
+  tilesContainer.innerHTML = '';
+  renderAndShowTiles();
 }
 
 
@@ -344,6 +368,7 @@ function setCustomThemeStyle(opt_themeInfo) {
   var head = document.head;
 
   if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
+    ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
     var themeStyle =
       '#attribution {' +
       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
@@ -358,11 +383,11 @@ function setCustomThemeStyle(opt_themeInfo) {
       '  -webkit-filter: drop-shadow(0 0 0 ' +
           convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
       '}' +
-      '.mv-page-ready {' +
+      '.mv-page-ready .mv-mask {' +
       '  border: 1px solid ' +
-        convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
+          convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
       '}' +
-      '.mv-page-ready:hover, .mv-page-ready:focus {' +
+      '.mv-page-ready:hover .mv-mask, .mv-page-ready:focus .mv-mask {' +
       '  border-color: ' +
           convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
       '}';
@@ -377,8 +402,10 @@ function setCustomThemeStyle(opt_themeInfo) {
       head.appendChild(customStyleElement);
     }
 
-  } else if (customStyleElement) {
-    head.removeChild(customStyleElement);
+  } else {
+    ntpContents.classList.add(CLASSES.DEFAULT_THEME);
+    if (customStyleElement)
+      head.removeChild(customStyleElement);
   }
 }
 
@@ -432,89 +459,166 @@ function convertToRGBAColor(color) {
  * Handles a new set of Most Visited page data.
  */
 function onMostVisitedChange() {
-  var pages = ntpApiHandle.mostVisited;
-
   if (isBlacklisting) {
-    // Trigger the blacklist animation and re-render the tiles when it
-    // completes.
-    var lastBlacklistedTileElement = lastBlacklistedTile.elem;
-    lastBlacklistedTileElement.addEventListener(
+    // Trigger the blacklist animation, which then triggers reloadAllTiles().
+    var lastBlacklistedTileElem = lastBlacklistedTile.elem;
+    lastBlacklistedTileElem.addEventListener(
         'webkitTransitionEnd', blacklistAnimationDone);
-    lastBlacklistedTileElement.classList.add(CLASSES.BLACKLIST);
-
+    lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST);
   } else {
-    // Otherwise render the tiles using the new data without animation.
-    tiles = [];
-    for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) {
-      tiles.push(createTile(pages[i], i));
-    }
-    if (!userInitiatedMostVisitedChange) {
-      tilesContainer.hidden = true;
-      window.setTimeout(function() {
-        if (tilesContainer) {
-          tilesContainer.hidden = false;
-        }
-      }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
-    }
-    renderTiles();
+    reloadAllTiles();
   }
 }
 
 
 /**
- * Renders the current set of tiles.
+ * Handles the end of the blacklist animation by showing the notification and
+ * re-rendering the new set of tiles.
  */
-function renderTiles() {
-  var rows = tilesContainer.children;
-  for (var i = 0; i < rows.length; ++i) {
-    removeChildren(rows[i]);
-  }
+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;
 
-  for (var i = 0, length = tiles.length;
-       i < Math.min(length, numColumnsShown * NUM_ROWS); ++i) {
-    rows[Math.floor(i / numColumnsShown)].appendChild(tiles[i].elem);
+  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 <iframe> elements.
+ * @param {Tile} tile The main tile to bind events to.
+ * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible
+ *   the moment all tiles are loaded.
+ */
+function bindTileOnloadEvents(tile, tileVisibilityBarrier) {
+  if (tile.titleElem) {
+    tileVisibilityBarrier.add();
+    tile.titleElem.onload = function() {
+      tileVisibilityBarrier.remove();
+    };
+  }
+  if (tile.thumbnailElem) {
+    tileVisibilityBarrier.add();
+    tile.thumbnailElem.onload = function() {
+      tile.elem.classList.add(CLASSES.PAGE_READY);
+      tileVisibilityBarrier.remove();
+    };
   }
 }
 
 
 /**
- * Shows most visited tiles if all child iframes are loaded, and hides them
- * otherwise.
+ * Renders the current list of visible tiles to DOM, and hides tiles that are
+ * already in the DOM but should not be seen.
  */
-function updateMostVisitedVisibility() {
-  var iframes = tilesContainer.querySelectorAll('iframe');
-  var ready = true;
-  for (var i = 0, numIframes = iframes.length; i < numIframes; i++) {
-    if (iframes[i].hidden) {
-      ready = false;
-      break;
+function renderAndShowTiles() {
+  var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length;
+  // Only add visible tiles to the DOM, to avoid creating invisible tiles that
+  // produce meaningless impression metrics. However, if a tile becomes
+  // invisible then we leave it in DOM to prevent reload if it's shown again.
+  var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS);
+
+  // If we need to render new tiles, manage the visibility to hide intermediate
+  // load states of the <iframe>s.
+  if (numExisting < numDesired) {
+    var showAll = function() {
+      for (var i = 0; i < numDesired; ++i) {
+        if (tiles[i].titleElem || tiles[i].thumbnailElem)
+          tiles[i].elem.classList.add(CLASSES.PAGE_READY);
+      }
+    };
+    var tileVisibilityBarrier = new Barrier(showAll);
+
+    if (!userInitiatedMostVisitedChange) {
+      // Make titleContainer invisible, but still taking up space.
+      // titleContainer becomes visible again (1) on timeout, or (2) when all
+      // tiles finish loading (using tileVisibilityBarrier).
+      window.setTimeout(function() {
+        tileVisibilityBarrier.cancel();
+        showAll();
+      }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
     }
-  }
-  if (ready) {
-    tilesContainer.hidden = false;
     userInitiatedMostVisitedChange = false;
+
+    for (var i = numExisting; i < numDesired; ++i) {
+      bindTileOnloadEvents(tiles[i], tileVisibilityBarrier);
+      tilesContainer.appendChild(tiles[i].elem);
+    }
   }
+
+  // Show only the desired tiles. Note that .hidden does not work for
+  // inline-block elements like tiles[i].elem.
+  for (var i = 0; i < numDesired; ++i)
+    tiles[i].elem.style.display = 'inline-block';
+  // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs
+  // when window is downsized).
+  for (; i < numExisting; ++i)
+    tiles[i].elem.style.display = 'none';
 }
 
 
 /**
- * Builds a URL to display a most visited tile component in an iframe.
- * @param {string} filename The desired most visited component filename.
+ * Builds a URL to display a most visited tile title in an iframe.
  * @param {number} rid The restricted ID.
- * @param {string} color The text color for text in the iframe.
- * @param {string} fontFamily The font family for text in the iframe.
- * @param {number} fontSize The font size for text in the iframe.
  * @param {number} position The position of the iframe in the UI.
- * @return {string} An URL to display the most visited component in an iframe.
- */
-function getMostVisitedIframeUrl(filename, rid, color, fontFamily, fontSize,
-    position) {
-  return 'chrome-search://most-visited/' + encodeURIComponent(filename) + '?' +
-      ['rid=' + encodeURIComponent(rid),
-       'c=' + encodeURIComponent(color),
-       'f=' + encodeURIComponent(fontFamily),
-       'fs=' + encodeURIComponent(fontSize),
-       'pos=' + encodeURIComponent(position)].join('&');
+ * @return {string} An URL to display the most visited title in an iframe.
+ */
+function getMostVisitedTitleIframeUrl(rid, position) {
+  var url = 'chrome-search://most-visited/' +
+      encodeURIComponent(MOST_VISITED_TITLE_IFRAME);
+  var titleColor = isBackgroundDark ? NTP_DESIGN.titleColorAgainstDark :
+      NTP_DESIGN.titleColor;
+  var params = [
+      'rid=' + encodeURIComponent(rid),
+      'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
+      'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
+      'c=' + encodeURIComponent(titleColor),
+      'pos=' + encodeURIComponent(position)];
+  if (NTP_DESIGN.titleTextAlign)
+    params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign));
+  if (NTP_DESIGN.titleTextFade)
+    params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade));
+  return url + '?' + params.join('&');
+}
+
+
+/**
+ * Builds a URL to display a most visited tile thumbnail in an iframe.
+ * @param {number} rid The restricted ID.
+ * @param {number} position The position of the iframe in the UI.
+ * @return {string} An URL to display the most visited thumbnail in an iframe.
+ */
+function getMostVisitedThumbnailIframeUrl(rid, position) {
+  var url = 'chrome-search://most-visited/' +
+      encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME);
+  var params = [
+      'rid=' + encodeURIComponent(rid),
+      'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
+      'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
+      'c=' + encodeURIComponent(NTP_DESIGN.thumbnailTextColor),
+      'pos=' + encodeURIComponent(position)];
+  if (NTP_DESIGN.thumbnailFallback)
+    params.push('etfb=1');
+  return url + '?' + params.join('&');
 }
 
 
@@ -526,12 +630,13 @@ function getMostVisitedIframeUrl(filename, rid, color, fontFamily, fontSize,
  * @return {Tile} The new Tile.
  */
 function createTile(page, position) {
-  var tileElement = document.createElement('div');
-  tileElement.classList.add(CLASSES.TILE);
+  var tileElem = document.createElement('div');
+  tileElem.classList.add(CLASSES.TILE);
+  var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER);
 
   if (page) {
     var rid = page.rid;
-    tileElement.classList.add(CLASSES.PAGE);
+    tileElem.classList.add(CLASSES.PAGE);
 
     var navigateFunction = function(e) {
       e.preventDefault();
@@ -539,15 +644,15 @@ function createTile(page, position) {
     };
 
     // The click handler for navigating to the page identified by the RID.
-    tileElement.addEventListener('click', navigateFunction);
+    tileElem.addEventListener('click', navigateFunction);
 
     // Make thumbnails tab-accessible.
-    tileElement.setAttribute('tabindex', '1');
-    registerKeyHandler(tileElement, KEYCODE.ENTER, navigateFunction);
+    tileElem.setAttribute('tabindex', '1');
+    registerKeyHandler(tileElem, KEYCODE.ENTER, navigateFunction);
 
     // The iframe which renders the page title.
-    var titleElement = document.createElement('iframe');
-    titleElement.tabIndex = '-1';
+    var titleElem = document.createElement('iframe');
+    titleElem.tabIndex = '-1';
 
     // Why iframes have IDs:
     //
@@ -563,62 +668,54 @@ function createTile(page, position) {
     //
     // TODO(jered): Find and fix the root (probably Blink) bug.
 
-    titleElement.src = getMostVisitedIframeUrl(
-        MOST_VISITED_TITLE_IFRAME, rid, MOST_VISITED_COLOR,
-        MOST_VISITED_FONT_FAMILY, MOST_VISITED_FONT_SIZE, position);
-
-    // Keep this id here. See comment above.
-    titleElement.id = 'title-' + rid;
-    titleElement.hidden = true;
-    titleElement.onload = function() {
-      titleElement.hidden = false;
-      updateMostVisitedVisibility();
-    };
-    titleElement.className = CLASSES.TITLE;
-    tileElement.appendChild(titleElement);
+    // Keep this ID here. See comment above.
+    titleElem.id = 'title-' + rid;
+    titleElem.className = CLASSES.TITLE;
+    titleElem.src = getMostVisitedTitleIframeUrl(rid, position);
+    innerElem.appendChild(titleElem);
+
+    // A fallback element for missing thumbnails.
+    if (NTP_DESIGN.thumbnailFallback) {
+      var fallbackElem = createAndAppendElement(
+          innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK);
+      if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT)
+        createAndAppendElement(fallbackElem, 'div', CLASSES.DOT);
+    }
 
     // The iframe which renders either a thumbnail or domain element.
-    var thumbnailElement = document.createElement('iframe');
-    thumbnailElement.tabIndex = '-1';
-    thumbnailElement.src = getMostVisitedIframeUrl(
-        MOST_VISITED_THUMBNAIL_IFRAME, rid, MOST_VISITED_COLOR,
-        MOST_VISITED_FONT_FAMILY, MOST_VISITED_FONT_SIZE, position);
-
-    // Keep this id here. See comment above.
-    thumbnailElement.id = 'thumb-' + rid;
-    thumbnailElement.hidden = true;
-    thumbnailElement.onload = function() {
-      thumbnailElement.hidden = false;
-      tileElement.classList.add(CLASSES.PAGE_READY);
-      updateMostVisitedVisibility();
-    };
-    thumbnailElement.className = CLASSES.THUMBNAIL;
-    tileElement.appendChild(thumbnailElement);
-
-    // A mask to darken the thumbnail on focus.
-    var maskElement = createAndAppendElement(
-        tileElement, 'div', CLASSES.THUMBNAIL_MASK);
+    var thumbnailElem = document.createElement('iframe');
+    thumbnailElem.tabIndex = '-1';
+    // Keep this ID here. See comment above.
+    thumbnailElem.id = 'thumb-' + rid;
+    thumbnailElem.className = CLASSES.THUMBNAIL;
+    thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position);
+    innerElem.appendChild(thumbnailElem);
 
     // The button used to blacklist this page.
     var blacklistButton = createAndAppendElement(
-        tileElement, 'div', CLASSES.BLACKLIST_BUTTON);
+        innerElem, 'div', CLASSES.BLACKLIST_BUTTON);
     var blacklistFunction = generateBlacklistFunction(rid);
     blacklistButton.addEventListener('click', blacklistFunction);
     blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
 
+    // A helper mask on top of the tile that is used to create hover border
+    // and/or to darken the thumbnail on focus.
+    var maskElement = createAndAppendElement(
+        innerElem, 'div', CLASSES.THUMBNAIL_MASK);
+
     // When a tile is focused, have delete also blacklist the page.
-    registerKeyHandler(tileElement, KEYCODE.DELETE, blacklistFunction);
-
-    // The page favicon, if any.
-    var faviconUrl = page.faviconUrl;
-    if (faviconUrl) {
-      var favicon = createAndAppendElement(
-          tileElement, 'div', CLASSES.FAVICON);
-      favicon.style.backgroundImage = 'url(' + faviconUrl + ')';
+    registerKeyHandler(tileElem, KEYCODE.DELETE, blacklistFunction);
+
+    // The page favicon, or a fallback.
+    var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON);
+    if (page.faviconUrl) {
+      favicon.style.backgroundImage = 'url(' + page.faviconUrl + ')';
+    } else {
+      favicon.classList.add(CLASSES.FAVICON_FALLBACK);
     }
-    return new Tile(tileElement, rid);
+    return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid);
   } else {
-    return new Tile(tileElement);
+    return new Tile(tileElem);
   }
 }
 
@@ -664,23 +761,6 @@ function hideNotification() {
 
 
 /**
- * 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.
-  onMostVisitedChange();
-}
-
-
-/**
  * Handles a click on the notification undo link by hiding the notification and
  * informing Chrome.
  */
@@ -705,39 +785,35 @@ function onRestoreAll() {
 
 
 /**
- * Re-renders the tiles if the number of columns has changed.  As a temporary
- * fix for crbug/240510, updates the width of the fakebox and most visited tiles
- * container.
+ * Resizes elements because the number of tile columns may need to change in
+ * response to resizing. Also shows or hides extra tiles tiles according to the
+ * new width of the page.
  */
 function onResize() {
+  var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
   // If innerWidth is zero, then use the maximum snap size.
-  var innerWidth = window.innerWidth || 820;
-
-  // These values should remain in sync with local_ntp.css.
-  // TODO(jeremycho): Delete once the root cause of crbug/240510 is resolved.
-  var setWidths = function(tilesContainerWidth) {
-    tilesContainer.style.width = tilesContainerWidth + 'px';
-    if (fakebox)
-      fakebox.style.width = (tilesContainerWidth - 2) + 'px';
-  };
-  if (innerWidth >= 820)
-    setWidths(620);
-  else if (innerWidth >= 660)
-    setWidths(460);
-  else
-    setWidths(300);
-
-  var tileRequiredWidth = TILE_WIDTH + TILE_MARGIN_START;
-  // Adds margin-start to the available width to compensate the extra margin
-  // counted above for the first tile (which does not have a margin-start).
-  var availableWidth = innerWidth + TILE_MARGIN_START -
+  var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
+      NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
+  var innerWidth = window.innerWidth || maxSnapSize;
+  // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
+  var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
       MIN_TOTAL_HORIZONTAL_PADDING;
-  var numColumnsToShow = Math.floor(availableWidth / tileRequiredWidth);
-  numColumnsToShow = Math.max(MIN_NUM_COLUMNS,
-                              Math.min(MAX_NUM_COLUMNS, numColumnsToShow));
-  if (numColumnsToShow != numColumnsShown) {
-    numColumnsShown = numColumnsToShow;
-    renderTiles();
+  var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
+  if (newNumColumns < MIN_NUM_COLUMNS)
+    newNumColumns = MIN_NUM_COLUMNS;
+  else if (newNumColumns > MAX_NUM_COLUMNS)
+    newNumColumns = MAX_NUM_COLUMNS;
+
+  if (numColumnsShown != newNumColumns) {
+    numColumnsShown = newNumColumns;
+    var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
+    tilesContainer.style.width = tilesContainerWidth + 'px';
+    if (fakebox) {
+      fakebox.style.width =  // -2 to account for border.
+          (tilesContainerWidth - NTP_DESIGN.tileMargin - 2) + 'px';
+    }
+    // Render without clearing tiles.
+    renderAndShowTiles();
   }
 }
 
@@ -880,15 +956,6 @@ function removeNode(node) {
 
 
 /**
- * Removes all the child nodes on a DOM node.
- * @param {Node} node Node to remove children from.
- */
-function removeChildren(node) {
-  node.innerHTML = '';
-}
-
-
-/**
  * @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.
@@ -924,22 +991,22 @@ function init() {
   attribution = $(IDS.ATTRIBUTION);
   ntpContents = $(IDS.NTP_CONTENTS);
 
-  for (var i = 0; i < NUM_ROWS; i++) {
-    var row = document.createElement('div');
-    row.classList.add(CLASSES.ROW);
-    tilesContainer.appendChild(row);
-  }
-
   if (configData.isGooglePage) {
     var logo = document.createElement('div');
     logo.id = IDS.LOGO;
 
     fakebox = document.createElement('div');
     fakebox.id = IDS.FAKEBOX;
-    fakebox.innerHTML =
-        '<input id="' + IDS.FAKEBOX_INPUT +
-            '" autocomplete="off" tabindex="-1" aria-hidden="true">' +
-        '<div id=cursor></div>';
+    var fakeboxHtml = [];
+    fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
+        '" autocomplete="off" tabindex="-1" aria-hidden="true">');
+    if (NTP_DESIGN.showFakeboxHint &&
+        configData.translatedStrings.searchboxPlaceholder) {
+      fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '">' +
+          configData.translatedStrings.searchboxPlaceholder + '</div>');
+    }
+    fakeboxHtml.push('<div id="cursor"></div>');
+    fakebox.innerHTML = fakeboxHtml.join('');
 
     ntpContents.insertBefore(fakebox, ntpContents.firstChild);
     ntpContents.insertBefore(logo, ntpContents.firstChild);
@@ -950,22 +1017,24 @@ function init() {
   var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
   notificationMessage.textContent =
       configData.translatedStrings.thumbnailRemovedNotification;
+
   var undoLink = $(IDS.UNDO_LINK);
   undoLink.addEventListener('click', onUndo);
   registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
   undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
+
   var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
   restoreAllLink.addEventListener('click', onRestoreAll);
   registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
   restoreAllLink.textContent =
       configData.translatedStrings.restoreThumbnailsShort;
+
   $(IDS.ATTRIBUTION_TEXT).textContent =
       configData.translatedStrings.attributionIntro;
 
   var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
   notificationCloseButton.addEventListener('click', hideNotification);
 
-  userInitiatedMostVisitedChange = false;
   window.addEventListener('resize', onResize);
   onResize();
 
@@ -981,7 +1050,7 @@ function init() {
   if (ntpApiHandle.isInputInProgress)
     onInputStart();
 
-  onThemeChange();
+  renderTheme();
   onMostVisitedChange();
 
   searchboxApiHandle = topLevelHandle.searchBox;
@@ -1024,6 +1093,7 @@ function init() {
 
   if (searchboxApiHandle.rtl) {
     $(IDS.NOTIFICATION).dir = 'rtl';
+    document.body.setAttribute('dir', 'rtl');
     // Add class for setting alignments based on language directionality.
     document.body.classList.add(CLASSES.RTL);
     $(IDS.TILES).dir = 'rtl';