Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / local_ntp.js
blob3db2d43f07c293e864418b8003eb58aa96f6ebd7
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.
6 /**
7 * @fileoverview The local InstantExtended NTP.
8 */
11 /**
12 * Controls rendering the new tab page for InstantExtended.
13 * @return {Object} A limited interface for testing the local NTP.
15 function LocalNTP() {
16 <include src="../../../../ui/webui/resources/js/assert.js">
17 <include src="local_ntp_design.js">
18 <include src="local_ntp_util.js">
19 <include src="window_disposition_util.js">
22 /**
23 * Enum for classnames.
24 * @enum {string}
25 * @const
27 var CLASSES = {
28 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
29 BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation
30 BLACKLIST_BUTTON: 'mv-x',
31 BLACKLIST_BUTTON_INNER: 'mv-x-inner',
32 DARK: 'dark',
33 DEFAULT_THEME: 'default-theme',
34 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
35 DOT: 'dot',
36 FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive
37 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox
38 // Applies drag focus style to the fakebox
39 FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
40 FAVICON: 'mv-favicon',
41 FAVICON_FALLBACK: 'mv-favicon-fallback',
42 FOCUSED: 'mv-focused',
43 HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation
44 HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo',
45 HIDE_NOTIFICATION: 'mv-notice-hide',
46 // Vertically centers the most visited section for a non-Google provided page.
47 NON_GOOGLE_PAGE: 'non-google-page',
48 PAGE: 'mv-page', // page tiles
49 PAGE_READY: 'mv-page-ready', // page tile when ready
50 RTL: 'rtl', // Right-to-left language text.
51 THUMBNAIL: 'mv-thumb',
52 THUMBNAIL_FALLBACK: 'mv-thumb-fallback',
53 THUMBNAIL_MASK: 'mv-mask',
54 TILE: 'mv-tile',
55 TILE_INNER: 'mv-tile-inner',
56 TITLE: 'mv-title'
60 /**
61 * Enum for HTML element ids.
62 * @enum {string}
63 * @const
65 var IDS = {
66 ATTRIBUTION: 'attribution',
67 ATTRIBUTION_TEXT: 'attribution-text',
68 CUSTOM_THEME_STYLE: 'ct-style',
69 FAKEBOX: 'fakebox',
70 FAKEBOX_INPUT: 'fakebox-input',
71 FAKEBOX_TEXT: 'fakebox-text',
72 LOGO: 'logo',
73 NOTIFICATION: 'mv-notice',
74 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x',
75 NOTIFICATION_MESSAGE: 'mv-msg',
76 NTP_CONTENTS: 'ntp-contents',
77 RESTORE_ALL_LINK: 'mv-restore',
78 TILES: 'mv-tiles',
79 UNDO_LINK: 'mv-undo'
83 /**
84 * Enum for keycodes.
85 * @enum {number}
86 * @const
88 var KEYCODE = {
89 ENTER: 13
93 /**
94 * Enum for the state of the NTP when it is disposed.
95 * @enum {number}
96 * @const
98 var NTP_DISPOSE_STATE = {
99 NONE: 0, // Preserve the NTP appearance and functionality
100 DISABLE_FAKEBOX: 1,
101 HIDE_FAKEBOX_AND_LOGO: 2
106 * The JavaScript button event value for a middle click.
107 * @type {number}
108 * @const
110 var MIDDLE_MOUSE_BUTTON = 1;
114 * Specifications for the NTP design.
115 * @const {NtpDesign}
117 var NTP_DESIGN = getNtpDesign(configData.ntpDesignName);
121 * The container for the tile elements.
122 * @type {Element}
124 var tilesContainer;
128 * The notification displayed when a page is blacklisted.
129 * @type {Element}
131 var notification;
135 * The container for the theme attribution.
136 * @type {Element}
138 var attribution;
142 * The "fakebox" - an input field that looks like a regular searchbox. When it
143 * is focused, any text the user types goes directly into the omnibox.
144 * @type {Element}
146 var fakebox;
150 * The container for NTP elements.
151 * @type {Element}
153 var ntpContents;
157 * The array of rendered tiles, ordered by appearance.
158 * @type {!Array.<Tile>}
160 var tiles = [];
164 * The last blacklisted tile if any, which by definition should not be filler.
165 * @type {?Tile}
167 var lastBlacklistedTile = null;
171 * The iframe element which is currently keyboard focused, or null.
172 * @type {?Element}
174 var focusedIframe = null;
178 * True if a page has been blacklisted and we're waiting on the
179 * onmostvisitedchange callback. See onMostVisitedChange() for how this
180 * is used.
181 * @type {boolean}
183 var isBlacklisting = false;
187 * Current number of tiles columns shown based on the window width, including
188 * those that just contain filler.
189 * @type {number}
191 var numColumnsShown = 0;
195 * A flag to indicate Most Visited changed caused by user action. If true, then
196 * in onMostVisitedChange() tiles remain visible so no flickering occurs.
197 * @type {boolean}
199 var userInitiatedMostVisitedChange = false;
203 * The browser embeddedSearch.newTabPage object.
204 * @type {Object}
206 var ntpApiHandle;
210 * The browser embeddedSearch.searchBox object.
211 * @type {Object}
213 var searchboxApiHandle;
217 * The state of the NTP when a query is entered into the Omnibox.
218 * @type {NTP_DISPOSE_STATE}
220 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
224 * The state of the NTP when a query is entered into the Fakebox.
225 * @type {NTP_DISPOSE_STATE}
227 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
230 /** @type {number} @const */
231 var MAX_NUM_TILES_TO_SHOW = 8;
234 /** @type {number} @const */
235 var MIN_NUM_COLUMNS = 2;
238 /** @type {number} @const */
239 var MAX_NUM_COLUMNS = 4;
242 /** @type {number} @const */
243 var NUM_ROWS = 2;
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
252 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
256 * The filename for a most visited iframe src which shows a page title.
257 * @type {string}
258 * @const
260 var MOST_VISITED_TITLE_IFRAME = 'title.html';
264 * The filename for a most visited iframe src which shows a thumbnail image.
265 * @type {string}
266 * @const
268 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
272 * The color of the title in RRGGBBAA format.
273 * @type {?string}
275 var titleColor = null;
279 * Hide most visited tiles for at most this many milliseconds while painting.
280 * @type {number}
281 * @const
283 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
287 * A Tile is either a rendering of a Most Visited page or "filler" used to
288 * pad out the section when not enough pages exist.
290 * @param {Element} elem The element for rendering the tile.
291 * @param {Element=} opt_innerElem The element for contents of tile.
292 * @param {Element=} opt_titleElem The element for rendering the title.
293 * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail.
294 * @param {number=} opt_rid The RID for the corresponding Most Visited page.
295 * Should only be left unspecified when creating a filler tile.
296 * @constructor
298 function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) {
299 /** @type {Element} */
300 this.elem = elem;
302 /** @type {Element|undefined} */
303 this.innerElem = opt_innerElem;
305 /** @type {Element|undefined} */
306 this.titleElem = opt_titleElem;
308 /** @type {Element|undefined} */
309 this.thumbnailElem = opt_thumbnailElem;
311 /** @type {number|undefined} */
312 this.rid = opt_rid;
317 * Heuristic to determine whether a theme should be considered to be dark, so
318 * the colors of various UI elements can be adjusted.
319 * @param {ThemeBackgroundInfo|undefined} info Theme background information.
320 * @return {boolean} Whether the theme is dark.
321 * @private
323 function getIsThemeDark(info) {
324 if (!info)
325 return false;
326 // Heuristic: light text implies dark theme.
327 var rgba = info.textColorRgba;
328 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
329 return luminance >= 128;
334 * Updates the NTP based on the current theme.
335 * @private
337 function renderTheme() {
338 var fakeboxText = $(IDS.FAKEBOX_TEXT);
339 if (fakeboxText) {
340 fakeboxText.innerHTML = '';
341 if (NTP_DESIGN.showFakeboxHint &&
342 configData.translatedStrings.searchboxPlaceholder) {
343 fakeboxText.textContent =
344 configData.translatedStrings.searchboxPlaceholder;
348 var info = ntpApiHandle.themeBackgroundInfo;
349 var isThemeDark = getIsThemeDark(info);
350 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
351 if (!info) {
352 titleColor = NTP_DESIGN.titleColor;
353 return;
356 if (!info.usingDefaultTheme && info.textColorRgba) {
357 titleColor = convertToRRGGBBAAColor(info.textColorRgba);
358 } else {
359 titleColor = isThemeDark ?
360 NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor;
363 var background = [convertToRGBAColor(info.backgroundColorRgba),
364 info.imageUrl,
365 info.imageTiling,
366 info.imageHorizontalAlignment,
367 info.imageVerticalAlignment].join(' ').trim();
369 document.body.style.background = background;
370 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
371 updateThemeAttribution(info.attributionUrl);
372 setCustomThemeStyle(info);
377 * Updates the NTP based on the current theme, then rerenders all tiles.
378 * @private
380 function onThemeChange() {
381 renderTheme();
382 tilesContainer.innerHTML = '';
383 renderAndShowTiles();
388 * Updates the NTP style according to theme.
389 * @param {Object=} opt_themeInfo The information about the theme. If it is
390 * omitted the style will be reverted to the default.
391 * @private
393 function setCustomThemeStyle(opt_themeInfo) {
394 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
395 var head = document.head;
396 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
397 ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
398 var themeStyle =
399 '#attribution {' +
400 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
401 '}' +
402 '#mv-msg {' +
403 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
404 '}' +
405 '#mv-notice-links span {' +
406 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
407 '}' +
408 '#mv-notice-x {' +
409 ' -webkit-filter: drop-shadow(0 0 0 ' +
410 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
411 '}' +
412 '.mv-page-ready .mv-mask {' +
413 ' border: 1px solid ' +
414 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
415 '}' +
416 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
417 ' border-color: ' +
418 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
419 '}';
421 if (customStyleElement) {
422 customStyleElement.textContent = themeStyle;
423 } else {
424 customStyleElement = document.createElement('style');
425 customStyleElement.type = 'text/css';
426 customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
427 customStyleElement.textContent = themeStyle;
428 head.appendChild(customStyleElement);
431 } else {
432 ntpContents.classList.add(CLASSES.DEFAULT_THEME);
433 if (customStyleElement)
434 head.removeChild(customStyleElement);
440 * Renders the attribution if the URL is present, otherwise hides it.
441 * @param {string} url The URL of the attribution image, if any.
442 * @private
444 function updateThemeAttribution(url) {
445 if (!url) {
446 setAttributionVisibility_(false);
447 return;
450 var attributionImage = attribution.querySelector('img');
451 if (!attributionImage) {
452 attributionImage = new Image();
453 attribution.appendChild(attributionImage);
455 attributionImage.style.content = url;
456 setAttributionVisibility_(true);
461 * Sets the visibility of the theme attribution.
462 * @param {boolean} show True to show the attribution.
463 * @private
465 function setAttributionVisibility_(show) {
466 if (attribution) {
467 attribution.style.display = show ? '' : 'none';
473 * Converts an Array of color components into RRGGBBAA format.
474 * @param {Array.<number>} color Array of rgba color components.
475 * @return {string} Color string in RRGGBBAA format.
476 * @private
478 function convertToRRGGBBAAColor(color) {
479 return color.map(function(t) {
480 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex.
481 }).join('');
486 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
487 * @param {Array.<number>} color Array of rgba color components.
488 * @return {string} CSS color in RGBA format.
489 * @private
491 function convertToRGBAColor(color) {
492 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
493 color[3] / 255 + ')';
498 * Handles a new set of Most Visited page data.
500 function onMostVisitedChange() {
501 if (isBlacklisting) {
502 // Trigger the blacklist animation, which then triggers reloadAllTiles().
503 var lastBlacklistedTileElem = lastBlacklistedTile.elem;
504 lastBlacklistedTileElem.addEventListener(
505 'webkitTransitionEnd', blacklistAnimationDone);
506 lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST);
507 } else {
508 reloadAllTiles();
514 * Handles the end of the blacklist animation by showing the notification and
515 * re-rendering the new set of tiles.
517 function blacklistAnimationDone() {
518 showNotification();
519 isBlacklisting = false;
520 tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
521 lastBlacklistedTile.elem.removeEventListener(
522 'webkitTransitionEnd', blacklistAnimationDone);
523 // Need to call explicitly to re-render the tiles, since the initial
524 // onmostvisitedchange issued by the blacklist function only triggered
525 // the animation.
526 reloadAllTiles();
531 * Fetches new data, creates, and renders tiles.
533 function reloadAllTiles() {
534 var pages = ntpApiHandle.mostVisited;
536 tiles = [];
537 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i)
538 tiles.push(createTile(pages[i], i));
540 tilesContainer.innerHTML = '';
541 renderAndShowTiles();
546 * Binds onload events for a tile's internal <iframe> elements.
547 * @param {Tile} tile The main tile to bind events to.
548 * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible
549 * the moment all tiles are loaded.
551 function bindTileOnloadEvents(tile, tileVisibilityBarrier) {
552 if (tile.titleElem) {
553 tileVisibilityBarrier.add();
554 tile.titleElem.onload = function() {
555 tileVisibilityBarrier.remove();
558 if (tile.thumbnailElem) {
559 tileVisibilityBarrier.add();
560 tile.thumbnailElem.onload = function() {
561 tile.elem.classList.add(CLASSES.PAGE_READY);
562 tileVisibilityBarrier.remove();
569 * Renders the current list of visible tiles to DOM, and hides tiles that are
570 * already in the DOM but should not be seen.
572 function renderAndShowTiles() {
573 var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length;
574 // Only add visible tiles to the DOM, to avoid creating invisible tiles that
575 // produce meaningless impression metrics. However, if a tile becomes
576 // invisible then we leave it in DOM to prevent reload if it's shown again.
577 var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS);
579 // If we need to render new tiles, manage the visibility to hide intermediate
580 // load states of the <iframe>s.
581 if (numExisting < numDesired) {
582 var showAll = function() {
583 for (var i = 0; i < numDesired; ++i) {
584 if (tiles[i].titleElem || tiles[i].thumbnailElem)
585 tiles[i].elem.classList.add(CLASSES.PAGE_READY);
588 var tileVisibilityBarrier = new Barrier(showAll);
590 if (!userInitiatedMostVisitedChange) {
591 // Make titleContainer invisible, but still taking up space.
592 // titleContainer becomes visible again (1) on timeout, or (2) when all
593 // tiles finish loading (using tileVisibilityBarrier).
594 window.setTimeout(function() {
595 tileVisibilityBarrier.cancel();
596 showAll();
597 }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
599 userInitiatedMostVisitedChange = false;
601 for (var i = numExisting; i < numDesired; ++i) {
602 bindTileOnloadEvents(tiles[i], tileVisibilityBarrier);
603 tilesContainer.appendChild(tiles[i].elem);
607 // Show only the desired tiles. Note that .hidden does not work for
608 // inline-block elements like tiles[i].elem.
609 for (var i = 0; i < numDesired; ++i)
610 tiles[i].elem.style.display = 'inline-block';
611 // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs
612 // when window is downsized).
613 for (; i < numExisting; ++i)
614 tiles[i].elem.style.display = 'none';
619 * Builds a URL to display a most visited tile title in an iframe.
620 * @param {number} rid The restricted ID.
621 * @param {number} position The position of the iframe in the UI.
622 * @return {string} An URL to display the most visited title in an iframe.
624 function getMostVisitedTitleIframeUrl(rid, position) {
625 var url = 'chrome-search://most-visited/' +
626 encodeURIComponent(MOST_VISITED_TITLE_IFRAME);
627 var params = [
628 'rid=' + encodeURIComponent(rid),
629 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
630 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
631 'c=' + encodeURIComponent(titleColor),
632 'pos=' + encodeURIComponent(position)];
633 if (NTP_DESIGN.titleTextAlign)
634 params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign));
635 if (NTP_DESIGN.titleTextFade)
636 params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade));
637 return url + '?' + params.join('&');
642 * Builds a URL to display a most visited tile thumbnail in an iframe.
643 * @param {number} rid The restricted ID.
644 * @param {number} position The position of the iframe in the UI.
645 * @return {string} An URL to display the most visited thumbnail in an iframe.
647 function getMostVisitedThumbnailIframeUrl(rid, position) {
648 var url = 'chrome-search://most-visited/' +
649 encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME);
650 var params = [
651 'rid=' + encodeURIComponent(rid),
652 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
653 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
654 'c=' + encodeURIComponent(NTP_DESIGN.thumbnailTextColor),
655 'pos=' + encodeURIComponent(position)];
656 if (NTP_DESIGN.thumbnailFallback)
657 params.push('etfb=1');
658 return url + '?' + params.join('&');
663 * Creates a Tile with the specified page data. If no data is provided, a
664 * filler Tile is created.
665 * @param {Object} page The page data.
666 * @param {number} position The position of the tile.
667 * @return {Tile} The new Tile.
669 function createTile(page, position) {
670 var tileElem = document.createElement('div');
671 tileElem.classList.add(CLASSES.TILE);
672 // Prevent tile from being selected (and highlighted) when areas outside the
673 // <iframe>s are clicked.
674 tileElem.addEventListener('mousedown', function(e) {
675 e.preventDefault();
677 var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER);
679 if (page) {
680 var rid = page.rid;
681 tileElem.classList.add(CLASSES.PAGE);
683 var navigateFunction = function(e) {
684 e.preventDefault();
685 ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e));
688 // The click handler for navigating to the page identified by the RID.
689 tileElem.addEventListener('click', navigateFunction);
691 // The iframe which renders the page title.
692 var titleElem = document.createElement('iframe');
693 // Enable tab navigation on the iframe, which will move the selection to the
694 // link element (which also has a tabindex).
695 titleElem.tabIndex = '0';
697 // Why iframes have IDs:
699 // On navigating back to the NTP we see several onmostvisitedchange() events
700 // in series with incrementing RIDs. After the first event, a set of iframes
701 // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get
702 // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1.
703 // Now due to crbug.com/68841, Chrome incorrectly loads the content for the
704 // first set of iframes into the most recent set of iframes.
706 // Giving iframes distinct ids seems to cause some invalidation and prevent
707 // associating the incorrect data.
709 // TODO(jered): Find and fix the root (probably Blink) bug.
711 // Keep this ID here. See comment above.
712 titleElem.id = 'title-' + rid;
713 titleElem.className = CLASSES.TITLE;
714 titleElem.src = getMostVisitedTitleIframeUrl(rid, position);
715 innerElem.appendChild(titleElem);
717 // A fallback element for missing thumbnails.
718 if (NTP_DESIGN.thumbnailFallback) {
719 var fallbackElem = createAndAppendElement(
720 innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK);
721 if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT)
722 createAndAppendElement(fallbackElem, 'div', CLASSES.DOT);
725 // The iframe which renders either a thumbnail or domain element.
726 var thumbnailElem = document.createElement('iframe');
727 thumbnailElem.tabIndex = '-1';
728 thumbnailElem.setAttribute('aria-hidden', 'true');
729 // Keep this ID here. See comment above.
730 thumbnailElem.id = 'thumb-' + rid;
731 thumbnailElem.className = CLASSES.THUMBNAIL;
732 thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position);
733 innerElem.appendChild(thumbnailElem);
735 // The button used to blacklist this page.
736 var blacklistButton = createAndAppendElement(
737 innerElem, 'div', CLASSES.BLACKLIST_BUTTON);
738 createAndAppendElement(
739 blacklistButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
740 var blacklistFunction = generateBlacklistFunction(rid);
741 blacklistButton.addEventListener('click', blacklistFunction);
742 blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
744 // A helper mask on top of the tile that is used to create hover border
745 // and/or to darken the thumbnail on focus.
746 var maskElement = createAndAppendElement(
747 innerElem, 'div', CLASSES.THUMBNAIL_MASK);
749 // The page favicon, or a fallback.
750 var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON);
751 if (page.faviconUrl) {
752 favicon.style.backgroundImage = 'url(' + page.faviconUrl + ')';
753 } else {
754 favicon.classList.add(CLASSES.FAVICON_FALLBACK);
756 return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid);
757 } else {
758 return new Tile(tileElem);
764 * Generates a function to be called when the page with the corresponding RID
765 * is blacklisted.
766 * @param {number} rid The RID of the page being blacklisted.
767 * @return {function(Event=)} A function which handles the blacklisting of the
768 * page by updating state variables and notifying Chrome.
770 function generateBlacklistFunction(rid) {
771 return function(e) {
772 // Prevent navigation when the page is being blacklisted.
773 if (e)
774 e.stopPropagation();
776 userInitiatedMostVisitedChange = true;
777 isBlacklisting = true;
778 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
779 lastBlacklistedTile = getTileByRid(rid);
780 ntpApiHandle.deleteMostVisitedItem(rid);
786 * Shows the blacklist notification and triggers a delay to hide it.
788 function showNotification() {
789 notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
790 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
791 notification.scrollTop;
792 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
797 * Hides the blacklist notification.
799 function hideNotification() {
800 notification.classList.add(CLASSES.HIDE_NOTIFICATION);
801 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
806 * Handles a click on the notification undo link by hiding the notification and
807 * informing Chrome.
809 function onUndo() {
810 userInitiatedMostVisitedChange = true;
811 hideNotification();
812 var lastBlacklistedRID = lastBlacklistedTile.rid;
813 if (typeof lastBlacklistedRID != 'undefined')
814 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
819 * Handles a click on the restore all notification link by hiding the
820 * notification and informing Chrome.
822 function onRestoreAll() {
823 userInitiatedMostVisitedChange = true;
824 hideNotification();
825 ntpApiHandle.undoAllMostVisitedDeletions();
830 * Recomputes the number of tile columns, and width of various contents based
831 * on the width of the window.
832 * @return {boolean} Whether the number of tile columns has changed.
834 function updateContentWidth() {
835 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
836 // If innerWidth is zero, then use the maximum snap size.
837 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
838 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
839 var innerWidth = window.innerWidth || maxSnapSize;
840 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
841 var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
842 MIN_TOTAL_HORIZONTAL_PADDING;
843 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
844 if (newNumColumns < MIN_NUM_COLUMNS)
845 newNumColumns = MIN_NUM_COLUMNS;
846 else if (newNumColumns > MAX_NUM_COLUMNS)
847 newNumColumns = MAX_NUM_COLUMNS;
849 if (numColumnsShown === newNumColumns)
850 return false;
852 numColumnsShown = newNumColumns;
853 var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
854 tilesContainer.style.width = tilesContainerWidth + 'px';
855 if (fakebox) {
856 // -2 to account for border.
857 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
858 fakebox.style.width = fakeboxWidth + 'px';
860 return true;
865 * Resizes elements because the number of tile columns may need to change in
866 * response to resizing. Also shows or hides extra tiles tiles according to the
867 * new width of the page.
869 function onResize() {
870 if (updateContentWidth()) {
871 // Render without clearing tiles.
872 renderAndShowTiles();
878 * Returns the tile corresponding to the specified page RID.
879 * @param {number} rid The page RID being looked up.
880 * @return {Tile} The corresponding tile.
882 function getTileByRid(rid) {
883 for (var i = 0, length = tiles.length; i < length; ++i) {
884 var tile = tiles[i];
885 if (tile.rid == rid)
886 return tile;
888 return null;
893 * Handles new input by disposing the NTP, according to where the input was
894 * entered.
896 function onInputStart() {
897 if (fakebox && isFakeboxFocused()) {
898 setFakeboxFocus(false);
899 setFakeboxDragFocus(false);
900 disposeNtp(true);
901 } else if (!isFakeboxFocused()) {
902 disposeNtp(false);
908 * Disposes the NTP, according to where the input was entered.
909 * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
911 function disposeNtp(wasFakeboxInput) {
912 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
913 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
914 setFakeboxActive(false);
915 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
916 setFakeboxAndLogoVisibility(false);
921 * Restores the NTP (re-enables the fakebox and unhides the logo.)
923 function restoreNtp() {
924 setFakeboxActive(true);
925 setFakeboxAndLogoVisibility(true);
930 * @param {boolean} focus True to focus the fakebox.
932 function setFakeboxFocus(focus) {
933 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
937 * @param {boolean} focus True to show a dragging focus to the fakebox.
939 function setFakeboxDragFocus(focus) {
940 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
944 * @return {boolean} True if the fakebox has focus.
946 function isFakeboxFocused() {
947 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
948 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
953 * @param {boolean} enable True to enable the fakebox.
955 function setFakeboxActive(enable) {
956 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
961 * @param {!Event} event The click event.
962 * @return {boolean} True if the click occurred in an enabled fakebox.
964 function isFakeboxClick(event) {
965 return fakebox.contains(event.target) &&
966 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
971 * @param {boolean} show True to show the fakebox and logo.
973 function setFakeboxAndLogoVisibility(show) {
974 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
979 * Shortcut for document.getElementById.
980 * @param {string} id of the element.
981 * @return {HTMLElement} with the id.
983 function $(id) {
984 return document.getElementById(id);
989 * Utility function which creates an element with an optional classname and
990 * appends it to the specified parent.
991 * @param {Element} parent The parent to append the new element.
992 * @param {string} name The name of the new element.
993 * @param {string=} opt_class The optional classname of the new element.
994 * @return {Element} The new element.
996 function createAndAppendElement(parent, name, opt_class) {
997 var child = document.createElement(name);
998 if (opt_class)
999 child.classList.add(opt_class);
1000 parent.appendChild(child);
1001 return child;
1006 * Removes a node from its parent.
1007 * @param {Node} node The node to remove.
1009 function removeNode(node) {
1010 node.parentNode.removeChild(node);
1015 * @param {!Element} element The element to register the handler for.
1016 * @param {number} keycode The keycode of the key to register.
1017 * @param {!Function} handler The key handler to register.
1019 function registerKeyHandler(element, keycode, handler) {
1020 element.addEventListener('keydown', function(event) {
1021 if (event.keyCode == keycode)
1022 handler(event);
1028 * @return {Object} the handle to the embeddedSearch API.
1030 function getEmbeddedSearchApiHandle() {
1031 if (window.cideb)
1032 return window.cideb;
1033 if (window.chrome && window.chrome.embeddedSearch)
1034 return window.chrome.embeddedSearch;
1035 return null;
1040 * Event handler for the focus changed and blacklist messages on link elements.
1041 * Used to toggle visual treatment on the tiles (depending on the message).
1042 * @param {Event} event Event received.
1044 function handlePostMessage(event) {
1045 if (event.origin !== 'chrome-search://most-visited')
1046 return;
1048 if (event.data === 'linkFocused') {
1049 var activeElement = document.activeElement;
1050 if (activeElement.classList.contains(CLASSES.TITLE)) {
1051 activeElement.classList.add(CLASSES.FOCUSED);
1052 focusedIframe = activeElement;
1054 } else if (event.data === 'linkBlurred') {
1055 if (focusedIframe)
1056 focusedIframe.classList.remove(CLASSES.FOCUSED);
1057 focusedIframe = null;
1058 } else if (event.data.indexOf('tileBlacklisted') === 0) {
1059 var tilePosition = event.data.split(',')[1];
1060 if (tilePosition)
1061 generateBlacklistFunction(tiles[parseInt(tilePosition, 10)].rid)();
1067 * Prepares the New Tab Page by adding listeners, rendering the current
1068 * theme, the most visited pages section, and Google-specific elements for a
1069 * Google-provided page.
1071 function init() {
1072 tilesContainer = $(IDS.TILES);
1073 notification = $(IDS.NOTIFICATION);
1074 attribution = $(IDS.ATTRIBUTION);
1075 ntpContents = $(IDS.NTP_CONTENTS);
1077 if (configData.isGooglePage) {
1078 var logo = document.createElement('div');
1079 logo.id = IDS.LOGO;
1081 fakebox = document.createElement('div');
1082 fakebox.id = IDS.FAKEBOX;
1083 var fakeboxHtml = [];
1084 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
1085 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
1086 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
1087 fakeboxHtml.push('<div id="cursor"></div>');
1088 fakebox.innerHTML = fakeboxHtml.join('');
1090 ntpContents.insertBefore(fakebox, ntpContents.firstChild);
1091 ntpContents.insertBefore(logo, ntpContents.firstChild);
1092 } else {
1093 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
1096 // Hide notifications after fade out, so we can't focus on links via keyboard.
1097 notification.addEventListener('webkitTransitionEnd', hideNotification);
1099 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
1100 notificationMessage.textContent =
1101 configData.translatedStrings.thumbnailRemovedNotification;
1103 var undoLink = $(IDS.UNDO_LINK);
1104 undoLink.addEventListener('click', onUndo);
1105 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
1106 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
1108 var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
1109 restoreAllLink.addEventListener('click', onRestoreAll);
1110 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
1111 restoreAllLink.textContent =
1112 configData.translatedStrings.restoreThumbnailsShort;
1114 $(IDS.ATTRIBUTION_TEXT).textContent =
1115 configData.translatedStrings.attributionIntro;
1117 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
1118 createAndAppendElement(
1119 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
1120 notificationCloseButton.addEventListener('click', hideNotification);
1122 window.addEventListener('resize', onResize);
1123 updateContentWidth();
1125 var topLevelHandle = getEmbeddedSearchApiHandle();
1127 ntpApiHandle = topLevelHandle.newTabPage;
1128 ntpApiHandle.onthemechange = onThemeChange;
1129 ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
1131 ntpApiHandle.oninputstart = onInputStart;
1132 ntpApiHandle.oninputcancel = restoreNtp;
1134 if (ntpApiHandle.isInputInProgress)
1135 onInputStart();
1137 renderTheme();
1138 onMostVisitedChange();
1140 searchboxApiHandle = topLevelHandle.searchBox;
1142 if (fakebox) {
1143 // Listener for updating the key capture state.
1144 document.body.onmousedown = function(event) {
1145 if (isFakeboxClick(event))
1146 searchboxApiHandle.startCapturingKeyStrokes();
1147 else if (isFakeboxFocused())
1148 searchboxApiHandle.stopCapturingKeyStrokes();
1150 searchboxApiHandle.onkeycapturechange = function() {
1151 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1153 var inputbox = $(IDS.FAKEBOX_INPUT);
1154 if (inputbox) {
1155 inputbox.onpaste = function(event) {
1156 event.preventDefault();
1157 // Send pasted text to Omnibox.
1158 var text = event.clipboardData.getData('text/plain');
1159 if (text)
1160 searchboxApiHandle.paste(text);
1162 inputbox.ondrop = function(event) {
1163 event.preventDefault();
1164 var text = event.dataTransfer.getData('text/plain');
1165 if (text) {
1166 searchboxApiHandle.paste(text);
1169 inputbox.ondragenter = function() {
1170 setFakeboxDragFocus(true);
1172 inputbox.ondragleave = function() {
1173 setFakeboxDragFocus(false);
1177 // Update the fakebox style to match the current key capturing state.
1178 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1181 if (searchboxApiHandle.rtl) {
1182 $(IDS.NOTIFICATION).dir = 'rtl';
1183 document.body.setAttribute('dir', 'rtl');
1184 // Add class for setting alignments based on language directionality.
1185 document.body.classList.add(CLASSES.RTL);
1186 $(IDS.TILES).dir = 'rtl';
1189 window.addEventListener('message', handlePostMessage);
1194 * Binds event listeners.
1196 function listen() {
1197 document.addEventListener('DOMContentLoaded', init);
1200 return {
1201 init: init,
1202 listen: listen
1206 if (!window.localNTPUnitTest) {
1207 LocalNTP().listen();