1 // Copyright 2015 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.
18 <include src
="../../../../ui/webui/resources/js/util.js">
19 <include src
="local_ntp_design.js">
22 * Enum for classnames.
27 ALTERNATE_LOGO
: 'alternate-logo', // Shows white logo if required by theme
29 DEFAULT_THEME
: 'default-theme',
30 DELAYED_HIDE_NOTIFICATION
: 'mv-notice-delayed-hide',
31 FAKEBOX_DISABLE
: 'fakebox-disable', // Makes fakebox non-interactive
32 FAKEBOX_FOCUS
: 'fakebox-focused', // Applies focus styles to the fakebox
33 // Applies drag focus style to the fakebox
34 FAKEBOX_DRAG_FOCUS
: 'fakebox-drag-focused',
35 HIDE_FAKEBOX_AND_LOGO
: 'hide-fakebox-logo',
36 HIDE_NOTIFICATION
: 'mv-notice-hide',
37 // Vertically centers the most visited section for a non-Google provided page.
38 NON_GOOGLE_PAGE
: 'non-google-page',
39 RTL
: 'rtl' // Right-to-left language text.
44 * Enum for HTML element ids.
49 ATTRIBUTION
: 'attribution',
50 ATTRIBUTION_TEXT
: 'attribution-text',
51 CUSTOM_THEME_STYLE
: 'ct-style',
53 FAKEBOX_INPUT
: 'fakebox-input',
54 FAKEBOX_TEXT
: 'fakebox-text',
56 NOTIFICATION
: 'mv-notice',
57 NOTIFICATION_CLOSE_BUTTON
: 'mv-notice-x',
58 NOTIFICATION_MESSAGE
: 'mv-msg',
59 NTP_CONTENTS
: 'ntp-contents',
60 RESTORE_ALL_LINK
: 'mv-restore',
77 * Enum for the state of the NTP when it is disposed.
81 var NTP_DISPOSE_STATE
= {
82 NONE
: 0, // Preserve the NTP appearance and functionality
84 HIDE_FAKEBOX_AND_LOGO
: 2
89 * The notification displayed when a page is blacklisted.
96 * The container for the theme attribution.
103 * The "fakebox" - an input field that looks like a regular searchbox. When it
104 * is focused, any text the user types goes directly into the omnibox.
111 * The container for NTP elements.
118 * The last blacklisted tile rid if any, which by definition should not be
122 var lastBlacklistedTile
= null;
126 * Current number of tiles columns shown based on the window width, including
127 * those that just contain filler.
130 var numColumnsShown
= 0;
134 * The browser embeddedSearch.newTabPage object.
141 * The browser embeddedSearch.searchBox object.
144 var searchboxApiHandle
;
148 * The state of the NTP when a query is entered into the Omnibox.
149 * @type {NTP_DISPOSE_STATE}
151 var omniboxInputBehavior
= NTP_DISPOSE_STATE
.NONE
;
155 * The state of the NTP when a query is entered into the Fakebox.
156 * @type {NTP_DISPOSE_STATE}
158 var fakeboxInputBehavior
= NTP_DISPOSE_STATE
.HIDE_FAKEBOX_AND_LOGO
;
161 /** @type {number} @const */
162 var MAX_NUM_TILES_TO_SHOW
= 8;
165 /** @type {number} @const */
166 var MIN_NUM_COLUMNS
= 2;
169 /** @type {number} @const */
170 var MAX_NUM_COLUMNS
= 4;
173 /** @type {number} @const */
178 * Minimum total padding to give to the left and right of the most visited
179 * section. Used to determine how many tiles to show.
183 var MIN_TOTAL_HORIZONTAL_PADDING
= 200;
187 * Heuristic to determine whether a theme should be considered to be dark, so
188 * the colors of various UI elements can be adjusted.
189 * @param {ThemeBackgroundInfo|undefined} info Theme background information.
190 * @return {boolean} Whether the theme is dark.
193 function getIsThemeDark(info
) {
196 // Heuristic: light text implies dark theme.
197 var rgba
= info
.textColorRgba
;
198 var luminance
= 0.3 * rgba
[0] + 0.59 * rgba
[1] + 0.11 * rgba
[2];
199 return luminance
>= 128;
204 * Updates the NTP based on the current theme.
207 function renderTheme() {
208 var fakeboxText
= $(IDS
.FAKEBOX_TEXT
);
210 fakeboxText
.innerHTML
= '';
211 if (configData
.translatedStrings
.searchboxPlaceholder
) {
212 fakeboxText
.textContent
=
213 configData
.translatedStrings
.searchboxPlaceholder
;
217 var info
= ntpApiHandle
.themeBackgroundInfo
;
218 var isThemeDark
= getIsThemeDark(info
);
219 ntpContents
.classList
.toggle(CLASSES
.DARK
, isThemeDark
);
224 var background
= [convertToRGBAColor(info
.backgroundColorRgba
),
227 info
.imageHorizontalAlignment
,
228 info
.imageVerticalAlignment
].join(' ').trim();
230 document
.body
.style
.background
= background
;
231 document
.body
.classList
.toggle(CLASSES
.ALTERNATE_LOGO
, info
.alternateLogo
);
232 updateThemeAttribution(info
.attributionUrl
);
233 setCustomThemeStyle(info
);
235 var themeinfo
= {cmd
: 'updateTheme'};
236 if (!info
.usingDefaultTheme
) {
237 themeinfo
.tileBorderColor
= convertToRGBAColor(info
.sectionBorderColorRgba
);
238 themeinfo
.tileHoverBorderColor
= convertToRGBAColor(info
.headerColorRgba
);
240 themeinfo
.isThemeDark
= isThemeDark
;
242 var titleColor
= NTP_DESIGN
.titleColor
;
243 if (!info
.usingDefaultTheme
&& info
.textColorRgba
) {
244 titleColor
= info
.textColorRgba
;
245 } else if (isThemeDark
) {
246 titleColor
= NTP_DESIGN
.titleColorAgainstDark
;
248 themeinfo
.tileTitleColor
= convertToRGBAColor(titleColor
);
250 $('mv-single').contentWindow
.postMessage(themeinfo
, '*');
255 * Updates the NTP based on the current theme, then rerenders all tiles.
258 function onThemeChange() {
264 * Updates the NTP style according to theme.
265 * @param {Object=} opt_themeInfo The information about the theme. If it is
266 * omitted the style will be reverted to the default.
269 function setCustomThemeStyle(opt_themeInfo
) {
270 var customStyleElement
= $(IDS
.CUSTOM_THEME_STYLE
);
271 var head
= document
.head
;
272 if (opt_themeInfo
&& !opt_themeInfo
.usingDefaultTheme
) {
273 ntpContents
.classList
.remove(CLASSES
.DEFAULT_THEME
);
276 ' color: ' + convertToRGBAColor(opt_themeInfo
.textColorLightRgba
) + ';' +
279 ' color: ' + convertToRGBAColor(opt_themeInfo
.textColorRgba
) + ';' +
281 '#mv-notice-links span {' +
282 ' color: ' + convertToRGBAColor(opt_themeInfo
.textColorLightRgba
) + ';' +
285 ' -webkit-filter: drop-shadow(0 0 0 ' +
286 convertToRGBAColor(opt_themeInfo
.textColorRgba
) + ');' +
288 '.mv-page-ready .mv-mask {' +
289 ' border: 1px solid ' +
290 convertToRGBAColor(opt_themeInfo
.sectionBorderColorRgba
) + ';' +
292 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
294 convertToRGBAColor(opt_themeInfo
.headerColorRgba
) + ';' +
297 if (customStyleElement
) {
298 customStyleElement
.textContent
= themeStyle
;
300 customStyleElement
= document
.createElement('style');
301 customStyleElement
.type
= 'text/css';
302 customStyleElement
.id
= IDS
.CUSTOM_THEME_STYLE
;
303 customStyleElement
.textContent
= themeStyle
;
304 head
.appendChild(customStyleElement
);
308 ntpContents
.classList
.add(CLASSES
.DEFAULT_THEME
);
309 if (customStyleElement
)
310 head
.removeChild(customStyleElement
);
316 * Renders the attribution if the URL is present, otherwise hides it.
317 * @param {string} url The URL of the attribution image, if any.
320 function updateThemeAttribution(url
) {
322 setAttributionVisibility_(false);
326 var attributionImage
= attribution
.querySelector('img');
327 if (!attributionImage
) {
328 attributionImage
= new Image();
329 attribution
.appendChild(attributionImage
);
331 attributionImage
.style
.content
= url
;
332 setAttributionVisibility_(true);
337 * Sets the visibility of the theme attribution.
338 * @param {boolean} show True to show the attribution.
341 function setAttributionVisibility_(show
) {
343 attribution
.style
.display
= show
? '' : 'none';
349 * Converts an Array of color components into RRGGBBAA format.
350 * @param {Array<number>} color Array of rgba color components.
351 * @return {string} Color string in RRGGBBAA format.
354 function convertToRRGGBBAAColor(color
) {
355 return color
.map(function(t
) {
356 return ('0' + t
.toString(16)).slice(-2); // To 2-digit, 0-padded hex.
362 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
363 * @param {Array<number>} color Array of rgba color components.
364 * @return {string} CSS color in RGBA format.
367 function convertToRGBAColor(color
) {
368 return 'rgba(' + color
[0] + ',' + color
[1] + ',' + color
[2] + ',' +
369 color
[3] / 255 + ')';
374 * Called when page data change.
376 function onMostVisitedChange() {
382 * Fetches new data, creates, and renders tiles.
384 function reloadTiles() {
385 var pages
= ntpApiHandle
.mostVisited
;
387 for (var i
= 0; i
< Math
.min(MAX_NUM_TILES_TO_SHOW
, pages
.length
); ++i
) {
388 cmds
.push({cmd
: 'tile', rid
: pages
[i
].rid
});
390 cmds
.push({cmd
: 'show', maxVisible
: numColumnsShown
* NUM_ROWS
});
392 $('mv-single').contentWindow
.postMessage(cmds
, '*');
397 * Shows the blacklist notification and triggers a delay to hide it.
399 function showNotification() {
400 notification
.classList
.remove(CLASSES
.HIDE_NOTIFICATION
);
401 notification
.classList
.remove(CLASSES
.DELAYED_HIDE_NOTIFICATION
);
402 notification
.scrollTop
;
403 notification
.classList
.add(CLASSES
.DELAYED_HIDE_NOTIFICATION
);
408 * Hides the blacklist notification.
410 function hideNotification() {
411 notification
.classList
.add(CLASSES
.HIDE_NOTIFICATION
);
412 notification
.classList
.remove(CLASSES
.DELAYED_HIDE_NOTIFICATION
);
417 * Handles a click on the notification undo link by hiding the notification and
422 if (lastBlacklistedTile
!= null) {
423 ntpApiHandle
.undoMostVisitedDeletion(lastBlacklistedTile
);
429 * Handles a click on the restore all notification link by hiding the
430 * notification and informing Chrome.
432 function onRestoreAll() {
434 ntpApiHandle
.undoAllMostVisitedDeletions();
439 * Recomputes the number of tile columns, and width of various contents based
440 * on the width of the window.
441 * @return {boolean} Whether the number of tile columns has changed.
443 function updateContentWidth() {
444 var tileRequiredWidth
= NTP_DESIGN
.tileWidth
+ NTP_DESIGN
.tileMargin
;
445 // If innerWidth is zero, then use the maximum snap size.
446 var maxSnapSize
= MAX_NUM_COLUMNS
* tileRequiredWidth
-
447 NTP_DESIGN
.tileMargin
+ MIN_TOTAL_HORIZONTAL_PADDING
;
448 var innerWidth
= window
.innerWidth
|| maxSnapSize
;
449 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
450 var availableWidth
= innerWidth
+ NTP_DESIGN
.tileMargin
-
451 NTP_DESIGN
.fakeboxWingSize
* 2 - MIN_TOTAL_HORIZONTAL_PADDING
;
452 var newNumColumns
= Math
.floor(availableWidth
/ tileRequiredWidth
);
453 if (newNumColumns
< MIN_NUM_COLUMNS
)
454 newNumColumns
= MIN_NUM_COLUMNS
;
455 else if (newNumColumns
> MAX_NUM_COLUMNS
)
456 newNumColumns
= MAX_NUM_COLUMNS
;
458 if (numColumnsShown
=== newNumColumns
)
461 numColumnsShown
= newNumColumns
;
462 var tilesContainerWidth
= numColumnsShown
* tileRequiredWidth
;
463 $(IDS
.TILES
).style
.width
= tilesContainerWidth
+ 'px';
465 // -2 to account for border.
466 var fakeboxWidth
= (tilesContainerWidth
- NTP_DESIGN
.tileMargin
- 2);
467 fakeboxWidth
+= NTP_DESIGN
.fakeboxWingSize
* 2;
468 fakebox
.style
.width
= fakeboxWidth
+ 'px';
475 * Resizes elements because the number of tile columns may need to change in
476 * response to resizing. Also shows or hides extra tiles tiles according to the
477 * new width of the page.
479 function onResize() {
480 updateContentWidth();
481 $('mv-single').contentWindow
.postMessage(
482 {cmd
: 'tilesVisible', maxVisible
: numColumnsShown
* NUM_ROWS
}, '*');
487 * Handles new input by disposing the NTP, according to where the input was
490 function onInputStart() {
491 if (fakebox
&& isFakeboxFocused()) {
492 setFakeboxFocus(false);
493 setFakeboxDragFocus(false);
495 } else if (!isFakeboxFocused()) {
502 * Disposes the NTP, according to where the input was entered.
503 * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
505 function disposeNtp(wasFakeboxInput
) {
506 var behavior
= wasFakeboxInput
? fakeboxInputBehavior
: omniboxInputBehavior
;
507 if (behavior
== NTP_DISPOSE_STATE
.DISABLE_FAKEBOX
)
508 setFakeboxActive(false);
509 else if (behavior
== NTP_DISPOSE_STATE
.HIDE_FAKEBOX_AND_LOGO
)
510 setFakeboxAndLogoVisibility(false);
515 * Restores the NTP (re-enables the fakebox and unhides the logo.)
517 function restoreNtp() {
518 setFakeboxActive(true);
519 setFakeboxAndLogoVisibility(true);
524 * @param {boolean} focus True to focus the fakebox.
526 function setFakeboxFocus(focus
) {
527 document
.body
.classList
.toggle(CLASSES
.FAKEBOX_FOCUS
, focus
);
531 * @param {boolean} focus True to show a dragging focus to the fakebox.
533 function setFakeboxDragFocus(focus
) {
534 document
.body
.classList
.toggle(CLASSES
.FAKEBOX_DRAG_FOCUS
, focus
);
538 * @return {boolean} True if the fakebox has focus.
540 function isFakeboxFocused() {
541 return document
.body
.classList
.contains(CLASSES
.FAKEBOX_FOCUS
) ||
542 document
.body
.classList
.contains(CLASSES
.FAKEBOX_DRAG_FOCUS
);
547 * @param {boolean} enable True to enable the fakebox.
549 function setFakeboxActive(enable
) {
550 document
.body
.classList
.toggle(CLASSES
.FAKEBOX_DISABLE
, !enable
);
555 * @param {!Event} event The click event.
556 * @return {boolean} True if the click occurred in an enabled fakebox.
558 function isFakeboxClick(event
) {
559 return fakebox
.contains(event
.target
) &&
560 !document
.body
.classList
.contains(CLASSES
.FAKEBOX_DISABLE
);
565 * @param {boolean} show True to show the fakebox and logo.
567 function setFakeboxAndLogoVisibility(show
) {
568 document
.body
.classList
.toggle(CLASSES
.HIDE_FAKEBOX_AND_LOGO
, !show
);
573 * Shortcut for document.getElementById.
574 * @param {string} id of the element.
575 * @return {HTMLElement} with the id.
578 return document
.getElementById(id
);
583 * Utility function which creates an element with an optional classname and
584 * appends it to the specified parent.
585 * @param {Element} parent The parent to append the new element.
586 * @param {string} name The name of the new element.
587 * @param {string=} opt_class The optional classname of the new element.
588 * @return {Element} The new element.
590 function createAndAppendElement(parent
, name
, opt_class
) {
591 var child
= document
.createElement(name
);
593 child
.classList
.add(opt_class
);
594 parent
.appendChild(child
);
600 * @param {!Element} element The element to register the handler for.
601 * @param {number} keycode The keycode of the key to register.
602 * @param {!Function} handler The key handler to register.
604 function registerKeyHandler(element
, keycode
, handler
) {
605 element
.addEventListener('keydown', function(event
) {
606 if (event
.keyCode
== keycode
)
613 * @return {Object} the handle to the embeddedSearch API.
615 function getEmbeddedSearchApiHandle() {
618 if (window
.chrome
&& window
.chrome
.embeddedSearch
)
619 return window
.chrome
.embeddedSearch
;
625 * Event handler for the focus changed and blacklist messages on link elements.
626 * Used to toggle visual treatment on the tiles (depending on the message).
627 * @param {Event} event Event received.
629 function handlePostMessage(event
) {
630 var cmd
= event
.data
.cmd
;
631 var args
= event
.data
;
632 if (cmd
== 'tileBlacklisted') {
634 lastBlacklistedTile
= args
.tid
;
636 ntpApiHandle
.deleteMostVisitedItem(args
.tid
);
642 * Prepares the New Tab Page by adding listeners, rendering the current
643 * theme, the most visited pages section, and Google-specific elements for a
644 * Google-provided page.
647 notification
= $(IDS
.NOTIFICATION
);
648 attribution
= $(IDS
.ATTRIBUTION
);
649 ntpContents
= $(IDS
.NTP_CONTENTS
);
651 if (configData
.isGooglePage
) {
652 var logo
= document
.createElement('div');
654 logo
.title
= 'Google';
656 fakebox
= document
.createElement('div');
657 fakebox
.id
= IDS
.FAKEBOX
;
658 var fakeboxHtml
= [];
659 fakeboxHtml
.push('<div id="' + IDS
.FAKEBOX_TEXT
+ '"></div>');
660 fakeboxHtml
.push('<input id="' + IDS
.FAKEBOX_INPUT
+
661 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
662 fakeboxHtml
.push('<div id="cursor"></div>');
663 fakebox
.innerHTML
= fakeboxHtml
.join('');
665 ntpContents
.insertBefore(fakebox
, ntpContents
.firstChild
);
666 ntpContents
.insertBefore(logo
, ntpContents
.firstChild
);
668 document
.body
.classList
.add(CLASSES
.NON_GOOGLE_PAGE
);
671 // Modify design for experimental icon NTP, if specified.
672 if (configData
.useIcons
)
673 modifyNtpDesignForIcons();
674 document
.querySelector('#ntp-contents').classList
.add(NTP_DESIGN
.mainClass
);
676 // Hide notifications after fade out, so we can't focus on links via keyboard.
677 notification
.addEventListener('webkitTransitionEnd', hideNotification
);
679 var notificationMessage
= $(IDS
.NOTIFICATION_MESSAGE
);
680 notificationMessage
.textContent
=
681 configData
.translatedStrings
.thumbnailRemovedNotification
;
683 var undoLink
= $(IDS
.UNDO_LINK
);
684 undoLink
.addEventListener('click', onUndo
);
685 registerKeyHandler(undoLink
, KEYCODE
.ENTER
, onUndo
);
686 undoLink
.textContent
= configData
.translatedStrings
.undoThumbnailRemove
;
688 var restoreAllLink
= $(IDS
.RESTORE_ALL_LINK
);
689 restoreAllLink
.addEventListener('click', onRestoreAll
);
690 registerKeyHandler(restoreAllLink
, KEYCODE
.ENTER
, onUndo
);
691 restoreAllLink
.textContent
=
692 configData
.translatedStrings
.restoreThumbnailsShort
;
694 $(IDS
.ATTRIBUTION_TEXT
).textContent
=
695 configData
.translatedStrings
.attributionIntro
;
697 var notificationCloseButton
= $(IDS
.NOTIFICATION_CLOSE_BUTTON
);
698 createAndAppendElement(
699 notificationCloseButton
, 'div', CLASSES
.BLACKLIST_BUTTON_INNER
);
700 notificationCloseButton
.addEventListener('click', hideNotification
);
702 window
.addEventListener('resize', onResize
);
703 updateContentWidth();
705 var topLevelHandle
= getEmbeddedSearchApiHandle();
707 ntpApiHandle
= topLevelHandle
.newTabPage
;
708 ntpApiHandle
.onthemechange
= onThemeChange
;
709 ntpApiHandle
.onmostvisitedchange
= onMostVisitedChange
;
711 ntpApiHandle
.oninputstart
= onInputStart
;
712 ntpApiHandle
.oninputcancel
= restoreNtp
;
714 if (ntpApiHandle
.isInputInProgress
)
717 searchboxApiHandle
= topLevelHandle
.searchBox
;
720 // Listener for updating the key capture state.
721 document
.body
.onmousedown = function(event
) {
722 if (isFakeboxClick(event
))
723 searchboxApiHandle
.startCapturingKeyStrokes();
724 else if (isFakeboxFocused())
725 searchboxApiHandle
.stopCapturingKeyStrokes();
727 searchboxApiHandle
.onkeycapturechange = function() {
728 setFakeboxFocus(searchboxApiHandle
.isKeyCaptureEnabled
);
730 var inputbox
= $(IDS
.FAKEBOX_INPUT
);
732 inputbox
.onpaste = function(event
) {
733 event
.preventDefault();
734 // Send pasted text to Omnibox.
735 var text
= event
.clipboardData
.getData('text/plain');
737 searchboxApiHandle
.paste(text
);
739 inputbox
.ondrop = function(event
) {
740 event
.preventDefault();
741 var text
= event
.dataTransfer
.getData('text/plain');
743 searchboxApiHandle
.paste(text
);
745 setFakeboxDragFocus(false);
747 inputbox
.ondragenter = function() {
748 setFakeboxDragFocus(true);
750 inputbox
.ondragleave = function() {
751 setFakeboxDragFocus(false);
755 // Update the fakebox style to match the current key capturing state.
756 setFakeboxFocus(searchboxApiHandle
.isKeyCaptureEnabled
);
759 if (searchboxApiHandle
.rtl
) {
760 $(IDS
.NOTIFICATION
).dir
= 'rtl';
761 // Grabbing the root HTML element.
762 document
.documentElement
.setAttribute('dir', 'rtl');
763 // Add class for setting alignments based on language directionality.
764 document
.documentElement
.classList
.add(CLASSES
.RTL
);
767 var iframe
= document
.createElement('iframe');
768 iframe
.id
= 'mv-single';
771 if (searchboxApiHandle
.rtl
)
773 if (window
.configData
.useIcons
)
774 args
.push('icons=1');
775 if (NTP_DESIGN
.numTitleLines
> 1)
776 args
.push('ntl=' + NTP_DESIGN
.numTitleLines
);
778 args
.push('removeTooltip=' +
779 encodeURIComponent(configData
.translatedStrings
.removeThumbnailTooltip
));
781 iframe
.src
= '//most-visited/single.html?' + args
.join('&');
782 $(IDS
.TILES
).appendChild(iframe
);
784 iframe
.onload = function() {
789 window
.addEventListener('message', handlePostMessage
);
794 * Binds event listeners.
797 document
.addEventListener('DOMContentLoaded', init
);
806 if (!window
.localNTPUnitTest
) {