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 */
15function 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 */
27var 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 */
65var 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 */
88var 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 */
98var 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 */
110var MIDDLE_MOUSE_BUTTON = 1;
111
112
113/**
114 * Specifications for the NTP design.
115 * @const {NtpDesign}
116 */
117var NTP_DESIGN = getNtpDesign(configData.ntpDesignName);
118
119
120/**
121 * The container for the tile elements.
122 * @type {Element}
123 */
124var tilesContainer;
125
126
127/**
128 * The notification displayed when a page is blacklisted.
129 * @type {Element}
130 */
131var notification;
132
133
134/**
135 * The container for the theme attribution.
136 * @type {Element}
137 */
138var 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 */
146var fakebox;
147
148
149/**
150 * The container for NTP elements.
151 * @type {Element}
152 */
153var ntpContents;
154
155
156/**
157 * The array of rendered tiles, ordered by appearance.
158 * @type {!Array.<Tile>}
159 */
160var tiles = [];
161
162
163/**
164 * The last blacklisted tile if any, which by definition should not be filler.
165 * @type {?Tile}
166 */
167var lastBlacklistedTile = null;
168
169
170/**
171 * The iframe element which is currently keyboard focused, or null.
172 * @type {?Element}
173 */
174var 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 */
183var 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 */
191var 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 */
199var userInitiatedMostVisitedChange = false;
200
201
202/**
203 * The browser embeddedSearch.newTabPage object.
204 * @type {Object}
205 */
206var ntpApiHandle;
207
208
209/**
210 * The browser embeddedSearch.searchBox object.
211 * @type {Object}
212 */
213var searchboxApiHandle;
214
215
216/**
217 * The state of the NTP when a query is entered into the Omnibox.
218 * @type {NTP_DISPOSE_STATE}
219 */
220var 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 */
227var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
228
229
230/** @type {number} @const */
231var MAX_NUM_TILES_TO_SHOW = 8;
232
233
234/** @type {number} @const */
235var MIN_NUM_COLUMNS = 2;
236
237
238/** @type {number} @const */
239var MAX_NUM_COLUMNS = 4;
240
241
242/** @type {number} @const */
243var 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 */
252var 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 */
260var 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 */
268var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
269
270
271/**
272 * The color of the title in RRGGBBAA format.
273 * @type {?string}
274 */
275var titleColor = null;
276
277
278/**
279 * Hide most visited tiles for at most this many milliseconds while painting.
280 * @type {number}
281 * @const
282 */
283var 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 */
298function 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 */
323function 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 */
337function 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 */
380function 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 */
393function 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 */
444function 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 */
465function 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 */
478function 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 */
491function 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 */
500function 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 */
517function 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 */
533function 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 */
551function 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 */
572function 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 */
624function 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 */
647function 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 */
669function 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 */
770function 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 */
788function 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 */
799function 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 */
809function 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 */
822function 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 */
834function 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 */
869function 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 */
882function 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 */
896function 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 */
911function 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 */
923function restoreNtp() {
924  setFakeboxActive(true);
925  setFakeboxAndLogoVisibility(true);
926}
927
928
929/**
930 * @param {boolean} focus True to focus the fakebox.
931 */
932function 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 */
939function 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 */
946function 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 */
955function 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 */
964function 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 */
973function 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 */
983function $(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 */
996function 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 */
1009function 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 */
1019function 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 */
1030function 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 */
1044function 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 */
1071function 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" type="url" 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 */
1193function listen() {
1194  document.addEventListener('DOMContentLoaded', init);
1195}
1196
1197return {
1198  init: init,
1199  listen: listen
1200};
1201}
1202
1203if (!window.localNTPUnitTest) {
1204  LocalNTP().listen();
1205}
1206