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