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