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