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