Home | History | Annotate | Download | only in local_ntp
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 
      6 /**
      7  * @fileoverview The local InstantExtended NTP.
      8  */
      9 
     10 
     11 /**
     12  * Controls rendering the new tab page for InstantExtended.
     13  * @return {Object} A limited interface for testing the local NTP.
     14  */
     15 function LocalNTP() {
     16 <include src="../../../../ui/webui/resources/js/assert.js">
     17 <include src="local_ntp_design.js">
     18 <include src="local_ntp_util.js">
     19 <include src="window_disposition_util.js">
     20 
     21 
     22 /**
     23  * Enum for classnames.
     24  * @enum {string}
     25  * @const
     26  */
     27 var CLASSES = {
     28   ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
     29   BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation
     30   BLACKLIST_BUTTON: 'mv-x',
     31   BLACKLIST_BUTTON_INNER: 'mv-x-inner',
     32   DARK: 'dark',
     33   DEFAULT_THEME: 'default-theme',
     34   DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
     35   DOT: 'dot',
     36   FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive
     37   FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox
     38   // Applies drag focus style to the fakebox
     39   FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
     40   FAVICON: 'mv-favicon',
     41   FAVICON_FALLBACK: 'mv-favicon-fallback',
     42   FOCUSED: 'mv-focused',
     43   HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation
     44   HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo',
     45   HIDE_NOTIFICATION: 'mv-notice-hide',
     46   // Vertically centers the most visited section for a non-Google provided page.
     47   NON_GOOGLE_PAGE: 'non-google-page',
     48   PAGE: 'mv-page', // page tiles
     49   PAGE_READY: 'mv-page-ready',  // page tile when ready
     50   RTL: 'rtl',  // Right-to-left language text.
     51   THUMBNAIL: 'mv-thumb',
     52   THUMBNAIL_FALLBACK: 'mv-thumb-fallback',
     53   THUMBNAIL_MASK: 'mv-mask',
     54   TILE: 'mv-tile',
     55   TILE_INNER: 'mv-tile-inner',
     56   TITLE: 'mv-title'
     57 };
     58 
     59 
     60 /**
     61  * Enum for HTML element ids.
     62  * @enum {string}
     63  * @const
     64  */
     65 var IDS = {
     66   ATTRIBUTION: 'attribution',
     67   ATTRIBUTION_TEXT: 'attribution-text',
     68   CUSTOM_THEME_STYLE: 'ct-style',
     69   FAKEBOX: 'fakebox',
     70   FAKEBOX_INPUT: 'fakebox-input',
     71   FAKEBOX_TEXT: 'fakebox-text',
     72   LOGO: 'logo',
     73   NOTIFICATION: 'mv-notice',
     74   NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x',
     75   NOTIFICATION_MESSAGE: 'mv-msg',
     76   NTP_CONTENTS: 'ntp-contents',
     77   RESTORE_ALL_LINK: 'mv-restore',
     78   TILES: 'mv-tiles',
     79   UNDO_LINK: 'mv-undo'
     80 };
     81 
     82 
     83 /**
     84  * Enum for keycodes.
     85  * @enum {number}
     86  * @const
     87  */
     88 var KEYCODE = {
     89   ENTER: 13
     90 };
     91 
     92 
     93 /**
     94  * Enum for the state of the NTP when it is disposed.
     95  * @enum {number}
     96  * @const
     97  */
     98 var NTP_DISPOSE_STATE = {
     99   NONE: 0,  // Preserve the NTP appearance and functionality
    100   DISABLE_FAKEBOX: 1,
    101   HIDE_FAKEBOX_AND_LOGO: 2
    102 };
    103 
    104 
    105 /**
    106  * The JavaScript button event value for a middle click.
    107  * @type {number}
    108  * @const
    109  */
    110 var MIDDLE_MOUSE_BUTTON = 1;
    111 
    112 
    113 /**
    114  * Specifications for the NTP design.
    115  * @const {NtpDesign}
    116  */
    117 var NTP_DESIGN = getNtpDesign(configData.ntpDesignName);
    118 
    119 
    120 /**
    121  * The container for the tile elements.
    122  * @type {Element}
    123  */
    124 var tilesContainer;
    125 
    126 
    127 /**
    128  * The notification displayed when a page is blacklisted.
    129  * @type {Element}
    130  */
    131 var notification;
    132 
    133 
    134 /**
    135  * The container for the theme attribution.
    136  * @type {Element}
    137  */
    138 var attribution;
    139 
    140 
    141 /**
    142  * The "fakebox" - an input field that looks like a regular searchbox.  When it
    143  * is focused, any text the user types goes directly into the omnibox.
    144  * @type {Element}
    145  */
    146 var fakebox;
    147 
    148 
    149 /**
    150  * The container for NTP elements.
    151  * @type {Element}
    152  */
    153 var ntpContents;
    154 
    155 
    156 /**
    157  * The array of rendered tiles, ordered by appearance.
    158  * @type {!Array.<Tile>}
    159  */
    160 var tiles = [];
    161 
    162 
    163 /**
    164  * The last blacklisted tile if any, which by definition should not be filler.
    165  * @type {?Tile}
    166  */
    167 var lastBlacklistedTile = null;
    168 
    169 
    170 /**
    171  * The iframe element which is currently keyboard focused, or null.
    172  * @type {?Element}
    173  */
    174 var focusedIframe = null;
    175 
    176 
    177 /**
    178  * True if a page has been blacklisted and we're waiting on the
    179  * onmostvisitedchange callback. See onMostVisitedChange() for how this
    180  * is used.
    181  * @type {boolean}
    182  */
    183 var isBlacklisting = false;
    184 
    185 
    186 /**
    187  * Current number of tiles columns shown based on the window width, including
    188  * those that just contain filler.
    189  * @type {number}
    190  */
    191 var numColumnsShown = 0;
    192 
    193 
    194 /**
    195  * A flag to indicate Most Visited changed caused by user action. If true, then
    196  * in onMostVisitedChange() tiles remain visible so no flickering occurs.
    197  * @type {boolean}
    198  */
    199 var userInitiatedMostVisitedChange = false;
    200 
    201 
    202 /**
    203  * The browser embeddedSearch.newTabPage object.
    204  * @type {Object}
    205  */
    206 var ntpApiHandle;
    207 
    208 
    209 /**
    210  * The browser embeddedSearch.searchBox object.
    211  * @type {Object}
    212  */
    213 var searchboxApiHandle;
    214 
    215 
    216 /**
    217  * The state of the NTP when a query is entered into the Omnibox.
    218  * @type {NTP_DISPOSE_STATE}
    219  */
    220 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
    221 
    222 
    223 /**
    224  * The state of the NTP when a query is entered into the Fakebox.
    225  * @type {NTP_DISPOSE_STATE}
    226  */
    227 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
    228 
    229 
    230 /** @type {number} @const */
    231 var MAX_NUM_TILES_TO_SHOW = 8;
    232 
    233 
    234 /** @type {number} @const */
    235 var MIN_NUM_COLUMNS = 2;
    236 
    237 
    238 /** @type {number} @const */
    239 var MAX_NUM_COLUMNS = 4;
    240 
    241 
    242 /** @type {number} @const */
    243 var NUM_ROWS = 2;
    244 
    245 
    246 /**
    247  * Minimum total padding to give to the left and right of the most visited
    248  * section. Used to determine how many tiles to show.
    249  * @type {number}
    250  * @const
    251  */
    252 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
    253 
    254 
    255 /**
    256  * The filename for a most visited iframe src which shows a page title.
    257  * @type {string}
    258  * @const
    259  */
    260 var MOST_VISITED_TITLE_IFRAME = 'title.html';
    261 
    262 
    263 /**
    264  * The filename for a most visited iframe src which shows a thumbnail image.
    265  * @type {string}
    266  * @const
    267  */
    268 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
    269 
    270 
    271 /**
    272  * The color of the title in RRGGBBAA format.
    273  * @type {?string}
    274  */
    275 var titleColor = null;
    276 
    277 
    278 /**
    279  * Hide most visited tiles for at most this many milliseconds while painting.
    280  * @type {number}
    281  * @const
    282  */
    283 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
    284 
    285 
    286 /**
    287  * A Tile is either a rendering of a Most Visited page or "filler" used to
    288  * pad out the section when not enough pages exist.
    289  *
    290  * @param {Element} elem The element for rendering the tile.
    291  * @param {Element=} opt_innerElem The element for contents of tile.
    292  * @param {Element=} opt_titleElem The element for rendering the title.
    293  * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail.
    294  * @param {number=} opt_rid The RID for the corresponding Most Visited page.
    295  *     Should only be left unspecified when creating a filler tile.
    296  * @constructor
    297  */
    298 function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) {
    299   /** @type {Element} */
    300   this.elem = elem;
    301 
    302   /** @type {Element|undefined} */
    303   this.innerElem = opt_innerElem;
    304 
    305   /** @type {Element|undefined} */
    306   this.titleElem = opt_titleElem;
    307 
    308   /** @type {Element|undefined} */
    309   this.thumbnailElem = opt_thumbnailElem;
    310 
    311   /** @type {number|undefined} */
    312   this.rid = opt_rid;
    313 }
    314 
    315 
    316 /**
    317  * Heuristic to determine whether a theme should be considered to be dark, so
    318  * the colors of various UI elements can be adjusted.
    319  * @param {ThemeBackgroundInfo|undefined} info Theme background information.
    320  * @return {boolean} Whether the theme is dark.
    321  * @private
    322  */
    323 function getIsThemeDark(info) {
    324   if (!info)
    325     return false;
    326   // Heuristic: light text implies dark theme.
    327   var rgba = info.textColorRgba;
    328   var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
    329   return luminance >= 128;
    330 }
    331 
    332 
    333 /**
    334  * Updates the NTP based on the current theme.
    335  * @private
    336  */
    337 function renderTheme() {
    338   var fakeboxText = $(IDS.FAKEBOX_TEXT);
    339   if (fakeboxText) {
    340     fakeboxText.innerHTML = '';
    341     if (NTP_DESIGN.showFakeboxHint &&
    342         configData.translatedStrings.searchboxPlaceholder) {
    343       fakeboxText.textContent =
    344           configData.translatedStrings.searchboxPlaceholder;
    345     }
    346   }
    347 
    348   var info = ntpApiHandle.themeBackgroundInfo;
    349   var isThemeDark = getIsThemeDark(info);
    350   ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
    351   if (!info) {
    352     titleColor = NTP_DESIGN.titleColor;
    353     return;
    354   }
    355 
    356   if (!info.usingDefaultTheme && info.textColorRgba) {
    357     titleColor = convertToRRGGBBAAColor(info.textColorRgba);
    358   } else {
    359     titleColor = isThemeDark ?
    360         NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor;
    361   }
    362 
    363   var background = [convertToRGBAColor(info.backgroundColorRgba),
    364                     info.imageUrl,
    365                     info.imageTiling,
    366                     info.imageHorizontalAlignment,
    367                     info.imageVerticalAlignment].join(' ').trim();
    368 
    369   document.body.style.background = background;
    370   document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
    371   updateThemeAttribution(info.attributionUrl);
    372   setCustomThemeStyle(info);
    373 }
    374 
    375 
    376 /**
    377  * Updates the NTP based on the current theme, then rerenders all tiles.
    378  * @private
    379  */
    380 function onThemeChange() {
    381   renderTheme();
    382   tilesContainer.innerHTML = '';
    383   renderAndShowTiles();
    384 }
    385 
    386 
    387 /**
    388  * Updates the NTP style according to theme.
    389  * @param {Object=} opt_themeInfo The information about the theme. If it is
    390  * omitted the style will be reverted to the default.
    391  * @private
    392  */
    393 function setCustomThemeStyle(opt_themeInfo) {
    394   var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
    395   var head = document.head;
    396   if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
    397     ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
    398     var themeStyle =
    399       '#attribution {' +
    400       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
    401       '}' +
    402       '#mv-msg {' +
    403       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
    404       '}' +
    405       '#mv-notice-links span {' +
    406       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
    407       '}' +
    408       '#mv-notice-x {' +
    409       '  -webkit-filter: drop-shadow(0 0 0 ' +
    410           convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
    411       '}' +
    412       '.mv-page-ready .mv-mask {' +
    413       '  border: 1px solid ' +
    414           convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
    415       '}' +
    416       '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
    417       '  border-color: ' +
    418           convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
    419       '}';
    420 
    421     if (customStyleElement) {
    422       customStyleElement.textContent = themeStyle;
    423     } else {
    424       customStyleElement = document.createElement('style');
    425       customStyleElement.type = 'text/css';
    426       customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
    427       customStyleElement.textContent = themeStyle;
    428       head.appendChild(customStyleElement);
    429     }
    430 
    431   } else {
    432     ntpContents.classList.add(CLASSES.DEFAULT_THEME);
    433     if (customStyleElement)
    434       head.removeChild(customStyleElement);
    435   }
    436 }
    437 
    438 
    439 /**
    440  * Renders the attribution if the URL is present, otherwise hides it.
    441  * @param {string} url The URL of the attribution image, if any.
    442  * @private
    443  */
    444 function updateThemeAttribution(url) {
    445   if (!url) {
    446     setAttributionVisibility_(false);
    447     return;
    448   }
    449 
    450   var attributionImage = attribution.querySelector('img');
    451   if (!attributionImage) {
    452     attributionImage = new Image();
    453     attribution.appendChild(attributionImage);
    454   }
    455   attributionImage.style.content = url;
    456   setAttributionVisibility_(true);
    457 }
    458 
    459 
    460 /**
    461  * Sets the visibility of the theme attribution.
    462  * @param {boolean} show True to show the attribution.
    463  * @private
    464  */
    465 function setAttributionVisibility_(show) {
    466   if (attribution) {
    467     attribution.style.display = show ? '' : 'none';
    468   }
    469 }
    470 
    471 
    472  /**
    473  * Converts an Array of color components into RRGGBBAA format.
    474  * @param {Array.<number>} color Array of rgba color components.
    475  * @return {string} Color string in RRGGBBAA format.
    476  * @private
    477  */
    478 function convertToRRGGBBAAColor(color) {
    479   return color.map(function(t) {
    480     return ('0' + t.toString(16)).slice(-2);  // To 2-digit, 0-padded hex.
    481   }).join('');
    482 }
    483 
    484 
    485  /**
    486  * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
    487  * @param {Array.<number>} color Array of rgba color components.
    488  * @return {string} CSS color in RGBA format.
    489  * @private
    490  */
    491 function convertToRGBAColor(color) {
    492   return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
    493                     color[3] / 255 + ')';
    494 }
    495 
    496 
    497 /**
    498  * Handles a new set of Most Visited page data.
    499  */
    500 function onMostVisitedChange() {
    501   if (isBlacklisting) {
    502     // Trigger the blacklist animation, which then triggers reloadAllTiles().
    503     var lastBlacklistedTileElem = lastBlacklistedTile.elem;
    504     lastBlacklistedTileElem.addEventListener(
    505         'webkitTransitionEnd', blacklistAnimationDone);
    506     lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST);
    507   } else {
    508     reloadAllTiles();
    509   }
    510 }
    511 
    512 
    513 /**
    514  * Handles the end of the blacklist animation by showing the notification and
    515  * re-rendering the new set of tiles.
    516  */
    517 function blacklistAnimationDone() {
    518   showNotification();
    519   isBlacklisting = false;
    520   tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
    521   lastBlacklistedTile.elem.removeEventListener(
    522       'webkitTransitionEnd', blacklistAnimationDone);
    523   // Need to call explicitly to re-render the tiles, since the initial
    524   // onmostvisitedchange issued by the blacklist function only triggered
    525   // the animation.
    526   reloadAllTiles();
    527 }
    528 
    529 
    530 /**
    531  * Fetches new data, creates, and renders tiles.
    532  */
    533 function reloadAllTiles() {
    534   var pages = ntpApiHandle.mostVisited;
    535 
    536   tiles = [];
    537   for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i)
    538     tiles.push(createTile(pages[i], i));
    539 
    540   tilesContainer.innerHTML = '';
    541   renderAndShowTiles();
    542 }
    543 
    544 
    545 /**
    546  * Binds onload events for a tile's internal <iframe> elements.
    547  * @param {Tile} tile The main tile to bind events to.
    548  * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible
    549  *   the moment all tiles are loaded.
    550  */
    551 function bindTileOnloadEvents(tile, tileVisibilityBarrier) {
    552   if (tile.titleElem) {
    553     tileVisibilityBarrier.add();
    554     tile.titleElem.onload = function() {
    555       tileVisibilityBarrier.remove();
    556     };
    557   }
    558   if (tile.thumbnailElem) {
    559     tileVisibilityBarrier.add();
    560     tile.thumbnailElem.onload = function() {
    561       tile.elem.classList.add(CLASSES.PAGE_READY);
    562       tileVisibilityBarrier.remove();
    563     };
    564   }
    565 }
    566 
    567 
    568 /**
    569  * Renders the current list of visible tiles to DOM, and hides tiles that are
    570  * already in the DOM but should not be seen.
    571  */
    572 function renderAndShowTiles() {
    573   var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length;
    574   // Only add visible tiles to the DOM, to avoid creating invisible tiles that
    575   // produce meaningless impression metrics. However, if a tile becomes
    576   // invisible then we leave it in DOM to prevent reload if it's shown again.
    577   var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS);
    578 
    579   // If we need to render new tiles, manage the visibility to hide intermediate
    580   // load states of the <iframe>s.
    581   if (numExisting < numDesired) {
    582     var showAll = function() {
    583       for (var i = 0; i < numDesired; ++i) {
    584         if (tiles[i].titleElem || tiles[i].thumbnailElem)
    585           tiles[i].elem.classList.add(CLASSES.PAGE_READY);
    586       }
    587     };
    588     var tileVisibilityBarrier = new Barrier(showAll);
    589 
    590     if (!userInitiatedMostVisitedChange) {
    591       // Make titleContainer invisible, but still taking up space.
    592       // titleContainer becomes visible again (1) on timeout, or (2) when all
    593       // tiles finish loading (using tileVisibilityBarrier).
    594       window.setTimeout(function() {
    595         tileVisibilityBarrier.cancel();
    596         showAll();
    597       }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
    598     }
    599     userInitiatedMostVisitedChange = false;
    600 
    601     for (var i = numExisting; i < numDesired; ++i) {
    602       bindTileOnloadEvents(tiles[i], tileVisibilityBarrier);
    603       tilesContainer.appendChild(tiles[i].elem);
    604     }
    605   }
    606 
    607   // Show only the desired tiles. Note that .hidden does not work for
    608   // inline-block elements like tiles[i].elem.
    609   for (var i = 0; i < numDesired; ++i)
    610     tiles[i].elem.style.display = 'inline-block';
    611   // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs
    612   // when window is downsized).
    613   for (; i < numExisting; ++i)
    614     tiles[i].elem.style.display = 'none';
    615 }
    616 
    617 
    618 /**
    619  * Builds a URL to display a most visited tile title in an iframe.
    620  * @param {number} rid The restricted ID.
    621  * @param {number} position The position of the iframe in the UI.
    622  * @return {string} An URL to display the most visited title in an iframe.
    623  */
    624 function getMostVisitedTitleIframeUrl(rid, position) {
    625   var url = 'chrome-search://most-visited/' +
    626       encodeURIComponent(MOST_VISITED_TITLE_IFRAME);
    627   var params = [
    628       'rid=' + encodeURIComponent(rid),
    629       'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
    630       'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
    631       'c=' + encodeURIComponent(titleColor),
    632       'pos=' + encodeURIComponent(position)];
    633   if (NTP_DESIGN.titleTextAlign)
    634     params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign));
    635   if (NTP_DESIGN.titleTextFade)
    636     params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade));
    637   return url + '?' + params.join('&');
    638 }
    639 
    640 
    641 /**
    642  * Builds a URL to display a most visited tile thumbnail in an iframe.
    643  * @param {number} rid The restricted ID.
    644  * @param {number} position The position of the iframe in the UI.
    645  * @return {string} An URL to display the most visited thumbnail in an iframe.
    646  */
    647 function getMostVisitedThumbnailIframeUrl(rid, position) {
    648   var url = 'chrome-search://most-visited/' +
    649       encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME);
    650   var params = [
    651       'rid=' + encodeURIComponent(rid),
    652       'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
    653       'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
    654       'c=' + encodeURIComponent(NTP_DESIGN.thumbnailTextColor),
    655       'pos=' + encodeURIComponent(position)];
    656   if (NTP_DESIGN.thumbnailFallback)
    657     params.push('etfb=1');
    658   return url + '?' + params.join('&');
    659 }
    660 
    661 
    662 /**
    663  * Creates a Tile with the specified page data. If no data is provided, a
    664  * filler Tile is created.
    665  * @param {Object} page The page data.
    666  * @param {number} position The position of the tile.
    667  * @return {Tile} The new Tile.
    668  */
    669 function createTile(page, position) {
    670   var tileElem = document.createElement('div');
    671   tileElem.classList.add(CLASSES.TILE);
    672   // Prevent tile from being selected (and highlighted) when areas outside the
    673   // <iframe>s are clicked.
    674   tileElem.addEventListener('mousedown', function(e) {
    675     e.preventDefault();
    676   });
    677   var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER);
    678 
    679   if (page) {
    680     var rid = page.rid;
    681     tileElem.classList.add(CLASSES.PAGE);
    682 
    683     var navigateFunction = function(e) {
    684       e.preventDefault();
    685       ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e));
    686     };
    687 
    688     // The click handler for navigating to the page identified by the RID.
    689     tileElem.addEventListener('click', navigateFunction);
    690 
    691     // The iframe which renders the page title.
    692     var titleElem = document.createElement('iframe');
    693     // Enable tab navigation on the iframe, which will move the selection to the
    694     // link element (which also has a tabindex).
    695     titleElem.tabIndex = '0';
    696 
    697     // Why iframes have IDs:
    698     //
    699     // On navigating back to the NTP we see several onmostvisitedchange() events
    700     // in series with incrementing RIDs. After the first event, a set of iframes
    701     // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get
    702     // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1.
    703     // Now due to crbug.com/68841, Chrome incorrectly loads the content for the
    704     // first set of iframes into the most recent set of iframes.
    705     //
    706     // Giving iframes distinct ids seems to cause some invalidation and prevent
    707     // associating the incorrect data.
    708     //
    709     // TODO(jered): Find and fix the root (probably Blink) bug.
    710 
    711     // Keep this ID here. See comment above.
    712     titleElem.id = 'title-' + rid;
    713     titleElem.className = CLASSES.TITLE;
    714     titleElem.src = getMostVisitedTitleIframeUrl(rid, position);
    715     innerElem.appendChild(titleElem);
    716 
    717     // A fallback element for missing thumbnails.
    718     if (NTP_DESIGN.thumbnailFallback) {
    719       var fallbackElem = createAndAppendElement(
    720           innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK);
    721       if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT)
    722         createAndAppendElement(fallbackElem, 'div', CLASSES.DOT);
    723     }
    724 
    725     // The iframe which renders either a thumbnail or domain element.
    726     var thumbnailElem = document.createElement('iframe');
    727     thumbnailElem.tabIndex = '-1';
    728     thumbnailElem.setAttribute('aria-hidden', 'true');
    729     // Keep this ID here. See comment above.
    730     thumbnailElem.id = 'thumb-' + rid;
    731     thumbnailElem.className = CLASSES.THUMBNAIL;
    732     thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position);
    733     innerElem.appendChild(thumbnailElem);
    734 
    735     // The button used to blacklist this page.
    736     var blacklistButton = createAndAppendElement(
    737         innerElem, 'div', CLASSES.BLACKLIST_BUTTON);
    738     createAndAppendElement(
    739         blacklistButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
    740     var blacklistFunction = generateBlacklistFunction(rid);
    741     blacklistButton.addEventListener('click', blacklistFunction);
    742     blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
    743 
    744     // A helper mask on top of the tile that is used to create hover border
    745     // and/or to darken the thumbnail on focus.
    746     var maskElement = createAndAppendElement(
    747         innerElem, 'div', CLASSES.THUMBNAIL_MASK);
    748 
    749     // The page favicon, or a fallback.
    750     var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON);
    751     if (page.faviconUrl) {
    752       favicon.style.backgroundImage = 'url(' + page.faviconUrl + ')';
    753     } else {
    754       favicon.classList.add(CLASSES.FAVICON_FALLBACK);
    755     }
    756     return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid);
    757   } else {
    758     return new Tile(tileElem);
    759   }
    760 }
    761 
    762 
    763 /**
    764  * Generates a function to be called when the page with the corresponding RID
    765  * is blacklisted.
    766  * @param {number} rid The RID of the page being blacklisted.
    767  * @return {function(Event=)} A function which handles the blacklisting of the
    768  *     page by updating state variables and notifying Chrome.
    769  */
    770 function generateBlacklistFunction(rid) {
    771   return function(e) {
    772     // Prevent navigation when the page is being blacklisted.
    773     if (e)
    774       e.stopPropagation();
    775 
    776     userInitiatedMostVisitedChange = true;
    777     isBlacklisting = true;
    778     tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
    779     lastBlacklistedTile = getTileByRid(rid);
    780     ntpApiHandle.deleteMostVisitedItem(rid);
    781   };
    782 }
    783 
    784 
    785 /**
    786  * Shows the blacklist notification and triggers a delay to hide it.
    787  */
    788 function showNotification() {
    789   notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
    790   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
    791   notification.scrollTop;
    792   notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
    793 }
    794 
    795 
    796 /**
    797  * Hides the blacklist notification.
    798  */
    799 function hideNotification() {
    800   notification.classList.add(CLASSES.HIDE_NOTIFICATION);
    801   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
    802 }
    803 
    804 
    805 /**
    806  * Handles a click on the notification undo link by hiding the notification and
    807  * informing Chrome.
    808  */
    809 function onUndo() {
    810   userInitiatedMostVisitedChange = true;
    811   hideNotification();
    812   var lastBlacklistedRID = lastBlacklistedTile.rid;
    813   if (typeof lastBlacklistedRID != 'undefined')
    814     ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
    815 }
    816 
    817 
    818 /**
    819  * Handles a click on the restore all notification link by hiding the
    820  * notification and informing Chrome.
    821  */
    822 function onRestoreAll() {
    823   userInitiatedMostVisitedChange = true;
    824   hideNotification();
    825   ntpApiHandle.undoAllMostVisitedDeletions();
    826 }
    827 
    828 
    829 /**
    830  * Recomputes the number of tile columns, and width of various contents based
    831  * on the width of the window.
    832  * @return {boolean} Whether the number of tile columns has changed.
    833  */
    834 function updateContentWidth() {
    835   var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
    836   // If innerWidth is zero, then use the maximum snap size.
    837   var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
    838       NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
    839   var innerWidth = window.innerWidth || maxSnapSize;
    840   // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
    841   var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
    842       MIN_TOTAL_HORIZONTAL_PADDING;
    843   var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
    844   if (newNumColumns < MIN_NUM_COLUMNS)
    845     newNumColumns = MIN_NUM_COLUMNS;
    846   else if (newNumColumns > MAX_NUM_COLUMNS)
    847     newNumColumns = MAX_NUM_COLUMNS;
    848 
    849   if (numColumnsShown === newNumColumns)
    850     return false;
    851 
    852   numColumnsShown = newNumColumns;
    853   var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
    854   tilesContainer.style.width = tilesContainerWidth + 'px';
    855   if (fakebox) {
    856     // -2 to account for border.
    857     var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
    858     fakebox.style.width = fakeboxWidth + 'px';
    859   }
    860   return true;
    861 }
    862 
    863 
    864 /**
    865  * Resizes elements because the number of tile columns may need to change in
    866  * response to resizing. Also shows or hides extra tiles tiles according to the
    867  * new width of the page.
    868  */
    869 function onResize() {
    870   if (updateContentWidth()) {
    871     // Render without clearing tiles.
    872     renderAndShowTiles();
    873   }
    874 }
    875 
    876 
    877 /**
    878  * Returns the tile corresponding to the specified page RID.
    879  * @param {number} rid The page RID being looked up.
    880  * @return {Tile} The corresponding tile.
    881  */
    882 function getTileByRid(rid) {
    883   for (var i = 0, length = tiles.length; i < length; ++i) {
    884     var tile = tiles[i];
    885     if (tile.rid == rid)
    886       return tile;
    887   }
    888   return null;
    889 }
    890 
    891 
    892 /**
    893  * Handles new input by disposing the NTP, according to where the input was
    894  * entered.
    895  */
    896 function onInputStart() {
    897   if (fakebox && isFakeboxFocused()) {
    898     setFakeboxFocus(false);
    899     setFakeboxDragFocus(false);
    900     disposeNtp(true);
    901   } else if (!isFakeboxFocused()) {
    902     disposeNtp(false);
    903   }
    904 }
    905 
    906 
    907 /**
    908  * Disposes the NTP, according to where the input was entered.
    909  * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
    910  */
    911 function disposeNtp(wasFakeboxInput) {
    912   var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
    913   if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
    914     setFakeboxActive(false);
    915   else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
    916     setFakeboxAndLogoVisibility(false);
    917 }
    918 
    919 
    920 /**
    921  * Restores the NTP (re-enables the fakebox and unhides the logo.)
    922  */
    923 function restoreNtp() {
    924   setFakeboxActive(true);
    925   setFakeboxAndLogoVisibility(true);
    926 }
    927 
    928 
    929 /**
    930  * @param {boolean} focus True to focus the fakebox.
    931  */
    932 function setFakeboxFocus(focus) {
    933   document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
    934 }
    935 
    936 /**
    937  * @param {boolean} focus True to show a dragging focus to the fakebox.
    938  */
    939 function setFakeboxDragFocus(focus) {
    940   document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
    941 }
    942 
    943 /**
    944  * @return {boolean} True if the fakebox has focus.
    945  */
    946 function isFakeboxFocused() {
    947   return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
    948       document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
    949 }
    950 
    951 
    952 /**
    953  * @param {boolean} enable True to enable the fakebox.
    954  */
    955 function setFakeboxActive(enable) {
    956   document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
    957 }
    958 
    959 
    960 /**
    961  * @param {!Event} event The click event.
    962  * @return {boolean} True if the click occurred in an enabled fakebox.
    963  */
    964 function isFakeboxClick(event) {
    965   return fakebox.contains(event.target) &&
    966       !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
    967 }
    968 
    969 
    970 /**
    971  * @param {boolean} show True to show the fakebox and logo.
    972  */
    973 function setFakeboxAndLogoVisibility(show) {
    974   document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
    975 }
    976 
    977 
    978 /**
    979  * Shortcut for document.getElementById.
    980  * @param {string} id of the element.
    981  * @return {HTMLElement} with the id.
    982  */
    983 function $(id) {
    984   return document.getElementById(id);
    985 }
    986 
    987 
    988 /**
    989  * Utility function which creates an element with an optional classname and
    990  * appends it to the specified parent.
    991  * @param {Element} parent The parent to append the new element.
    992  * @param {string} name The name of the new element.
    993  * @param {string=} opt_class The optional classname of the new element.
    994  * @return {Element} The new element.
    995  */
    996 function createAndAppendElement(parent, name, opt_class) {
    997   var child = document.createElement(name);
    998   if (opt_class)
    999     child.classList.add(opt_class);
   1000   parent.appendChild(child);
   1001   return child;
   1002 }
   1003 
   1004 
   1005 /**
   1006  * Removes a node from its parent.
   1007  * @param {Node} node The node to remove.
   1008  */
   1009 function removeNode(node) {
   1010   node.parentNode.removeChild(node);
   1011 }
   1012 
   1013 
   1014 /**
   1015  * @param {!Element} element The element to register the handler for.
   1016  * @param {number} keycode The keycode of the key to register.
   1017  * @param {!Function} handler The key handler to register.
   1018  */
   1019 function registerKeyHandler(element, keycode, handler) {
   1020   element.addEventListener('keydown', function(event) {
   1021     if (event.keyCode == keycode)
   1022       handler(event);
   1023   });
   1024 }
   1025 
   1026 
   1027 /**
   1028  * @return {Object} the handle to the embeddedSearch API.
   1029  */
   1030 function getEmbeddedSearchApiHandle() {
   1031   if (window.cideb)
   1032     return window.cideb;
   1033   if (window.chrome && window.chrome.embeddedSearch)
   1034     return window.chrome.embeddedSearch;
   1035   return null;
   1036 }
   1037 
   1038 
   1039 /**
   1040  * Event handler for the focus changed and blacklist messages on link elements.
   1041  * Used to toggle visual treatment on the tiles (depending on the message).
   1042  * @param {Event} event Event received.
   1043  */
   1044 function handlePostMessage(event) {
   1045   if (event.origin !== 'chrome-search://most-visited')
   1046     return;
   1047 
   1048   if (event.data === 'linkFocused') {
   1049     var activeElement = document.activeElement;
   1050     if (activeElement.classList.contains(CLASSES.TITLE)) {
   1051       activeElement.classList.add(CLASSES.FOCUSED);
   1052       focusedIframe = activeElement;
   1053     }
   1054   } else if (event.data === 'linkBlurred') {
   1055     if (focusedIframe)
   1056       focusedIframe.classList.remove(CLASSES.FOCUSED);
   1057     focusedIframe = null;
   1058   } else if (event.data.indexOf('tileBlacklisted') === 0) {
   1059     var tilePosition = event.data.split(',')[1];
   1060     if (tilePosition)
   1061       generateBlacklistFunction(tiles[parseInt(tilePosition, 10)].rid)();
   1062   }
   1063 }
   1064 
   1065 
   1066 /**
   1067  * Prepares the New Tab Page by adding listeners, rendering the current
   1068  * theme, the most visited pages section, and Google-specific elements for a
   1069  * Google-provided page.
   1070  */
   1071 function init() {
   1072   tilesContainer = $(IDS.TILES);
   1073   notification = $(IDS.NOTIFICATION);
   1074   attribution = $(IDS.ATTRIBUTION);
   1075   ntpContents = $(IDS.NTP_CONTENTS);
   1076 
   1077   if (configData.isGooglePage) {
   1078     var logo = document.createElement('div');
   1079     logo.id = IDS.LOGO;
   1080 
   1081     fakebox = document.createElement('div');
   1082     fakebox.id = IDS.FAKEBOX;
   1083     var fakeboxHtml = [];
   1084     fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
   1085         '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
   1086     fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
   1087     fakeboxHtml.push('<div id="cursor"></div>');
   1088     fakebox.innerHTML = fakeboxHtml.join('');
   1089 
   1090     ntpContents.insertBefore(fakebox, ntpContents.firstChild);
   1091     ntpContents.insertBefore(logo, ntpContents.firstChild);
   1092   } else {
   1093     document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
   1094   }
   1095 
   1096   // Hide notifications after fade out, so we can't focus on links via keyboard.
   1097   notification.addEventListener('webkitTransitionEnd', hideNotification);
   1098 
   1099   var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
   1100   notificationMessage.textContent =
   1101       configData.translatedStrings.thumbnailRemovedNotification;
   1102 
   1103   var undoLink = $(IDS.UNDO_LINK);
   1104   undoLink.addEventListener('click', onUndo);
   1105   registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
   1106   undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
   1107 
   1108   var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
   1109   restoreAllLink.addEventListener('click', onRestoreAll);
   1110   registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
   1111   restoreAllLink.textContent =
   1112       configData.translatedStrings.restoreThumbnailsShort;
   1113 
   1114   $(IDS.ATTRIBUTION_TEXT).textContent =
   1115       configData.translatedStrings.attributionIntro;
   1116 
   1117   var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
   1118   createAndAppendElement(
   1119       notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
   1120   notificationCloseButton.addEventListener('click', hideNotification);
   1121 
   1122   window.addEventListener('resize', onResize);
   1123   updateContentWidth();
   1124 
   1125   var topLevelHandle = getEmbeddedSearchApiHandle();
   1126 
   1127   ntpApiHandle = topLevelHandle.newTabPage;
   1128   ntpApiHandle.onthemechange = onThemeChange;
   1129   ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
   1130 
   1131   ntpApiHandle.oninputstart = onInputStart;
   1132   ntpApiHandle.oninputcancel = restoreNtp;
   1133 
   1134   if (ntpApiHandle.isInputInProgress)
   1135     onInputStart();
   1136 
   1137   renderTheme();
   1138   onMostVisitedChange();
   1139 
   1140   searchboxApiHandle = topLevelHandle.searchBox;
   1141 
   1142   if (fakebox) {
   1143     // Listener for updating the key capture state.
   1144     document.body.onmousedown = function(event) {
   1145       if (isFakeboxClick(event))
   1146         searchboxApiHandle.startCapturingKeyStrokes();
   1147       else if (isFakeboxFocused())
   1148         searchboxApiHandle.stopCapturingKeyStrokes();
   1149     };
   1150     searchboxApiHandle.onkeycapturechange = function() {
   1151       setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
   1152     };
   1153     var inputbox = $(IDS.FAKEBOX_INPUT);
   1154     if (inputbox) {
   1155       inputbox.onpaste = function(event) {
   1156         event.preventDefault();
   1157         searchboxApiHandle.paste();
   1158       };
   1159       inputbox.ondrop = function(event) {
   1160         event.preventDefault();
   1161         var text = event.dataTransfer.getData('text/plain');
   1162         if (text) {
   1163           searchboxApiHandle.paste(text);
   1164         }
   1165       };
   1166       inputbox.ondragenter = function() {
   1167         setFakeboxDragFocus(true);
   1168       };
   1169       inputbox.ondragleave = function() {
   1170         setFakeboxDragFocus(false);
   1171       };
   1172     }
   1173 
   1174     // Update the fakebox style to match the current key capturing state.
   1175     setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
   1176   }
   1177 
   1178   if (searchboxApiHandle.rtl) {
   1179     $(IDS.NOTIFICATION).dir = 'rtl';
   1180     document.body.setAttribute('dir', 'rtl');
   1181     // Add class for setting alignments based on language directionality.
   1182     document.body.classList.add(CLASSES.RTL);
   1183     $(IDS.TILES).dir = 'rtl';
   1184   }
   1185 
   1186   window.addEventListener('message', handlePostMessage);
   1187 }
   1188 
   1189 
   1190 /**
   1191  * Binds event listeners.
   1192  */
   1193 function listen() {
   1194   document.addEventListener('DOMContentLoaded', init);
   1195 }
   1196 
   1197 return {
   1198   init: init,
   1199   listen: listen
   1200 };
   1201 }
   1202 
   1203 if (!window.localNTPUnitTest) {
   1204   LocalNTP().listen();
   1205 }
   1206