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