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