Give names to all utility processes.
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / local_ntp.js
blob4bf0dd2e707901c15e009c44ad6ca487e7f476f4
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.
6 /**
7  * @fileoverview The local InstantExtended NTP.
8  */
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">
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'
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'
83 /**
84  * Enum for keycodes.
85  * @enum {number}
86  * @const
87  */
88 var KEYCODE = {
89   ENTER: 13
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
106  * The JavaScript button event value for a middle click.
107  * @type {number}
108  * @const
109  */
110 var MIDDLE_MOUSE_BUTTON = 1;
114  * The container for the tile elements.
115  * @type {Element}
116  */
117 var tilesContainer;
121  * The notification displayed when a page is blacklisted.
122  * @type {Element}
123  */
124 var notification;
128  * The container for the theme attribution.
129  * @type {Element}
130  */
131 var attribution;
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;
143  * The container for NTP elements.
144  * @type {Element}
145  */
146 var ntpContents;
150  * The array of rendered tiles, ordered by appearance.
151  * @type {!Array<Tile>}
152  */
153 var tiles = [];
157  * The last blacklisted tile if any, which by definition should not be filler.
158  * @type {?Tile}
159  */
160 var lastBlacklistedTile = null;
164  * The iframe element which is currently keyboard focused, or null.
165  * @type {?Element}
166  */
167 var focusedIframe = null;
171  * True if a page has been blacklisted and we're waiting on the
172  * onmostvisitedchange callback. See renderAllTiles() for how this is used.
173  * @type {boolean}
174  */
175 var isBlacklisting = false;
179  * Current number of tiles columns shown based on the window width, including
180  * those that just contain filler.
181  * @type {number}
182  */
183 var numColumnsShown = 0;
187  * A flag to indicate Most Visited changed caused by user action. If true, then
188  * in renderAllTiles() tiles remain visible so no flickering occurs.
189  * @type {boolean}
190  */
191 var userInitiatedMostVisitedChange = false;
195  * The browser embeddedSearch.newTabPage object.
196  * @type {Object}
197  */
198 var ntpApiHandle;
202  * The browser embeddedSearch.searchBox object.
203  * @type {Object}
204  */
205 var searchboxApiHandle;
209  * The state of the NTP when a query is entered into the Omnibox.
210  * @type {NTP_DISPOSE_STATE}
211  */
212 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
216  * The state of the NTP when a query is entered into the Fakebox.
217  * @type {NTP_DISPOSE_STATE}
218  */
219 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
222 /** @type {number} @const */
223 var MAX_NUM_TILES_TO_SHOW = 8;
226 /** @type {number} @const */
227 var MIN_NUM_COLUMNS = 2;
230 /** @type {number} @const */
231 var MAX_NUM_COLUMNS = 4;
234 /** @type {number} @const */
235 var NUM_ROWS = 2;
239  * Minimum total padding to give to the left and right of the most visited
240  * section. Used to determine how many tiles to show.
241  * @type {number}
242  * @const
243  */
244 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
248  * The filename for a most visited iframe src which shows a page title.
249  * @type {string}
250  * @const
251  */
252 var MOST_VISITED_TITLE_IFRAME = 'title.html';
256  * The filename for a most visited iframe src which shows a thumbnail image.
257  * @type {string}
258  * @const
259  */
260 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
264  * The color of the title in RRGGBBAA format.
265  * @type {?string}
266  */
267 var titleColor = null;
271  * Hide most visited tiles for at most this many milliseconds while painting.
272  * @type {number}
273  * @const
274  */
275 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
279  * A Tile is either a rendering of a Most Visited page or "filler" used to
280  * pad out the section when not enough pages exist.
282  * @param {Element} elem The element for rendering the tile.
283  * @param {Element=} opt_innerElem The element for contents of tile.
284  * @param {Element=} opt_titleElem The element for rendering the title.
285  * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail.
286  * @param {number=} opt_rid The RID for the corresponding Most Visited page.
287  *     Should only be left unspecified when creating a filler tile.
288  * @constructor
289  */
290 function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) {
291   /** @type {Element} */
292   this.elem = elem;
294   /** @type {Element|undefined} */
295   this.innerElem = opt_innerElem;
297   /** @type {Element|undefined} */
298   this.titleElem = opt_titleElem;
300   /** @type {Element|undefined} */
301   this.thumbnailElem = opt_thumbnailElem;
303   /** @type {number|undefined} */
304   this.rid = opt_rid;
309  * Heuristic to determine whether a theme should be considered to be dark, so
310  * the colors of various UI elements can be adjusted.
311  * @param {ThemeBackgroundInfo|undefined} info Theme background information.
312  * @return {boolean} Whether the theme is dark.
313  * @private
314  */
315 function getIsThemeDark(info) {
316   if (!info)
317     return false;
318   // Heuristic: light text implies dark theme.
319   var rgba = info.textColorRgba;
320   var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
321   return luminance >= 128;
326  * Updates the NTP based on the current theme.
327  * @private
328  */
329 function renderTheme() {
330   var fakeboxText = $(IDS.FAKEBOX_TEXT);
331   if (fakeboxText) {
332     fakeboxText.innerHTML = '';
333     if (configData.translatedStrings.searchboxPlaceholder) {
334       fakeboxText.textContent =
335           configData.translatedStrings.searchboxPlaceholder;
336     }
337   }
339   var info = ntpApiHandle.themeBackgroundInfo;
340   var isThemeDark = getIsThemeDark(info);
341   ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
342   if (!info) {
343     titleColor = convertToRRGGBBAAColor(NTP_DESIGN.titleColor);
344     return;
345   }
347   if (!info.usingDefaultTheme && info.textColorRgba) {
348     titleColor = convertToRRGGBBAAColor(info.textColorRgba);
349   } else {
350     titleColor = convertToRRGGBBAAColor(isThemeDark ?
351         NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor);
352   }
354   var background = [convertToRGBAColor(info.backgroundColorRgba),
355                     info.imageUrl,
356                     info.imageTiling,
357                     info.imageHorizontalAlignment,
358                     info.imageVerticalAlignment].join(' ').trim();
360   document.body.style.background = background;
361   document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
362   updateThemeAttribution(info.attributionUrl);
363   setCustomThemeStyle(info);
368  * Updates the NTP based on the current theme, then rerenders all tiles.
369  * @private
370  */
371 function onThemeChange() {
372   renderTheme();
373   tilesContainer.innerHTML = '';
374   renderAllTiles();
379  * Updates the NTP style according to theme.
380  * @param {Object=} opt_themeInfo The information about the theme. If it is
381  * omitted the style will be reverted to the default.
382  * @private
383  */
384 function setCustomThemeStyle(opt_themeInfo) {
385   var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
386   var head = document.head;
387   if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
388     ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
389     var themeStyle =
390       '#attribution {' +
391       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
392       '}' +
393       '#mv-msg {' +
394       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
395       '}' +
396       '#mv-notice-links span {' +
397       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
398       '}' +
399       '#mv-notice-x {' +
400       '  -webkit-filter: drop-shadow(0 0 0 ' +
401           convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
402       '}' +
403       '.mv-page-ready .mv-mask {' +
404       '  border: 1px solid ' +
405           convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
406       '}' +
407       '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
408       '  border-color: ' +
409           convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
410       '}';
412     if (customStyleElement) {
413       customStyleElement.textContent = themeStyle;
414     } else {
415       customStyleElement = document.createElement('style');
416       customStyleElement.type = 'text/css';
417       customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
418       customStyleElement.textContent = themeStyle;
419       head.appendChild(customStyleElement);
420     }
422   } else {
423     ntpContents.classList.add(CLASSES.DEFAULT_THEME);
424     if (customStyleElement)
425       head.removeChild(customStyleElement);
426   }
431  * Renders the attribution if the URL is present, otherwise hides it.
432  * @param {string} url The URL of the attribution image, if any.
433  * @private
434  */
435 function updateThemeAttribution(url) {
436   if (!url) {
437     setAttributionVisibility_(false);
438     return;
439   }
441   var attributionImage = attribution.querySelector('img');
442   if (!attributionImage) {
443     attributionImage = new Image();
444     attribution.appendChild(attributionImage);
445   }
446   attributionImage.style.content = url;
447   setAttributionVisibility_(true);
452  * Sets the visibility of the theme attribution.
453  * @param {boolean} show True to show the attribution.
454  * @private
455  */
456 function setAttributionVisibility_(show) {
457   if (attribution) {
458     attribution.style.display = show ? '' : 'none';
459   }
463  /**
464  * Converts an Array of color components into RRGGBBAA format.
465  * @param {Array<number>} color Array of rgba color components.
466  * @return {string} Color string in RRGGBBAA format.
467  * @private
468  */
469 function convertToRRGGBBAAColor(color) {
470   return color.map(function(t) {
471     return ('0' + t.toString(16)).slice(-2);  // To 2-digit, 0-padded hex.
472   }).join('');
476  /**
477  * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
478  * @param {Array<number>} color Array of rgba color components.
479  * @return {string} CSS color in RGBA format.
480  * @private
481  */
482 function convertToRGBAColor(color) {
483   return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
484                     color[3] / 255 + ')';
489  * Called when page data change.
490  */
491 function onMostVisitedChange() {
492   renderAllTiles();
497  * Rerenders all tiles based on Most Visited page data.
498  */
499 function renderAllTiles() {
500   if (isBlacklisting) {
501     // Trigger the blacklist animation, which then triggers reloadAllTiles().
502     var lastBlacklistedTileElem = lastBlacklistedTile.elem;
503     lastBlacklistedTileElem.addEventListener(
504         'webkitTransitionEnd', blacklistAnimationDone);
505     lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST);
506   } else {
507     reloadAllTiles();
508   }
513  * Handles the end of the blacklist animation by showing the notification and
514  * re-rendering the new set of tiles.
515  * @param {Event} e The associated event.
516  */
517 function blacklistAnimationDone(e) {
518   if (e.propertyName != 'width') {
519     return;
520   }
521   showNotification();
522   isBlacklisting = false;
523   tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
524   lastBlacklistedTile.elem.removeEventListener(
525       'webkitTransitionEnd', blacklistAnimationDone);
526   // Need to call explicitly to re-render the tiles, since the initial
527   // renderAllTiles() issued by the blacklist function only triggered the
528   // animation.
529   reloadAllTiles();
534  * Fetches new data, creates, and renders tiles.
535  */
536 function reloadAllTiles() {
537   var pages = ntpApiHandle.mostVisited;
539   tiles = [];
540   for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i)
541     tiles.push(createTile(pages[i], i));
543   tilesContainer.innerHTML = '';
544   renderAndShowTiles();
549  * Binds onload events for a tile's internal iframe elements.
550  * @param {Tile} tile The main tile to bind events to.
551  * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible
552  *   the moment all tiles are loaded.
553  */
554 function bindTileOnloadEvents(tile, tileVisibilityBarrier) {
555   if (tile.titleElem) {
556     tileVisibilityBarrier.add();
557     tile.titleElem.onload = function() {
558       tileVisibilityBarrier.remove();
559     };
560   }
561   if (tile.thumbnailElem) {
562     tileVisibilityBarrier.add();
563     tile.thumbnailElem.onload = function() {
564       tile.elem.classList.add(CLASSES.PAGE_READY);
565       tileVisibilityBarrier.remove();
566     };
567   }
572  * Renders the current list of visible tiles to DOM, and hides tiles that are
573  * already in the DOM but should not be seen.
574  */
575 function renderAndShowTiles() {
576   var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length;
577   // Only add visible tiles to the DOM, to avoid creating invisible tiles that
578   // produce meaningless impression metrics. However, if a tile becomes
579   // invisible then we leave it in DOM to prevent reload if it's shown again.
580   var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS);
582   // If we need to render new tiles, manage the visibility to hide intermediate
583   // load states of the iframes.
584   if (numExisting < numDesired) {
585     var showAll = function() {
586       for (var i = 0; i < numDesired; ++i) {
587         if (tiles[i].titleElem || tiles[i].thumbnailElem)
588           tiles[i].elem.classList.add(CLASSES.PAGE_READY);
589       }
590     };
591     var tileVisibilityBarrier = new Barrier(showAll);
593     if (!userInitiatedMostVisitedChange) {
594       // Make titleContainer invisible, but still taking up space.
595       // titleContainer becomes visible again (1) on timeout, or (2) when all
596       // tiles finish loading (using tileVisibilityBarrier).
597       window.setTimeout(function() {
598         tileVisibilityBarrier.cancel();
599         showAll();
600       }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
601     }
602     userInitiatedMostVisitedChange = false;
604     for (var i = numExisting; i < numDesired; ++i) {
605       bindTileOnloadEvents(tiles[i], tileVisibilityBarrier);
606       tilesContainer.appendChild(tiles[i].elem);
607     }
608   }
610   // Show only the desired tiles. Note that .hidden does not work for
611   // inline-block elements like tiles[i].elem.
612   for (var i = 0; i < numDesired; ++i)
613     tiles[i].elem.style.display = 'inline-block';
614   // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs
615   // when window is downsized).
616   for (; i < numExisting; ++i)
617     tiles[i].elem.style.display = 'none';
622  * Builds a URL to display a most visited tile title in an iframe.
623  * @param {number} rid The restricted ID.
624  * @param {number} position The position of the iframe in the UI.
625  * @return {string} An URL to display the most visited title in an iframe.
626  */
627 function getMostVisitedTitleIframeUrl(rid, position) {
628   var url = 'chrome-search://most-visited/' +
629       encodeURIComponent(MOST_VISITED_TITLE_IFRAME);
630   var params = [
631       'rid=' + encodeURIComponent(rid),
632       'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
633       'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
634       'c=' + encodeURIComponent(titleColor),
635       'pos=' + encodeURIComponent(position)];
636   if (NTP_DESIGN.titleTextAlign)
637     params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign));
638   if (NTP_DESIGN.titleTextFade)
639     params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade));
640   if (NTP_DESIGN.numTitleLines > 1)
641     params.push('ntl=' + NTP_DESIGN.numTitleLines);
642   return url + '?' + params.join('&');
647  * Builds a URL to display a most visited tile thumbnail in an iframe.
648  * @param {number} rid The restricted ID.
649  * @param {number} position The position of the iframe in the UI.
650  * @return {string} An URL to display the most visited thumbnail in an iframe.
651  */
652 function getMostVisitedThumbnailIframeUrl(rid, position) {
653   var url = 'chrome-search://most-visited/' +
654       encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME);
655   var colorString = convertToRRGGBBAAColor(NTP_DESIGN.thumbnailTextColor);
656   var params = [
657       'rid=' + encodeURIComponent(rid),
658       'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
659       'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
660       'c=' + encodeURIComponent(colorString),
661       'pos=' + encodeURIComponent(position)];
662   if (NTP_DESIGN.thumbnailFallback)
663     params.push('etfb=1');
664   if (configData.useIcons)
665     params.push('icons=1');
666   return url + '?' + params.join('&');
671  * Creates a Tile with the specified page data. If no data is provided, a
672  * filler Tile is created.
673  * @param {?Object} page The page data.
674  * @param {number} position The position of the tile.
675  * @return {Tile} The new Tile.
676  */
677 function createTile(page, position) {
678   var tileElem = document.createElement('div');
679   tileElem.classList.add(CLASSES.TILE);
680   // Prevent tile from being selected (and highlighted) when areas outside the
681   // iframes are clicked.
682   tileElem.addEventListener('mousedown', function(e) {
683     e.preventDefault();
684   });
686   if (!page) {
687     return new Tile(tileElem);
688   }
690   var rid = page.rid;
691   tileElem.classList.add(CLASSES.PAGE);
693   var navigateFunction = function(e) {
694     e.preventDefault();
695     ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e));
696   };
698   // The click handler for navigating to the page identified by the RID.
699   tileElem.addEventListener('click', navigateFunction);
701   // Container of tile contents.
702   var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER);
704   // The iframe which renders the page title.
705   var titleElem = document.createElement('iframe');
706   // Enable tab navigation on the iframe, which will move the selection to the
707   // link element (which also has a tabindex).
708   titleElem.tabIndex = '0';
710   // Make the iframe presentational for accessibility so screen readers perceive
711   // the iframe content as just part of the same page.
712   titleElem.setAttribute('role', 'presentation');
714   // Why iframes have IDs:
715   //
716   // On navigating back to the NTP we see several onmostvisitedchange() events
717   // in series with incrementing RIDs. After the first event, a set of iframes
718   // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get
719   // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1.
720   // Now due to crbug.com/68841, Chrome incorrectly loads the content for the
721   // first set of iframes into the most recent set of iframes.
722   //
723   // Giving iframes distinct ids seems to cause some invalidation and prevent
724   // associating the incorrect data.
725   //
726   // TODO(jered): Find and fix the root (probably Blink) bug.
728   // Keep this ID here. See comment above.
729   titleElem.id = 'title-' + rid;
730   titleElem.className = CLASSES.TITLE;
731   titleElem.src = getMostVisitedTitleIframeUrl(rid, position);
732   innerElem.appendChild(titleElem);
734   // A fallback element for missing thumbnails.
735   if (NTP_DESIGN.thumbnailFallback) {
736     var fallbackElem = createAndAppendElement(
737         innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK);
738     if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT)
739       createAndAppendElement(fallbackElem, 'div', CLASSES.DOT);
740   }
742   // The iframe which renders either a thumbnail or domain element.
743   var thumbnailElem = document.createElement('iframe');
744   thumbnailElem.tabIndex = '-1';
745   thumbnailElem.setAttribute('aria-hidden', 'true');
746   // Keep this ID here. See comment above.
747   thumbnailElem.id = 'thumb-' + rid;
748   thumbnailElem.className = CLASSES.THUMBNAIL;
749   thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position);
750   innerElem.appendChild(thumbnailElem);
752   // The button used to blacklist this page.
753   var blacklistButton = createAndAppendElement(
754       innerElem, 'div', CLASSES.BLACKLIST_BUTTON);
755   createAndAppendElement(
756       blacklistButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
757   var blacklistFunction = generateBlacklistFunction(rid);
758   blacklistButton.addEventListener('click', blacklistFunction);
759   blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
761   // A helper mask on top of the tile that is used to create hover border
762   // and/or to darken the thumbnail on focus.
763   var maskElement = createAndAppendElement(
764       innerElem, 'div', CLASSES.THUMBNAIL_MASK);
766   // The page favicon, or a fallback.
767   if (NTP_DESIGN.showFavicon) {
768     var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON);
769     if (page.faviconUrl) {
770       var fi = document.createElement('img');
771       fi.src = page.faviconUrl;
772       // Set the title to empty so screen readers won't say the image name.
773       fi.title = '';
774       fi.addEventListener('error', function(ev) {
775         favicon.removeChild(fi);
776         favicon.classList.add(CLASSES.FAVICON_FALLBACK);
777       });
778       favicon.appendChild(fi);
779     } else {
780       favicon.classList.add(CLASSES.FAVICON_FALLBACK);
781     }
782   }
783   return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid);
788  * Generates a function to be called when the page with the corresponding RID
789  * is blacklisted.
790  * @param {number} rid The RID of the page being blacklisted.
791  * @return {function(Event=)} A function which handles the blacklisting of the
792  *     page by updating state variables and notifying Chrome.
793  */
794 function generateBlacklistFunction(rid) {
795   return function(e) {
796     // Prevent navigation when the page is being blacklisted.
797     if (e)
798       e.stopPropagation();
800     userInitiatedMostVisitedChange = true;
801     isBlacklisting = true;
802     tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
803     lastBlacklistedTile = getTileByRid(rid);
804     ntpApiHandle.deleteMostVisitedItem(rid);
805   };
810  * Shows the blacklist notification and triggers a delay to hide it.
811  */
812 function showNotification() {
813   notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
814   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
815   notification.scrollTop;
816   notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
821  * Hides the blacklist notification.
822  */
823 function hideNotification() {
824   notification.classList.add(CLASSES.HIDE_NOTIFICATION);
825   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
830  * Handles a click on the notification undo link by hiding the notification and
831  * informing Chrome.
832  */
833 function onUndo() {
834   userInitiatedMostVisitedChange = true;
835   hideNotification();
836   var lastBlacklistedRID = lastBlacklistedTile.rid;
837   if (typeof lastBlacklistedRID != 'undefined')
838     ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
843  * Handles a click on the restore all notification link by hiding the
844  * notification and informing Chrome.
845  */
846 function onRestoreAll() {
847   userInitiatedMostVisitedChange = true;
848   hideNotification();
849   ntpApiHandle.undoAllMostVisitedDeletions();
854  * Recomputes the number of tile columns, and width of various contents based
855  * on the width of the window.
856  * @return {boolean} Whether the number of tile columns has changed.
857  */
858 function updateContentWidth() {
859   var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
860   // If innerWidth is zero, then use the maximum snap size.
861   var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
862       NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
863   var innerWidth = window.innerWidth || maxSnapSize;
864   // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
865   var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
866       NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING;
867   var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
868   if (newNumColumns < MIN_NUM_COLUMNS)
869     newNumColumns = MIN_NUM_COLUMNS;
870   else if (newNumColumns > MAX_NUM_COLUMNS)
871     newNumColumns = MAX_NUM_COLUMNS;
873   if (numColumnsShown === newNumColumns)
874     return false;
876   numColumnsShown = newNumColumns;
877   var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
878   tilesContainer.style.width = tilesContainerWidth + 'px';
879   if (fakebox) {
880     // -2 to account for border.
881     var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
882     fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2;
883     fakebox.style.width = fakeboxWidth + 'px';
884   }
885   return true;
890  * Resizes elements because the number of tile columns may need to change in
891  * response to resizing. Also shows or hides extra tiles tiles according to the
892  * new width of the page.
893  */
894 function onResize() {
895   if (updateContentWidth()) {
896     // Render without clearing tiles.
897     renderAndShowTiles();
898   }
903  * Returns the tile corresponding to the specified page RID.
904  * @param {number} rid The page RID being looked up.
905  * @return {Tile} The corresponding tile.
906  */
907 function getTileByRid(rid) {
908   for (var i = 0, length = tiles.length; i < length; ++i) {
909     var tile = tiles[i];
910     if (tile.rid == rid)
911       return tile;
912   }
913   return null;
918  * Handles new input by disposing the NTP, according to where the input was
919  * entered.
920  */
921 function onInputStart() {
922   if (fakebox && isFakeboxFocused()) {
923     setFakeboxFocus(false);
924     setFakeboxDragFocus(false);
925     disposeNtp(true);
926   } else if (!isFakeboxFocused()) {
927     disposeNtp(false);
928   }
933  * Disposes the NTP, according to where the input was entered.
934  * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
935  */
936 function disposeNtp(wasFakeboxInput) {
937   var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
938   if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
939     setFakeboxActive(false);
940   else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
941     setFakeboxAndLogoVisibility(false);
946  * Restores the NTP (re-enables the fakebox and unhides the logo.)
947  */
948 function restoreNtp() {
949   setFakeboxActive(true);
950   setFakeboxAndLogoVisibility(true);
955  * @param {boolean} focus True to focus the fakebox.
956  */
957 function setFakeboxFocus(focus) {
958   document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
962  * @param {boolean} focus True to show a dragging focus to the fakebox.
963  */
964 function setFakeboxDragFocus(focus) {
965   document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
969  * @return {boolean} True if the fakebox has focus.
970  */
971 function isFakeboxFocused() {
972   return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
973       document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
978  * @param {boolean} enable True to enable the fakebox.
979  */
980 function setFakeboxActive(enable) {
981   document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
986  * @param {!Event} event The click event.
987  * @return {boolean} True if the click occurred in an enabled fakebox.
988  */
989 function isFakeboxClick(event) {
990   return fakebox.contains(event.target) &&
991       !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
996  * @param {boolean} show True to show the fakebox and logo.
997  */
998 function setFakeboxAndLogoVisibility(show) {
999   document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
1004  * Shortcut for document.getElementById.
1005  * @param {string} id of the element.
1006  * @return {HTMLElement} with the id.
1007  */
1008 function $(id) {
1009   return document.getElementById(id);
1014  * Utility function which creates an element with an optional classname and
1015  * appends it to the specified parent.
1016  * @param {Element} parent The parent to append the new element.
1017  * @param {string} name The name of the new element.
1018  * @param {string=} opt_class The optional classname of the new element.
1019  * @return {Element} The new element.
1020  */
1021 function createAndAppendElement(parent, name, opt_class) {
1022   var child = document.createElement(name);
1023   if (opt_class)
1024     child.classList.add(opt_class);
1025   parent.appendChild(child);
1026   return child;
1031  * Removes a node from its parent.
1032  * @param {Node} node The node to remove.
1033  */
1034 function removeNode(node) {
1035   node.parentNode.removeChild(node);
1040  * @param {!Element} element The element to register the handler for.
1041  * @param {number} keycode The keycode of the key to register.
1042  * @param {!Function} handler The key handler to register.
1043  */
1044 function registerKeyHandler(element, keycode, handler) {
1045   element.addEventListener('keydown', function(event) {
1046     if (event.keyCode == keycode)
1047       handler(event);
1048   });
1053  * @return {Object} the handle to the embeddedSearch API.
1054  */
1055 function getEmbeddedSearchApiHandle() {
1056   if (window.cideb)
1057     return window.cideb;
1058   if (window.chrome && window.chrome.embeddedSearch)
1059     return window.chrome.embeddedSearch;
1060   return null;
1065  * Event handler for the focus changed and blacklist messages on link elements.
1066  * Used to toggle visual treatment on the tiles (depending on the message).
1067  * @param {Event} event Event received.
1068  */
1069 function handlePostMessage(event) {
1070   if (event.origin !== 'chrome-search://most-visited')
1071     return;
1073   if (event.data === 'linkFocused') {
1074     var activeElement = document.activeElement;
1075     if (activeElement.classList.contains(CLASSES.TITLE)) {
1076       activeElement.classList.add(CLASSES.FOCUSED);
1077       focusedIframe = activeElement;
1078     }
1079   } else if (event.data === 'linkBlurred') {
1080     if (focusedIframe)
1081       focusedIframe.classList.remove(CLASSES.FOCUSED);
1082     focusedIframe = null;
1083   } else if (event.data.indexOf('tileBlacklisted') === 0) {
1084     var tilePosition = event.data.split(',')[1];
1085     if (tilePosition)
1086       generateBlacklistFunction(tiles[parseInt(tilePosition, 10)].rid)();
1087   }
1092  * Prepares the New Tab Page by adding listeners, rendering the current
1093  * theme, the most visited pages section, and Google-specific elements for a
1094  * Google-provided page.
1095  */
1096 function init() {
1097   tilesContainer = $(IDS.TILES);
1098   notification = $(IDS.NOTIFICATION);
1099   attribution = $(IDS.ATTRIBUTION);
1100   ntpContents = $(IDS.NTP_CONTENTS);
1102   if (configData.isGooglePage) {
1103     var logo = document.createElement('div');
1104     logo.id = IDS.LOGO;
1105     logo.title = 'Google';
1107     fakebox = document.createElement('div');
1108     fakebox.id = IDS.FAKEBOX;
1109     var fakeboxHtml = [];
1110     fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
1111     fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
1112         '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
1113     fakeboxHtml.push('<div id="cursor"></div>');
1114     fakebox.innerHTML = fakeboxHtml.join('');
1116     ntpContents.insertBefore(fakebox, ntpContents.firstChild);
1117     ntpContents.insertBefore(logo, ntpContents.firstChild);
1118   } else {
1119     document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
1120   }
1122   // Modify design for experimental icon NTP, if specified.
1123   if (configData.useIcons)
1124     modifyNtpDesignForIcons();
1125   document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass);
1127   // Hide notifications after fade out, so we can't focus on links via keyboard.
1128   notification.addEventListener('webkitTransitionEnd', hideNotification);
1130   var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
1131   notificationMessage.textContent =
1132       configData.translatedStrings.thumbnailRemovedNotification;
1134   var undoLink = $(IDS.UNDO_LINK);
1135   undoLink.addEventListener('click', onUndo);
1136   registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
1137   undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
1139   var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
1140   restoreAllLink.addEventListener('click', onRestoreAll);
1141   registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
1142   restoreAllLink.textContent =
1143       configData.translatedStrings.restoreThumbnailsShort;
1145   $(IDS.ATTRIBUTION_TEXT).textContent =
1146       configData.translatedStrings.attributionIntro;
1148   var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
1149   createAndAppendElement(
1150       notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
1151   notificationCloseButton.addEventListener('click', hideNotification);
1153   window.addEventListener('resize', onResize);
1154   updateContentWidth();
1156   var topLevelHandle = getEmbeddedSearchApiHandle();
1158   ntpApiHandle = topLevelHandle.newTabPage;
1159   ntpApiHandle.onthemechange = onThemeChange;
1160   ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
1162   ntpApiHandle.oninputstart = onInputStart;
1163   ntpApiHandle.oninputcancel = restoreNtp;
1165   if (ntpApiHandle.isInputInProgress)
1166     onInputStart();
1168   renderTheme();
1169   renderAllTiles();
1171   searchboxApiHandle = topLevelHandle.searchBox;
1173   if (fakebox) {
1174     // Listener for updating the key capture state.
1175     document.body.onmousedown = function(event) {
1176       if (isFakeboxClick(event))
1177         searchboxApiHandle.startCapturingKeyStrokes();
1178       else if (isFakeboxFocused())
1179         searchboxApiHandle.stopCapturingKeyStrokes();
1180     };
1181     searchboxApiHandle.onkeycapturechange = function() {
1182       setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1183     };
1184     var inputbox = $(IDS.FAKEBOX_INPUT);
1185     if (inputbox) {
1186       inputbox.onpaste = function(event) {
1187         event.preventDefault();
1188         // Send pasted text to Omnibox.
1189         var text = event.clipboardData.getData('text/plain');
1190         if (text)
1191           searchboxApiHandle.paste(text);
1192       };
1193       inputbox.ondrop = function(event) {
1194         event.preventDefault();
1195         var text = event.dataTransfer.getData('text/plain');
1196         if (text) {
1197           searchboxApiHandle.paste(text);
1198         }
1199         setFakeboxDragFocus(false);
1200       };
1201       inputbox.ondragenter = function() {
1202         setFakeboxDragFocus(true);
1203       };
1204       inputbox.ondragleave = function() {
1205         setFakeboxDragFocus(false);
1206       };
1207     }
1209     // Update the fakebox style to match the current key capturing state.
1210     setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1211   }
1213   if (searchboxApiHandle.rtl) {
1214     $(IDS.NOTIFICATION).dir = 'rtl';
1215     // Grabbing the root HTML element.
1216     document.documentElement.setAttribute('dir', 'rtl');
1217     // Add class for setting alignments based on language directionality.
1218     document.documentElement.classList.add(CLASSES.RTL);
1219     $(IDS.TILES).dir = 'rtl';
1220   }
1222   window.addEventListener('message', handlePostMessage);
1227  * Binds event listeners.
1228  */
1229 function listen() {
1230   document.addEventListener('DOMContentLoaded', init);
1233 return {
1234   init: init,
1235   listen: listen
1239 if (!window.localNTPUnitTest) {
1240   LocalNTP().listen();