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="window_disposition_util.js">
     18 
     19 
     20 /**
     21  * Enum for classnames.
     22  * @enum {string}
     23  * @const
     24  */
     25 var CLASSES = {
     26   ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
     27   BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation
     28   BLACKLIST_BUTTON: 'mv-x',
     29   DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
     30   FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive
     31   FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox
     32   // Applies drag focus style to the fakebox
     33   FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
     34   FAVICON: 'mv-favicon',
     35   HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation
     36   HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo',
     37   HIDE_NOTIFICATION: 'mv-notice-hide',
     38   // Vertically centers the most visited section for a non-Google provided page.
     39   NON_GOOGLE_PAGE: 'non-google-page',
     40   PAGE: 'mv-page', // page tiles
     41   PAGE_READY: 'mv-page-ready',  // page tile when ready
     42   ROW: 'mv-row',  // tile row
     43   RTL: 'rtl',  // Right-to-left language text.
     44   THUMBNAIL: 'mv-thumb',
     45   THUMBNAIL_MASK: 'mv-mask',
     46   TILE: 'mv-tile',
     47   TITLE: 'mv-title'
     48 };
     49 
     50 
     51 /**
     52  * Enum for HTML element ids.
     53  * @enum {string}
     54  * @const
     55  */
     56 var IDS = {
     57   ATTRIBUTION: 'attribution',
     58   ATTRIBUTION_TEXT: 'attribution-text',
     59   CUSTOM_THEME_STYLE: 'ct-style',
     60   FAKEBOX: 'fakebox',
     61   FAKEBOX_INPUT: 'fakebox-input',
     62   LOGO: 'logo',
     63   NOTIFICATION: 'mv-notice',
     64   NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x',
     65   NOTIFICATION_MESSAGE: 'mv-msg',
     66   NTP_CONTENTS: 'ntp-contents',
     67   RESTORE_ALL_LINK: 'mv-restore',
     68   TILES: 'mv-tiles',
     69   UNDO_LINK: 'mv-undo'
     70 };
     71 
     72 
     73 /**
     74  * Enum for keycodes.
     75  * @enum {number}
     76  * @const
     77  */
     78 var KEYCODE = {
     79   DELETE: 46,
     80   ENTER: 13
     81 };
     82 
     83 
     84 /**
     85  * Enum for the state of the NTP when it is disposed.
     86  * @enum {number}
     87  * @const
     88  */
     89 var NTP_DISPOSE_STATE = {
     90   NONE: 0,  // Preserve the NTP appearance and functionality
     91   DISABLE_FAKEBOX: 1,
     92   HIDE_FAKEBOX_AND_LOGO: 2
     93 };
     94 
     95 
     96 /**
     97  * The JavaScript button event value for a middle click.
     98  * @type {number}
     99  * @const
    100  */
    101 var MIDDLE_MOUSE_BUTTON = 1;
    102 
    103 
    104 /**
    105  * The container for the tile elements.
    106  * @type {Element}
    107  */
    108 var tilesContainer;
    109 
    110 
    111 /**
    112  * The notification displayed when a page is blacklisted.
    113  * @type {Element}
    114  */
    115 var notification;
    116 
    117 
    118 /**
    119  * The container for the theme attribution.
    120  * @type {Element}
    121  */
    122 var attribution;
    123 
    124 
    125 /**
    126  * The "fakebox" - an input field that looks like a regular searchbox.  When it
    127  * is focused, any text the user types goes directly into the omnibox.
    128  * @type {Element}
    129  */
    130 var fakebox;
    131 
    132 
    133 /**
    134  * The container for NTP elements.
    135  * @type {Element}
    136  */
    137 var ntpContents;
    138 
    139 
    140 /**
    141  * The array of rendered tiles, ordered by appearance.
    142  * @type {!Array.<Tile>}
    143  */
    144 var tiles = [];
    145 
    146 
    147 /**
    148  * The last blacklisted tile if any, which by definition should not be filler.
    149  * @type {?Tile}
    150  */
    151 var lastBlacklistedTile = null;
    152 
    153 
    154 /**
    155  * True if a page has been blacklisted and we're waiting on the
    156  * onmostvisitedchange callback. See onMostVisitedChange() for how this
    157  * is used.
    158  * @type {boolean}
    159  */
    160 var isBlacklisting = false;
    161 
    162 
    163 /**
    164  * Current number of tiles columns shown based on the window width, including
    165  * those that just contain filler.
    166  * @type {number}
    167  */
    168 var numColumnsShown = 0;
    169 
    170 
    171 /**
    172  * True if the user initiated the current most visited change and false
    173  * otherwise.
    174  * @type {boolean}
    175  */
    176 var userInitiatedMostVisitedChange = false;
    177 
    178 
    179 /**
    180  * The browser embeddedSearch.newTabPage object.
    181  * @type {Object}
    182  */
    183 var ntpApiHandle;
    184 
    185 
    186 /**
    187  * The browser embeddedSearch.searchBox object.
    188  * @type {Object}
    189  */
    190 var searchboxApiHandle;
    191 
    192 
    193 /**
    194  * The state of the NTP when a query is entered into the Omnibox.
    195  * @type {NTP_DISPOSE_STATE}
    196  */
    197 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
    198 
    199 
    200 /**
    201  * The state of the NTP when a query is entered into the Fakebox.
    202  * @type {NTP_DISPOSE_STATE}
    203  */
    204 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
    205 
    206 
    207 /**
    208  * Total tile width. Should be equal to mv-tile's width + 2 * border-width.
    209  * @private {number}
    210  * @const
    211  */
    212 var TILE_WIDTH = 140;
    213 
    214 
    215 /**
    216  * Margin between tiles. Should be equal to mv-tile's -webkit-margin-start.
    217  * @private {number}
    218  * @const
    219  */
    220 var TILE_MARGIN_START = 20;
    221 
    222 
    223 /** @type {number} @const */
    224 var MAX_NUM_TILES_TO_SHOW = 8;
    225 
    226 
    227 /** @type {number} @const */
    228 var MIN_NUM_COLUMNS = 2;
    229 
    230 
    231 /** @type {number} @const */
    232 var MAX_NUM_COLUMNS = 4;
    233 
    234 
    235 /** @type {number} @const */
    236 var NUM_ROWS = 2;
    237 
    238 
    239 /**
    240  * Minimum total padding to give to the left and right of the most visited
    241  * section. Used to determine how many tiles to show.
    242  * @type {number}
    243  * @const
    244  */
    245 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
    246 
    247 
    248 /**
    249  * The filename for a most visited iframe src which shows a page title.
    250  * @type {string}
    251  * @const
    252  */
    253 var MOST_VISITED_TITLE_IFRAME = 'title.html';
    254 
    255 
    256 /**
    257  * The filename for a most visited iframe src which shows a thumbnail image.
    258  * @type {string}
    259  * @const
    260  */
    261 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
    262 
    263 
    264 /**
    265  * The hex color for most visited tile elements.
    266  * @type {string}
    267  * @const
    268  */
    269 var MOST_VISITED_COLOR = '777777';
    270 
    271 
    272 /**
    273  * The font family for most visited tile elements.
    274  * @type {string}
    275  * @const
    276  */
    277 var MOST_VISITED_FONT_FAMILY = 'arial, sans-serif';
    278 
    279 
    280 /**
    281  * The font size for most visited tile elements.
    282  * @type {number}
    283  * @const
    284  */
    285 var MOST_VISITED_FONT_SIZE = 11;
    286 
    287 
    288 /**
    289  * Hide most visited tiles for at most this many milliseconds while painting.
    290  * @type {number}
    291  * @const
    292  */
    293 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
    294 
    295 
    296 /**
    297  * A Tile is either a rendering of a Most Visited page or "filler" used to
    298  * pad out the section when not enough pages exist.
    299  *
    300  * @param {Element} elem The element for rendering the tile.
    301  * @param {number=} opt_rid The RID for the corresponding Most Visited page.
    302  *     Should only be left unspecified when creating a filler tile.
    303  * @constructor
    304  */
    305 function Tile(elem, opt_rid) {
    306   /** @type {Element} */
    307   this.elem = elem;
    308 
    309   /** @type {number|undefined} */
    310   this.rid = opt_rid;
    311 }
    312 
    313 
    314 /**
    315  * Updates the NTP based on the current theme.
    316  * @private
    317  */
    318 function onThemeChange() {
    319   var info = ntpApiHandle.themeBackgroundInfo;
    320   if (!info)
    321     return;
    322 
    323   var background = [convertToRGBAColor(info.backgroundColorRgba),
    324                     info.imageUrl,
    325                     info.imageTiling,
    326                     info.imageHorizontalAlignment,
    327                     info.imageVerticalAlignment].join(' ').trim();
    328   document.body.style.background = background;
    329   document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
    330   updateThemeAttribution(info.attributionUrl);
    331   setCustomThemeStyle(info);
    332   renderTiles();
    333 }
    334 
    335 
    336 /**
    337  * Updates the NTP style according to theme.
    338  * @param {Object=} opt_themeInfo The information about the theme. If it is
    339  * omitted the style will be reverted to the default.
    340  * @private
    341  */
    342 function setCustomThemeStyle(opt_themeInfo) {
    343   var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
    344   var head = document.head;
    345 
    346   if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
    347     var themeStyle =
    348       '#attribution {' +
    349       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
    350       '}' +
    351       '#mv-msg {' +
    352       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
    353       '}' +
    354       '#mv-notice-links span {' +
    355       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
    356       '}' +
    357       '#mv-notice-x {' +
    358       '  -webkit-filter: drop-shadow(0 0 0 ' +
    359           convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
    360       '}' +
    361       '.mv-page-ready {' +
    362       '  border: 1px solid ' +
    363         convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
    364       '}' +
    365       '.mv-page-ready:hover, .mv-page-ready:focus {' +
    366       '  border-color: ' +
    367           convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
    368       '}';
    369 
    370     if (customStyleElement) {
    371       customStyleElement.textContent = themeStyle;
    372     } else {
    373       customStyleElement = document.createElement('style');
    374       customStyleElement.type = 'text/css';
    375       customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
    376       customStyleElement.textContent = themeStyle;
    377       head.appendChild(customStyleElement);
    378     }
    379 
    380   } else if (customStyleElement) {
    381     head.removeChild(customStyleElement);
    382   }
    383 }
    384 
    385 
    386 /**
    387  * Renders the attribution if the URL is present, otherwise hides it.
    388  * @param {string} url The URL of the attribution image, if any.
    389  * @private
    390  */
    391 function updateThemeAttribution(url) {
    392   if (!url) {
    393     setAttributionVisibility_(false);
    394     return;
    395   }
    396 
    397   var attributionImage = attribution.querySelector('img');
    398   if (!attributionImage) {
    399     attributionImage = new Image();
    400     attribution.appendChild(attributionImage);
    401   }
    402   attributionImage.style.content = url;
    403   setAttributionVisibility_(true);
    404 }
    405 
    406 
    407 /**
    408  * Sets the visibility of the theme attribution.
    409  * @param {boolean} show True to show the attribution.
    410  * @private
    411  */
    412 function setAttributionVisibility_(show) {
    413   if (attribution) {
    414     attribution.style.display = show ? '' : 'none';
    415   }
    416 }
    417 
    418 
    419  /**
    420  * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
    421  * @param {Array.<number>} color Array of rgba color components.
    422  * @return {string} CSS color in RGBA format.
    423  * @private
    424  */
    425 function convertToRGBAColor(color) {
    426   return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
    427                     color[3] / 255 + ')';
    428 }
    429 
    430 
    431 /**
    432  * Handles a new set of Most Visited page data.
    433  */
    434 function onMostVisitedChange() {
    435   var pages = ntpApiHandle.mostVisited;
    436 
    437   if (isBlacklisting) {
    438     // Trigger the blacklist animation and re-render the tiles when it
    439     // completes.
    440     var lastBlacklistedTileElement = lastBlacklistedTile.elem;
    441     lastBlacklistedTileElement.addEventListener(
    442         'webkitTransitionEnd', blacklistAnimationDone);
    443     lastBlacklistedTileElement.classList.add(CLASSES.BLACKLIST);
    444 
    445   } else {
    446     // Otherwise render the tiles using the new data without animation.
    447     tiles = [];
    448     for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) {
    449       tiles.push(createTile(pages[i], i));
    450     }
    451     if (!userInitiatedMostVisitedChange) {
    452       tilesContainer.hidden = true;
    453       window.setTimeout(function() {
    454         if (tilesContainer) {
    455           tilesContainer.hidden = false;
    456         }
    457       }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
    458     }
    459     renderTiles();
    460   }
    461 }
    462 
    463 
    464 /**
    465  * Renders the current set of tiles.
    466  */
    467 function renderTiles() {
    468   var rows = tilesContainer.children;
    469   for (var i = 0; i < rows.length; ++i) {
    470     removeChildren(rows[i]);
    471   }
    472 
    473   for (var i = 0, length = tiles.length;
    474        i < Math.min(length, numColumnsShown * NUM_ROWS); ++i) {
    475     rows[Math.floor(i / numColumnsShown)].appendChild(tiles[i].elem);
    476   }
    477 }
    478 
    479 
    480 /**
    481  * Shows most visited tiles if all child iframes are loaded, and hides them
    482  * otherwise.
    483  */
    484 function updateMostVisitedVisibility() {
    485   var iframes = tilesContainer.querySelectorAll('iframe');
    486   var ready = true;
    487   for (var i = 0, numIframes = iframes.length; i < numIframes; i++) {
    488     if (iframes[i].hidden) {
    489       ready = false;
    490       break;
    491     }
    492   }
    493   if (ready) {
    494     tilesContainer.hidden = false;
    495     userInitiatedMostVisitedChange = false;
    496   }
    497 }
    498 
    499 
    500 /**
    501  * Builds a URL to display a most visited tile component in an iframe.
    502  * @param {string} filename The desired most visited component filename.
    503  * @param {number} rid The restricted ID.
    504  * @param {string} color The text color for text in the iframe.
    505  * @param {string} fontFamily The font family for text in the iframe.
    506  * @param {number} fontSize The font size for text in the iframe.
    507  * @param {number} position The position of the iframe in the UI.
    508  * @return {string} An URL to display the most visited component in an iframe.
    509  */
    510 function getMostVisitedIframeUrl(filename, rid, color, fontFamily, fontSize,
    511     position) {
    512   return 'chrome-search://most-visited/' + encodeURIComponent(filename) + '?' +
    513       ['rid=' + encodeURIComponent(rid),
    514        'c=' + encodeURIComponent(color),
    515        'f=' + encodeURIComponent(fontFamily),
    516        'fs=' + encodeURIComponent(fontSize),
    517        'pos=' + encodeURIComponent(position)].join('&');
    518 }
    519 
    520 
    521 /**
    522  * Creates a Tile with the specified page data. If no data is provided, a
    523  * filler Tile is created.
    524  * @param {Object} page The page data.
    525  * @param {number} position The position of the tile.
    526  * @return {Tile} The new Tile.
    527  */
    528 function createTile(page, position) {
    529   var tileElement = document.createElement('div');
    530   tileElement.classList.add(CLASSES.TILE);
    531 
    532   if (page) {
    533     var rid = page.rid;
    534     tileElement.classList.add(CLASSES.PAGE);
    535 
    536     var navigateFunction = function(e) {
    537       e.preventDefault();
    538       ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e));
    539     };
    540 
    541     // The click handler for navigating to the page identified by the RID.
    542     tileElement.addEventListener('click', navigateFunction);
    543 
    544     // Make thumbnails tab-accessible.
    545     tileElement.setAttribute('tabindex', '1');
    546     registerKeyHandler(tileElement, KEYCODE.ENTER, navigateFunction);
    547 
    548     // The iframe which renders the page title.
    549     var titleElement = document.createElement('iframe');
    550     titleElement.tabIndex = '-1';
    551 
    552     // Why iframes have IDs:
    553     //
    554     // On navigating back to the NTP we see several onmostvisitedchange() events
    555     // in series with incrementing RIDs. After the first event, a set of iframes
    556     // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get
    557     // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1.
    558     // Now due to crbug.com/68841, Chrome incorrectly loads the content for the
    559     // first set of iframes into the most recent set of iframes.
    560     //
    561     // Giving iframes distinct ids seems to cause some invalidation and prevent
    562     // associating the incorrect data.
    563     //
    564     // TODO(jered): Find and fix the root (probably Blink) bug.
    565 
    566     titleElement.src = getMostVisitedIframeUrl(
    567         MOST_VISITED_TITLE_IFRAME, rid, MOST_VISITED_COLOR,
    568         MOST_VISITED_FONT_FAMILY, MOST_VISITED_FONT_SIZE, position);
    569 
    570     // Keep this id here. See comment above.
    571     titleElement.id = 'title-' + rid;
    572     titleElement.hidden = true;
    573     titleElement.onload = function() {
    574       titleElement.hidden = false;
    575       updateMostVisitedVisibility();
    576     };
    577     titleElement.className = CLASSES.TITLE;
    578     tileElement.appendChild(titleElement);
    579 
    580     // The iframe which renders either a thumbnail or domain element.
    581     var thumbnailElement = document.createElement('iframe');
    582     thumbnailElement.tabIndex = '-1';
    583     thumbnailElement.src = getMostVisitedIframeUrl(
    584         MOST_VISITED_THUMBNAIL_IFRAME, rid, MOST_VISITED_COLOR,
    585         MOST_VISITED_FONT_FAMILY, MOST_VISITED_FONT_SIZE, position);
    586 
    587     // Keep this id here. See comment above.
    588     thumbnailElement.id = 'thumb-' + rid;
    589     thumbnailElement.hidden = true;
    590     thumbnailElement.onload = function() {
    591       thumbnailElement.hidden = false;
    592       tileElement.classList.add(CLASSES.PAGE_READY);
    593       updateMostVisitedVisibility();
    594     };
    595     thumbnailElement.className = CLASSES.THUMBNAIL;
    596     tileElement.appendChild(thumbnailElement);
    597 
    598     // A mask to darken the thumbnail on focus.
    599     var maskElement = createAndAppendElement(
    600         tileElement, 'div', CLASSES.THUMBNAIL_MASK);
    601 
    602     // The button used to blacklist this page.
    603     var blacklistButton = createAndAppendElement(
    604         tileElement, 'div', CLASSES.BLACKLIST_BUTTON);
    605     var blacklistFunction = generateBlacklistFunction(rid);
    606     blacklistButton.addEventListener('click', blacklistFunction);
    607     blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
    608 
    609     // When a tile is focused, have delete also blacklist the page.
    610     registerKeyHandler(tileElement, KEYCODE.DELETE, blacklistFunction);
    611 
    612     // The page favicon, if any.
    613     var faviconUrl = page.faviconUrl;
    614     if (faviconUrl) {
    615       var favicon = createAndAppendElement(
    616           tileElement, 'div', CLASSES.FAVICON);
    617       favicon.style.backgroundImage = 'url(' + faviconUrl + ')';
    618     }
    619     return new Tile(tileElement, rid);
    620   } else {
    621     return new Tile(tileElement);
    622   }
    623 }
    624 
    625 
    626 /**
    627  * Generates a function to be called when the page with the corresponding RID
    628  * is blacklisted.
    629  * @param {number} rid The RID of the page being blacklisted.
    630  * @return {function(Event)} A function which handles the blacklisting of the
    631  *     page by updating state variables and notifying Chrome.
    632  */
    633 function generateBlacklistFunction(rid) {
    634   return function(e) {
    635     // Prevent navigation when the page is being blacklisted.
    636     e.stopPropagation();
    637 
    638     userInitiatedMostVisitedChange = true;
    639     isBlacklisting = true;
    640     tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
    641     lastBlacklistedTile = getTileByRid(rid);
    642     ntpApiHandle.deleteMostVisitedItem(rid);
    643   };
    644 }
    645 
    646 
    647 /**
    648  * Shows the blacklist notification and triggers a delay to hide it.
    649  */
    650 function showNotification() {
    651   notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
    652   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
    653   notification.scrollTop;
    654   notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
    655 }
    656 
    657 
    658 /**
    659  * Hides the blacklist notification.
    660  */
    661 function hideNotification() {
    662   notification.classList.add(CLASSES.HIDE_NOTIFICATION);
    663 }
    664 
    665 
    666 /**
    667  * Handles the end of the blacklist animation by showing the notification and
    668  * re-rendering the new set of tiles.
    669  */
    670 function blacklistAnimationDone() {
    671   showNotification();
    672   isBlacklisting = false;
    673   tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
    674   lastBlacklistedTile.elem.removeEventListener(
    675       'webkitTransitionEnd', blacklistAnimationDone);
    676   // Need to call explicitly to re-render the tiles, since the initial
    677   // onmostvisitedchange issued by the blacklist function only triggered
    678   // the animation.
    679   onMostVisitedChange();
    680 }
    681 
    682 
    683 /**
    684  * Handles a click on the notification undo link by hiding the notification and
    685  * informing Chrome.
    686  */
    687 function onUndo() {
    688   userInitiatedMostVisitedChange = true;
    689   hideNotification();
    690   var lastBlacklistedRID = lastBlacklistedTile.rid;
    691   if (typeof lastBlacklistedRID != 'undefined')
    692     ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
    693 }
    694 
    695 
    696 /**
    697  * Handles a click on the restore all notification link by hiding the
    698  * notification and informing Chrome.
    699  */
    700 function onRestoreAll() {
    701   userInitiatedMostVisitedChange = true;
    702   hideNotification();
    703   ntpApiHandle.undoAllMostVisitedDeletions();
    704 }
    705 
    706 
    707 /**
    708  * Re-renders the tiles if the number of columns has changed.  As a temporary
    709  * fix for crbug/240510, updates the width of the fakebox and most visited tiles
    710  * container.
    711  */
    712 function onResize() {
    713   // If innerWidth is zero, then use the maximum snap size.
    714   var innerWidth = window.innerWidth || 820;
    715 
    716   // These values should remain in sync with local_ntp.css.
    717   // TODO(jeremycho): Delete once the root cause of crbug/240510 is resolved.
    718   var setWidths = function(tilesContainerWidth) {
    719     tilesContainer.style.width = tilesContainerWidth + 'px';
    720     if (fakebox)
    721       fakebox.style.width = (tilesContainerWidth - 2) + 'px';
    722   };
    723   if (innerWidth >= 820)
    724     setWidths(620);
    725   else if (innerWidth >= 660)
    726     setWidths(460);
    727   else
    728     setWidths(300);
    729 
    730   var tileRequiredWidth = TILE_WIDTH + TILE_MARGIN_START;
    731   // Adds margin-start to the available width to compensate the extra margin
    732   // counted above for the first tile (which does not have a margin-start).
    733   var availableWidth = innerWidth + TILE_MARGIN_START -
    734       MIN_TOTAL_HORIZONTAL_PADDING;
    735   var numColumnsToShow = Math.floor(availableWidth / tileRequiredWidth);
    736   numColumnsToShow = Math.max(MIN_NUM_COLUMNS,
    737                               Math.min(MAX_NUM_COLUMNS, numColumnsToShow));
    738   if (numColumnsToShow != numColumnsShown) {
    739     numColumnsShown = numColumnsToShow;
    740     renderTiles();
    741   }
    742 }
    743 
    744 
    745 /**
    746  * Returns the tile corresponding to the specified page RID.
    747  * @param {number} rid The page RID being looked up.
    748  * @return {Tile} The corresponding tile.
    749  */
    750 function getTileByRid(rid) {
    751   for (var i = 0, length = tiles.length; i < length; ++i) {
    752     var tile = tiles[i];
    753     if (tile.rid == rid)
    754       return tile;
    755   }
    756   return null;
    757 }
    758 
    759 
    760 /**
    761  * Handles new input by disposing the NTP, according to where the input was
    762  * entered.
    763  */
    764 function onInputStart() {
    765   if (fakebox && isFakeboxFocused()) {
    766     setFakeboxFocus(false);
    767     setFakeboxDragFocus(false);
    768     disposeNtp(true);
    769   } else if (!isFakeboxFocused()) {
    770     disposeNtp(false);
    771   }
    772 }
    773 
    774 
    775 /**
    776  * Disposes the NTP, according to where the input was entered.
    777  * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
    778  */
    779 function disposeNtp(wasFakeboxInput) {
    780   var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
    781   if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
    782     setFakeboxActive(false);
    783   else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
    784     setFakeboxAndLogoVisibility(false);
    785 }
    786 
    787 
    788 /**
    789  * Restores the NTP (re-enables the fakebox and unhides the logo.)
    790  */
    791 function restoreNtp() {
    792   setFakeboxActive(true);
    793   setFakeboxAndLogoVisibility(true);
    794 }
    795 
    796 
    797 /**
    798  * @param {boolean} focus True to focus the fakebox.
    799  */
    800 function setFakeboxFocus(focus) {
    801   document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
    802 }
    803 
    804 /**
    805  * @param {boolean} focus True to show a dragging focus to the fakebox.
    806  */
    807 function setFakeboxDragFocus(focus) {
    808   document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
    809 }
    810 
    811 /**
    812  * @return {boolean} True if the fakebox has focus.
    813  */
    814 function isFakeboxFocused() {
    815   return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
    816       document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
    817 }
    818 
    819 
    820 /**
    821  * @param {boolean} enable True to enable the fakebox.
    822  */
    823 function setFakeboxActive(enable) {
    824   document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
    825 }
    826 
    827 
    828 /**
    829  * @param {!Event} event The click event.
    830  * @return {boolean} True if the click occurred in an enabled fakebox.
    831  */
    832 function isFakeboxClick(event) {
    833   return fakebox.contains(event.target) &&
    834       !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
    835 }
    836 
    837 
    838 /**
    839  * @param {boolean} show True to show the fakebox and logo.
    840  */
    841 function setFakeboxAndLogoVisibility(show) {
    842   document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
    843 }
    844 
    845 
    846 /**
    847  * Shortcut for document.getElementById.
    848  * @param {string} id of the element.
    849  * @return {HTMLElement} with the id.
    850  */
    851 function $(id) {
    852   return document.getElementById(id);
    853 }
    854 
    855 
    856 /**
    857  * Utility function which creates an element with an optional classname and
    858  * appends it to the specified parent.
    859  * @param {Element} parent The parent to append the new element.
    860  * @param {string} name The name of the new element.
    861  * @param {string=} opt_class The optional classname of the new element.
    862  * @return {Element} The new element.
    863  */
    864 function createAndAppendElement(parent, name, opt_class) {
    865   var child = document.createElement(name);
    866   if (opt_class)
    867     child.classList.add(opt_class);
    868   parent.appendChild(child);
    869   return child;
    870 }
    871 
    872 
    873 /**
    874  * Removes a node from its parent.
    875  * @param {Node} node The node to remove.
    876  */
    877 function removeNode(node) {
    878   node.parentNode.removeChild(node);
    879 }
    880 
    881 
    882 /**
    883  * Removes all the child nodes on a DOM node.
    884  * @param {Node} node Node to remove children from.
    885  */
    886 function removeChildren(node) {
    887   node.innerHTML = '';
    888 }
    889 
    890 
    891 /**
    892  * @param {!Element} element The element to register the handler for.
    893  * @param {number} keycode The keycode of the key to register.
    894  * @param {!Function} handler The key handler to register.
    895  */
    896 function registerKeyHandler(element, keycode, handler) {
    897   element.addEventListener('keydown', function(event) {
    898     if (event.keyCode == keycode)
    899       handler(event);
    900   });
    901 }
    902 
    903 
    904 /**
    905  * @return {Object} the handle to the embeddedSearch API.
    906  */
    907 function getEmbeddedSearchApiHandle() {
    908   if (window.cideb)
    909     return window.cideb;
    910   if (window.chrome && window.chrome.embeddedSearch)
    911     return window.chrome.embeddedSearch;
    912   return null;
    913 }
    914 
    915 
    916 /**
    917  * Prepares the New Tab Page by adding listeners, rendering the current
    918  * theme, the most visited pages section, and Google-specific elements for a
    919  * Google-provided page.
    920  */
    921 function init() {
    922   tilesContainer = $(IDS.TILES);
    923   notification = $(IDS.NOTIFICATION);
    924   attribution = $(IDS.ATTRIBUTION);
    925   ntpContents = $(IDS.NTP_CONTENTS);
    926 
    927   for (var i = 0; i < NUM_ROWS; i++) {
    928     var row = document.createElement('div');
    929     row.classList.add(CLASSES.ROW);
    930     tilesContainer.appendChild(row);
    931   }
    932 
    933   if (configData.isGooglePage) {
    934     var logo = document.createElement('div');
    935     logo.id = IDS.LOGO;
    936 
    937     fakebox = document.createElement('div');
    938     fakebox.id = IDS.FAKEBOX;
    939     fakebox.innerHTML =
    940         '<input id="' + IDS.FAKEBOX_INPUT +
    941             '" autocomplete="off" tabindex="-1" aria-hidden="true">' +
    942         '<div id=cursor></div>';
    943 
    944     ntpContents.insertBefore(fakebox, ntpContents.firstChild);
    945     ntpContents.insertBefore(logo, ntpContents.firstChild);
    946   } else {
    947     document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
    948   }
    949 
    950   var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
    951   notificationMessage.textContent =
    952       configData.translatedStrings.thumbnailRemovedNotification;
    953   var undoLink = $(IDS.UNDO_LINK);
    954   undoLink.addEventListener('click', onUndo);
    955   registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
    956   undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
    957   var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
    958   restoreAllLink.addEventListener('click', onRestoreAll);
    959   registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
    960   restoreAllLink.textContent =
    961       configData.translatedStrings.restoreThumbnailsShort;
    962   $(IDS.ATTRIBUTION_TEXT).textContent =
    963       configData.translatedStrings.attributionIntro;
    964 
    965   var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
    966   notificationCloseButton.addEventListener('click', hideNotification);
    967 
    968   userInitiatedMostVisitedChange = false;
    969   window.addEventListener('resize', onResize);
    970   onResize();
    971 
    972   var topLevelHandle = getEmbeddedSearchApiHandle();
    973 
    974   ntpApiHandle = topLevelHandle.newTabPage;
    975   ntpApiHandle.onthemechange = onThemeChange;
    976   ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
    977 
    978   ntpApiHandle.oninputstart = onInputStart;
    979   ntpApiHandle.oninputcancel = restoreNtp;
    980 
    981   if (ntpApiHandle.isInputInProgress)
    982     onInputStart();
    983 
    984   onThemeChange();
    985   onMostVisitedChange();
    986 
    987   searchboxApiHandle = topLevelHandle.searchBox;
    988 
    989   if (fakebox) {
    990     // Listener for updating the key capture state.
    991     document.body.onmousedown = function(event) {
    992       if (isFakeboxClick(event))
    993         searchboxApiHandle.startCapturingKeyStrokes();
    994       else if (isFakeboxFocused())
    995         searchboxApiHandle.stopCapturingKeyStrokes();
    996     };
    997     searchboxApiHandle.onkeycapturechange = function() {
    998       setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
    999     };
   1000     var inputbox = $(IDS.FAKEBOX_INPUT);
   1001     if (inputbox) {
   1002       inputbox.onpaste = function(event) {
   1003         event.preventDefault();
   1004         searchboxApiHandle.paste();
   1005       };
   1006       inputbox.ondrop = function(event) {
   1007         event.preventDefault();
   1008         var text = event.dataTransfer.getData('text/plain');
   1009         if (text) {
   1010           searchboxApiHandle.paste(text);
   1011         }
   1012       };
   1013       inputbox.ondragenter = function() {
   1014         setFakeboxDragFocus(true);
   1015       };
   1016       inputbox.ondragleave = function() {
   1017         setFakeboxDragFocus(false);
   1018       };
   1019     }
   1020 
   1021     // Update the fakebox style to match the current key capturing state.
   1022     setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
   1023   }
   1024 
   1025   if (searchboxApiHandle.rtl) {
   1026     $(IDS.NOTIFICATION).dir = 'rtl';
   1027     // Add class for setting alignments based on language directionality.
   1028     document.body.classList.add(CLASSES.RTL);
   1029     $(IDS.TILES).dir = 'rtl';
   1030   }
   1031 }
   1032 
   1033 
   1034 /**
   1035  * Binds event listeners.
   1036  */
   1037 function listen() {
   1038   document.addEventListener('DOMContentLoaded', init);
   1039 }
   1040 
   1041 return {
   1042   init: init,
   1043   listen: listen
   1044 };
   1045 }
   1046 
   1047 if (!window.localNTPUnitTest) {
   1048   LocalNTP().listen();
   1049 }
   1050