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.
7 * @fileoverview The local InstantExtended NTP.
12 * Controls rendering the new tab page for InstantExtended.
13 * @return {Object} A limited interface for testing the local NTP.
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">
23 * Enum for classnames.
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',
33 DEFAULT_THEME: 'default-theme',
34 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
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',
55 TILE_INNER: 'mv-tile-inner',
61 * Enum for HTML element ids.
66 ATTRIBUTION: 'attribution',
67 ATTRIBUTION_TEXT: 'attribution-text',
68 CUSTOM_THEME_STYLE: 'ct-style',
70 FAKEBOX_INPUT: 'fakebox-input',
71 FAKEBOX_TEXT: 'fakebox-text',
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',
94 * Enum for the state of the NTP when it is disposed.
98 var NTP_DISPOSE_STATE = {
99 NONE: 0, // Preserve the NTP appearance and functionality
101 HIDE_FAKEBOX_AND_LOGO: 2
106 * The JavaScript button event value for a middle click.
110 var MIDDLE_MOUSE_BUTTON = 1;
114 * The container for the tile elements.
121 * The notification displayed when a page is blacklisted.
128 * The container for the theme attribution.
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.
143 * The container for NTP elements.
150 * The array of rendered tiles, ordered by appearance.
151 * @type {!Array<Tile>}
157 * The last blacklisted tile if any, which by definition should not be filler.
160 var lastBlacklistedTile = null;
164 * The iframe element which is currently keyboard focused, or null.
167 var focusedIframe = null;
171 * True if a page has been blacklisted and we're waiting on the
172 * onmostvisitedchange callback. See renderAllTiles() for how this is used.
175 var isBlacklisting = false;
179 * Current number of tiles columns shown based on the window width, including
180 * those that just contain filler.
183 var numColumnsShown = 0;
187 * A flag to indicate Most Visited changed caused by user action. If true, then
188 * in renderAllTiles() tiles remain visible so no flickering occurs.
191 var userInitiatedMostVisitedChange = false;
195 * The browser embeddedSearch.newTabPage object.
202 * The browser embeddedSearch.searchBox object.
205 var searchboxApiHandle;
209 * The state of the NTP when a query is entered into the Omnibox.
210 * @type {NTP_DISPOSE_STATE}
212 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE;
216 * The state of the NTP when a query is entered into the Fakebox.
217 * @type {NTP_DISPOSE_STATE}
219 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO;
222 /** @type {number} @const */
223 var MAX_NUM_TILES_TO_SHOW = 8;
226 /** @type {number} @const */
227 var MIN_NUM_COLUMNS = 2;
230 /** @type {number} @const */
231 var MAX_NUM_COLUMNS = 4;
234 /** @type {number} @const */
239 * Minimum total padding to give to the left and right of the most visited
240 * section. Used to determine how many tiles to show.
244 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
248 * The filename for a most visited iframe src which shows a page title.
252 var MOST_VISITED_TITLE_IFRAME = 'title.html';
256 * The filename for a most visited iframe src which shows a thumbnail image.
260 var MOST_VISITED_THUMBNAIL_IFRAME = 'thumbnail.html';
264 * The color of the title in RRGGBBAA format.
267 var titleColor = null;
271 * Hide most visited tiles for at most this many milliseconds while painting.
275 var MOST_VISITED_PAINT_TIMEOUT_MSEC = 500;
279 * A Tile is either a rendering of a Most Visited page or "filler" used to
280 * pad out the section when not enough pages exist.
282 * @param {Element} elem The element for rendering the tile.
283 * @param {Element=} opt_innerElem The element for contents of tile.
284 * @param {Element=} opt_titleElem The element for rendering the title.
285 * @param {Element=} opt_thumbnailElem The element for rendering the thumbnail.
286 * @param {number=} opt_rid The RID for the corresponding Most Visited page.
287 * Should only be left unspecified when creating a filler tile.
290 function Tile(elem, opt_innerElem, opt_titleElem, opt_thumbnailElem, opt_rid) {
291 /** @type {Element} */
294 /** @type {Element|undefined} */
295 this.innerElem = opt_innerElem;
297 /** @type {Element|undefined} */
298 this.titleElem = opt_titleElem;
300 /** @type {Element|undefined} */
301 this.thumbnailElem = opt_thumbnailElem;
303 /** @type {number|undefined} */
309 * Heuristic to determine whether a theme should be considered to be dark, so
310 * the colors of various UI elements can be adjusted.
311 * @param {ThemeBackgroundInfo|undefined} info Theme background information.
312 * @return {boolean} Whether the theme is dark.
315 function getIsThemeDark(info) {
318 // Heuristic: light text implies dark theme.
319 var rgba = info.textColorRgba;
320 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
321 return luminance >= 128;
326 * Updates the NTP based on the current theme.
329 function renderTheme() {
330 var fakeboxText = $(IDS.FAKEBOX_TEXT);
332 fakeboxText.innerHTML = '';
333 if (configData.translatedStrings.searchboxPlaceholder) {
334 fakeboxText.textContent =
335 configData.translatedStrings.searchboxPlaceholder;
339 var info = ntpApiHandle.themeBackgroundInfo;
340 var isThemeDark = getIsThemeDark(info);
341 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
343 titleColor = convertToRRGGBBAAColor(NTP_DESIGN.titleColor);
347 if (!info.usingDefaultTheme && info.textColorRgba) {
348 titleColor = convertToRRGGBBAAColor(info.textColorRgba);
350 titleColor = convertToRRGGBBAAColor(isThemeDark ?
351 NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor);
354 var background = [convertToRGBAColor(info.backgroundColorRgba),
357 info.imageHorizontalAlignment,
358 info.imageVerticalAlignment].join(' ').trim();
360 document.body.style.background = background;
361 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
362 updateThemeAttribution(info.attributionUrl);
363 setCustomThemeStyle(info);
368 * Updates the NTP based on the current theme, then rerenders all tiles.
371 function onThemeChange() {
373 tilesContainer.innerHTML = '';
379 * Updates the NTP style according to theme.
380 * @param {Object=} opt_themeInfo The information about the theme. If it is
381 * omitted the style will be reverted to the default.
384 function setCustomThemeStyle(opt_themeInfo) {
385 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
386 var head = document.head;
387 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
388 ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
391 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
394 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
396 '#mv-notice-links span {' +
397 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
400 ' -webkit-filter: drop-shadow(0 0 0 ' +
401 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
403 '.mv-page-ready .mv-mask {' +
404 ' border: 1px solid ' +
405 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
407 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
409 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
412 if (customStyleElement) {
413 customStyleElement.textContent = themeStyle;
415 customStyleElement = document.createElement('style');
416 customStyleElement.type = 'text/css';
417 customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
418 customStyleElement.textContent = themeStyle;
419 head.appendChild(customStyleElement);
423 ntpContents.classList.add(CLASSES.DEFAULT_THEME);
424 if (customStyleElement)
425 head.removeChild(customStyleElement);
431 * Renders the attribution if the URL is present, otherwise hides it.
432 * @param {string} url The URL of the attribution image, if any.
435 function updateThemeAttribution(url) {
437 setAttributionVisibility_(false);
441 var attributionImage = attribution.querySelector('img');
442 if (!attributionImage) {
443 attributionImage = new Image();
444 attribution.appendChild(attributionImage);
446 attributionImage.style.content = url;
447 setAttributionVisibility_(true);
452 * Sets the visibility of the theme attribution.
453 * @param {boolean} show True to show the attribution.
456 function setAttributionVisibility_(show) {
458 attribution.style.display = show ? '' : 'none';
464 * Converts an Array of color components into RRGGBBAA format.
465 * @param {Array<number>} color Array of rgba color components.
466 * @return {string} Color string in RRGGBBAA format.
469 function convertToRRGGBBAAColor(color) {
470 return color.map(function(t) {
471 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex.
477 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
478 * @param {Array<number>} color Array of rgba color components.
479 * @return {string} CSS color in RGBA format.
482 function convertToRGBAColor(color) {
483 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
484 color[3] / 255 + ')';
489 * Called when page data change.
491 function onMostVisitedChange() {
497 * Rerenders all tiles based on Most Visited page data.
499 function renderAllTiles() {
500 if (isBlacklisting) {
501 // Trigger the blacklist animation, which then triggers reloadAllTiles().
502 var lastBlacklistedTileElem = lastBlacklistedTile.elem;
503 lastBlacklistedTileElem.addEventListener(
504 'webkitTransitionEnd', blacklistAnimationDone);
505 lastBlacklistedTileElem.classList.add(CLASSES.BLACKLIST);
513 * Handles the end of the blacklist animation by showing the notification and
514 * re-rendering the new set of tiles.
515 * @param {Event} e The associated event.
517 function blacklistAnimationDone(e) {
518 if (e.propertyName != 'width') {
522 isBlacklisting = false;
523 tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
524 lastBlacklistedTile.elem.removeEventListener(
525 'webkitTransitionEnd', blacklistAnimationDone);
526 // Need to call explicitly to re-render the tiles, since the initial
527 // renderAllTiles() issued by the blacklist function only triggered the
534 * Fetches new data, creates, and renders tiles.
536 function reloadAllTiles() {
537 var pages = ntpApiHandle.mostVisited;
540 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i)
541 tiles.push(createTile(pages[i], i));
543 tilesContainer.innerHTML = '';
544 renderAndShowTiles();
549 * Binds onload events for a tile's internal iframe elements.
550 * @param {Tile} tile The main tile to bind events to.
551 * @param {Barrier} tileVisibilityBarrier A barrier to make all tiles visible
552 * the moment all tiles are loaded.
554 function bindTileOnloadEvents(tile, tileVisibilityBarrier) {
555 if (tile.titleElem) {
556 tileVisibilityBarrier.add();
557 tile.titleElem.onload = function() {
558 tileVisibilityBarrier.remove();
561 if (tile.thumbnailElem) {
562 tileVisibilityBarrier.add();
563 tile.thumbnailElem.onload = function() {
564 tile.elem.classList.add(CLASSES.PAGE_READY);
565 tileVisibilityBarrier.remove();
572 * Renders the current list of visible tiles to DOM, and hides tiles that are
573 * already in the DOM but should not be seen.
575 function renderAndShowTiles() {
576 var numExisting = tilesContainer.querySelectorAll('.' + CLASSES.TILE).length;
577 // Only add visible tiles to the DOM, to avoid creating invisible tiles that
578 // produce meaningless impression metrics. However, if a tile becomes
579 // invisible then we leave it in DOM to prevent reload if it's shown again.
580 var numDesired = Math.min(tiles.length, numColumnsShown * NUM_ROWS);
582 // If we need to render new tiles, manage the visibility to hide intermediate
583 // load states of the iframes.
584 if (numExisting < numDesired) {
585 var showAll = function() {
586 for (var i = 0; i < numDesired; ++i) {
587 if (tiles[i].titleElem || tiles[i].thumbnailElem)
588 tiles[i].elem.classList.add(CLASSES.PAGE_READY);
591 var tileVisibilityBarrier = new Barrier(showAll);
593 if (!userInitiatedMostVisitedChange) {
594 // Make titleContainer invisible, but still taking up space.
595 // titleContainer becomes visible again (1) on timeout, or (2) when all
596 // tiles finish loading (using tileVisibilityBarrier).
597 window.setTimeout(function() {
598 tileVisibilityBarrier.cancel();
600 }, MOST_VISITED_PAINT_TIMEOUT_MSEC);
602 userInitiatedMostVisitedChange = false;
604 for (var i = numExisting; i < numDesired; ++i) {
605 bindTileOnloadEvents(tiles[i], tileVisibilityBarrier);
606 tilesContainer.appendChild(tiles[i].elem);
610 // Show only the desired tiles. Note that .hidden does not work for
611 // inline-block elements like tiles[i].elem.
612 for (var i = 0; i < numDesired; ++i)
613 tiles[i].elem.style.display = 'inline-block';
614 // If |numDesired| < |numExisting| then hide extra tiles (e.g., this occurs
615 // when window is downsized).
616 for (; i < numExisting; ++i)
617 tiles[i].elem.style.display = 'none';
622 * Builds a URL to display a most visited tile title in an iframe.
623 * @param {number} rid The restricted ID.
624 * @param {number} position The position of the iframe in the UI.
625 * @return {string} An URL to display the most visited title in an iframe.
627 function getMostVisitedTitleIframeUrl(rid, position) {
628 var url = 'chrome-search://most-visited/' +
629 encodeURIComponent(MOST_VISITED_TITLE_IFRAME);
631 'rid=' + encodeURIComponent(rid),
632 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
633 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
634 'c=' + encodeURIComponent(titleColor),
635 'pos=' + encodeURIComponent(position)];
636 if (NTP_DESIGN.titleTextAlign)
637 params.push('ta=' + encodeURIComponent(NTP_DESIGN.titleTextAlign));
638 if (NTP_DESIGN.titleTextFade)
639 params.push('tf=' + encodeURIComponent(NTP_DESIGN.titleTextFade));
640 if (NTP_DESIGN.numTitleLines > 1)
641 params.push('ntl=' + NTP_DESIGN.numTitleLines);
642 return url + '?' + params.join('&');
647 * Builds a URL to display a most visited tile thumbnail in an iframe.
648 * @param {number} rid The restricted ID.
649 * @param {number} position The position of the iframe in the UI.
650 * @return {string} An URL to display the most visited thumbnail in an iframe.
652 function getMostVisitedThumbnailIframeUrl(rid, position) {
653 var url = 'chrome-search://most-visited/' +
654 encodeURIComponent(MOST_VISITED_THUMBNAIL_IFRAME);
655 var colorString = convertToRRGGBBAAColor(NTP_DESIGN.thumbnailTextColor);
657 'rid=' + encodeURIComponent(rid),
658 'f=' + encodeURIComponent(NTP_DESIGN.fontFamily),
659 'fs=' + encodeURIComponent(NTP_DESIGN.fontSize),
660 'c=' + encodeURIComponent(colorString),
661 'pos=' + encodeURIComponent(position)];
662 if (NTP_DESIGN.thumbnailFallback)
663 params.push('etfb=1');
664 if (configData.useIcons)
665 params.push('icons=1');
666 return url + '?' + params.join('&');
671 * Creates a Tile with the specified page data. If no data is provided, a
672 * filler Tile is created.
673 * @param {?Object} page The page data.
674 * @param {number} position The position of the tile.
675 * @return {Tile} The new Tile.
677 function createTile(page, position) {
678 var tileElem = document.createElement('div');
679 tileElem.classList.add(CLASSES.TILE);
680 // Prevent tile from being selected (and highlighted) when areas outside the
681 // iframes are clicked.
682 tileElem.addEventListener('mousedown', function(e) {
687 return new Tile(tileElem);
691 tileElem.classList.add(CLASSES.PAGE);
693 var navigateFunction = function(e) {
695 ntpApiHandle.navigateContentWindow(rid, getDispositionFromEvent(e));
698 // The click handler for navigating to the page identified by the RID.
699 tileElem.addEventListener('click', navigateFunction);
701 // Container of tile contents.
702 var innerElem = createAndAppendElement(tileElem, 'div', CLASSES.TILE_INNER);
704 // The iframe which renders the page title.
705 var titleElem = document.createElement('iframe');
706 // Enable tab navigation on the iframe, which will move the selection to the
707 // link element (which also has a tabindex).
708 titleElem.tabIndex = '0';
710 // Make the iframe presentational for accessibility so screen readers perceive
711 // the iframe content as just part of the same page.
712 titleElem.setAttribute('role', 'presentation');
714 // Why iframes have IDs:
716 // On navigating back to the NTP we see several onmostvisitedchange() events
717 // in series with incrementing RIDs. After the first event, a set of iframes
718 // begins loading RIDs n, n+1, ..., n+k-1; after the second event, these get
719 // destroyed and a new set begins loading RIDs n+k, n+k+1, ..., n+2k-1.
720 // Now due to crbug.com/68841, Chrome incorrectly loads the content for the
721 // first set of iframes into the most recent set of iframes.
723 // Giving iframes distinct ids seems to cause some invalidation and prevent
724 // associating the incorrect data.
726 // TODO(jered): Find and fix the root (probably Blink) bug.
728 // Keep this ID here. See comment above.
729 titleElem.id = 'title-' + rid;
730 titleElem.className = CLASSES.TITLE;
731 titleElem.src = getMostVisitedTitleIframeUrl(rid, position);
732 innerElem.appendChild(titleElem);
734 // A fallback element for missing thumbnails.
735 if (NTP_DESIGN.thumbnailFallback) {
736 var fallbackElem = createAndAppendElement(
737 innerElem, 'div', CLASSES.THUMBNAIL_FALLBACK);
738 if (NTP_DESIGN.thumbnailFallback === THUMBNAIL_FALLBACK.DOT)
739 createAndAppendElement(fallbackElem, 'div', CLASSES.DOT);
742 // The iframe which renders either a thumbnail or domain element.
743 var thumbnailElem = document.createElement('iframe');
744 thumbnailElem.tabIndex = '-1';
745 thumbnailElem.setAttribute('aria-hidden', 'true');
746 // Keep this ID here. See comment above.
747 thumbnailElem.id = 'thumb-' + rid;
748 thumbnailElem.className = CLASSES.THUMBNAIL;
749 thumbnailElem.src = getMostVisitedThumbnailIframeUrl(rid, position);
750 innerElem.appendChild(thumbnailElem);
752 // The button used to blacklist this page.
753 var blacklistButton = createAndAppendElement(
754 innerElem, 'div', CLASSES.BLACKLIST_BUTTON);
755 createAndAppendElement(
756 blacklistButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
757 var blacklistFunction = generateBlacklistFunction(rid);
758 blacklistButton.addEventListener('click', blacklistFunction);
759 blacklistButton.title = configData.translatedStrings.removeThumbnailTooltip;
761 // A helper mask on top of the tile that is used to create hover border
762 // and/or to darken the thumbnail on focus.
763 var maskElement = createAndAppendElement(
764 innerElem, 'div', CLASSES.THUMBNAIL_MASK);
766 // The page favicon, or a fallback.
767 if (NTP_DESIGN.showFavicon) {
768 var favicon = createAndAppendElement(innerElem, 'div', CLASSES.FAVICON);
769 if (page.faviconUrl) {
770 var fi = document.createElement('img');
771 fi.src = page.faviconUrl;
772 // Set the title to empty so screen readers won't say the image name.
774 fi.addEventListener('error', function(ev) {
775 favicon.removeChild(fi);
776 favicon.classList.add(CLASSES.FAVICON_FALLBACK);
778 favicon.appendChild(fi);
780 favicon.classList.add(CLASSES.FAVICON_FALLBACK);
783 return new Tile(tileElem, innerElem, titleElem, thumbnailElem, rid);
788 * Generates a function to be called when the page with the corresponding RID
790 * @param {number} rid The RID of the page being blacklisted.
791 * @return {function(Event=)} A function which handles the blacklisting of the
792 * page by updating state variables and notifying Chrome.
794 function generateBlacklistFunction(rid) {
796 // Prevent navigation when the page is being blacklisted.
800 userInitiatedMostVisitedChange = true;
801 isBlacklisting = true;
802 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
803 lastBlacklistedTile = getTileByRid(rid);
804 ntpApiHandle.deleteMostVisitedItem(rid);
810 * Shows the blacklist notification and triggers a delay to hide it.
812 function showNotification() {
813 notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
814 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
815 notification.scrollTop;
816 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
821 * Hides the blacklist notification.
823 function hideNotification() {
824 notification.classList.add(CLASSES.HIDE_NOTIFICATION);
825 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
830 * Handles a click on the notification undo link by hiding the notification and
834 userInitiatedMostVisitedChange = true;
836 var lastBlacklistedRID = lastBlacklistedTile.rid;
837 if (typeof lastBlacklistedRID != 'undefined')
838 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
843 * Handles a click on the restore all notification link by hiding the
844 * notification and informing Chrome.
846 function onRestoreAll() {
847 userInitiatedMostVisitedChange = true;
849 ntpApiHandle.undoAllMostVisitedDeletions();
854 * Recomputes the number of tile columns, and width of various contents based
855 * on the width of the window.
856 * @return {boolean} Whether the number of tile columns has changed.
858 function updateContentWidth() {
859 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
860 // If innerWidth is zero, then use the maximum snap size.
861 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
862 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
863 var innerWidth = window.innerWidth || maxSnapSize;
864 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
865 var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
866 NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING;
867 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
868 if (newNumColumns < MIN_NUM_COLUMNS)
869 newNumColumns = MIN_NUM_COLUMNS;
870 else if (newNumColumns > MAX_NUM_COLUMNS)
871 newNumColumns = MAX_NUM_COLUMNS;
873 if (numColumnsShown === newNumColumns)
876 numColumnsShown = newNumColumns;
877 var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
878 tilesContainer.style.width = tilesContainerWidth + 'px';
880 // -2 to account for border.
881 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
882 fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2;
883 fakebox.style.width = fakeboxWidth + 'px';
890 * Resizes elements because the number of tile columns may need to change in
891 * response to resizing. Also shows or hides extra tiles tiles according to the
892 * new width of the page.
894 function onResize() {
895 if (updateContentWidth()) {
896 // Render without clearing tiles.
897 renderAndShowTiles();
903 * Returns the tile corresponding to the specified page RID.
904 * @param {number} rid The page RID being looked up.
905 * @return {Tile} The corresponding tile.
907 function getTileByRid(rid) {
908 for (var i = 0, length = tiles.length; i < length; ++i) {
918 * Handles new input by disposing the NTP, according to where the input was
921 function onInputStart() {
922 if (fakebox && isFakeboxFocused()) {
923 setFakeboxFocus(false);
924 setFakeboxDragFocus(false);
926 } else if (!isFakeboxFocused()) {
933 * Disposes the NTP, according to where the input was entered.
934 * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
936 function disposeNtp(wasFakeboxInput) {
937 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
938 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
939 setFakeboxActive(false);
940 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
941 setFakeboxAndLogoVisibility(false);
946 * Restores the NTP (re-enables the fakebox and unhides the logo.)
948 function restoreNtp() {
949 setFakeboxActive(true);
950 setFakeboxAndLogoVisibility(true);
955 * @param {boolean} focus True to focus the fakebox.
957 function setFakeboxFocus(focus) {
958 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
962 * @param {boolean} focus True to show a dragging focus to the fakebox.
964 function setFakeboxDragFocus(focus) {
965 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
969 * @return {boolean} True if the fakebox has focus.
971 function isFakeboxFocused() {
972 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
973 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
978 * @param {boolean} enable True to enable the fakebox.
980 function setFakeboxActive(enable) {
981 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
986 * @param {!Event} event The click event.
987 * @return {boolean} True if the click occurred in an enabled fakebox.
989 function isFakeboxClick(event) {
990 return fakebox.contains(event.target) &&
991 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
996 * @param {boolean} show True to show the fakebox and logo.
998 function setFakeboxAndLogoVisibility(show) {
999 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
1004 * Shortcut for document.getElementById.
1005 * @param {string} id of the element.
1006 * @return {HTMLElement} with the id.
1009 return document.getElementById(id);
1014 * Utility function which creates an element with an optional classname and
1015 * appends it to the specified parent.
1016 * @param {Element} parent The parent to append the new element.
1017 * @param {string} name The name of the new element.
1018 * @param {string=} opt_class The optional classname of the new element.
1019 * @return {Element} The new element.
1021 function createAndAppendElement(parent, name, opt_class) {
1022 var child = document.createElement(name);
1024 child.classList.add(opt_class);
1025 parent.appendChild(child);
1031 * Removes a node from its parent.
1032 * @param {Node} node The node to remove.
1034 function removeNode(node) {
1035 node.parentNode.removeChild(node);
1040 * @param {!Element} element The element to register the handler for.
1041 * @param {number} keycode The keycode of the key to register.
1042 * @param {!Function} handler The key handler to register.
1044 function registerKeyHandler(element, keycode, handler) {
1045 element.addEventListener('keydown', function(event) {
1046 if (event.keyCode == keycode)
1053 * @return {Object} the handle to the embeddedSearch API.
1055 function getEmbeddedSearchApiHandle() {
1057 return window.cideb;
1058 if (window.chrome && window.chrome.embeddedSearch)
1059 return window.chrome.embeddedSearch;
1065 * Event handler for the focus changed and blacklist messages on link elements.
1066 * Used to toggle visual treatment on the tiles (depending on the message).
1067 * @param {Event} event Event received.
1069 function handlePostMessage(event) {
1070 if (event.origin !== 'chrome-search://most-visited')
1073 if (event.data === 'linkFocused') {
1074 var activeElement = document.activeElement;
1075 if (activeElement.classList.contains(CLASSES.TITLE)) {
1076 activeElement.classList.add(CLASSES.FOCUSED);
1077 focusedIframe = activeElement;
1079 } else if (event.data === 'linkBlurred') {
1081 focusedIframe.classList.remove(CLASSES.FOCUSED);
1082 focusedIframe = null;
1083 } else if (event.data.indexOf('tileBlacklisted') === 0) {
1084 var tilePosition = event.data.split(',')[1];
1086 generateBlacklistFunction(tiles[parseInt(tilePosition, 10)].rid)();
1092 * Prepares the New Tab Page by adding listeners, rendering the current
1093 * theme, the most visited pages section, and Google-specific elements for a
1094 * Google-provided page.
1097 tilesContainer = $(IDS.TILES);
1098 notification = $(IDS.NOTIFICATION);
1099 attribution = $(IDS.ATTRIBUTION);
1100 ntpContents = $(IDS.NTP_CONTENTS);
1102 if (configData.isGooglePage) {
1103 var logo = document.createElement('div');
1105 logo.title = 'Google';
1107 fakebox = document.createElement('div');
1108 fakebox.id = IDS.FAKEBOX;
1109 var fakeboxHtml = [];
1110 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
1111 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
1112 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
1113 fakeboxHtml.push('<div id="cursor"></div>');
1114 fakebox.innerHTML = fakeboxHtml.join('');
1116 ntpContents.insertBefore(fakebox, ntpContents.firstChild);
1117 ntpContents.insertBefore(logo, ntpContents.firstChild);
1119 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
1122 // Modify design for experimental icon NTP, if specified.
1123 if (configData.useIcons)
1124 modifyNtpDesignForIcons();
1125 document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass);
1127 // Hide notifications after fade out, so we can't focus on links via keyboard.
1128 notification.addEventListener('webkitTransitionEnd', hideNotification);
1130 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
1131 notificationMessage.textContent =
1132 configData.translatedStrings.thumbnailRemovedNotification;
1134 var undoLink = $(IDS.UNDO_LINK);
1135 undoLink.addEventListener('click', onUndo);
1136 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
1137 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
1139 var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
1140 restoreAllLink.addEventListener('click', onRestoreAll);
1141 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
1142 restoreAllLink.textContent =
1143 configData.translatedStrings.restoreThumbnailsShort;
1145 $(IDS.ATTRIBUTION_TEXT).textContent =
1146 configData.translatedStrings.attributionIntro;
1148 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
1149 createAndAppendElement(
1150 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
1151 notificationCloseButton.addEventListener('click', hideNotification);
1153 window.addEventListener('resize', onResize);
1154 updateContentWidth();
1156 var topLevelHandle = getEmbeddedSearchApiHandle();
1158 ntpApiHandle = topLevelHandle.newTabPage;
1159 ntpApiHandle.onthemechange = onThemeChange;
1160 ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
1162 ntpApiHandle.oninputstart = onInputStart;
1163 ntpApiHandle.oninputcancel = restoreNtp;
1165 if (ntpApiHandle.isInputInProgress)
1171 searchboxApiHandle = topLevelHandle.searchBox;
1174 // Listener for updating the key capture state.
1175 document.body.onmousedown = function(event) {
1176 if (isFakeboxClick(event))
1177 searchboxApiHandle.startCapturingKeyStrokes();
1178 else if (isFakeboxFocused())
1179 searchboxApiHandle.stopCapturingKeyStrokes();
1181 searchboxApiHandle.onkeycapturechange = function() {
1182 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1184 var inputbox = $(IDS.FAKEBOX_INPUT);
1186 inputbox.onpaste = function(event) {
1187 event.preventDefault();
1188 // Send pasted text to Omnibox.
1189 var text = event.clipboardData.getData('text/plain');
1191 searchboxApiHandle.paste(text);
1193 inputbox.ondrop = function(event) {
1194 event.preventDefault();
1195 var text = event.dataTransfer.getData('text/plain');
1197 searchboxApiHandle.paste(text);
1199 setFakeboxDragFocus(false);
1201 inputbox.ondragenter = function() {
1202 setFakeboxDragFocus(true);
1204 inputbox.ondragleave = function() {
1205 setFakeboxDragFocus(false);
1209 // Update the fakebox style to match the current key capturing state.
1210 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
1213 if (searchboxApiHandle.rtl) {
1214 $(IDS.NOTIFICATION).dir = 'rtl';
1215 // Grabbing the root HTML element.
1216 document.documentElement.setAttribute('dir', 'rtl');
1217 // Add class for setting alignments based on language directionality.
1218 document.documentElement.classList.add(CLASSES.RTL);
1219 $(IDS.TILES).dir = 'rtl';
1222 window.addEventListener('message', handlePostMessage);
1227 * Binds event listeners.
1230 document.addEventListener('DOMContentLoaded', init);
1239 if (!window.localNTPUnitTest) {
1240 LocalNTP().listen();