Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / local_ntp_fast.js
blob58eba1c22814dcd69bc071274bdead7aa286fe0e
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.
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.
14  */
15 function LocalNTP() {
16   'use strict';
18 <include src="../../../../ui/webui/resources/js/util.js">
19 <include src='local_ntp_design.js'>
21 /**
22  * Enum for classnames.
23  * @enum {string}
24  * @const
25  */
26 var CLASSES = {
27   ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme
28   DARK: 'dark',
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.
43 /**
44  * Enum for HTML element ids.
45  * @enum {string}
46  * @const
47  */
48 var IDS = {
49   ATTRIBUTION: 'attribution',
50   ATTRIBUTION_TEXT: 'attribution-text',
51   CUSTOM_THEME_STYLE: 'ct-style',
52   FAKEBOX: 'fakebox',
53   FAKEBOX_INPUT: 'fakebox-input',
54   FAKEBOX_TEXT: 'fakebox-text',
55   LOGO: 'logo',
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',
61   TILES: 'mv-tiles',
62   UNDO_LINK: 'mv-undo'
66 /**
67  * Enum for keycodes.
68  * @enum {number}
69  * @const
70  */
71 var KEYCODE = {
72   ENTER: 13
76 /**
77  * Enum for the state of the NTP when it is disposed.
78  * @enum {number}
79  * @const
80  */
81 var NTP_DISPOSE_STATE = {
82   NONE: 0,  // Preserve the NTP appearance and functionality
83   DISABLE_FAKEBOX: 1,
84   HIDE_FAKEBOX_AND_LOGO: 2
88 /**
89  * The notification displayed when a page is blacklisted.
90  * @type {Element}
91  */
92 var notification;
95 /**
96  * The container for the theme attribution.
97  * @type {Element}
98  */
99 var 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.
105  * @type {Element}
106  */
107 var fakebox;
111  * The container for NTP elements.
112  * @type {Element}
113  */
114 var ntpContents;
118  * The last blacklisted tile rid if any, which by definition should not be
119  * filler.
120  * @type {?number}
121  */
122 var lastBlacklistedTile = null;
126  * Current number of tiles columns shown based on the window width, including
127  * those that just contain filler.
128  * @type {number}
129  */
130 var numColumnsShown = 0;
134  * The browser embeddedSearch.newTabPage object.
135  * @type {Object}
136  */
137 var ntpApiHandle;
141  * The browser embeddedSearch.searchBox object.
142  * @type {Object}
143  */
144 var searchboxApiHandle;
148  * The state of the NTP when a query is entered into the Omnibox.
149  * @type {NTP_DISPOSE_STATE}
150  */
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}
157  */
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 */
174 var NUM_ROWS = 2;
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.
180  * @type {number}
181  * @const
182  */
183 var MIN_TOTAL_HORIZONTAL_PADDING = 200;
187  * The color of the title in RRGGBBAA format.
188  * @type {?string}
189  */
190 var titleColor = null;
194  * Heuristic to determine whether a theme should be considered to be dark, so
195  * the colors of various UI elements can be adjusted.
196  * @param {ThemeBackgroundInfo|undefined} info Theme background information.
197  * @return {boolean} Whether the theme is dark.
198  * @private
199  */
200 function getIsThemeDark(info) {
201   if (!info)
202     return false;
203   // Heuristic: light text implies dark theme.
204   var rgba = info.textColorRgba;
205   var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2];
206   return luminance >= 128;
211  * Updates the NTP based on the current theme.
212  * @private
213  */
214 function renderTheme() {
215   var fakeboxText = $(IDS.FAKEBOX_TEXT);
216   if (fakeboxText) {
217     fakeboxText.innerHTML = '';
218     if (configData.translatedStrings.searchboxPlaceholder) {
219       fakeboxText.textContent =
220           configData.translatedStrings.searchboxPlaceholder;
221     }
222   }
224   var info = ntpApiHandle.themeBackgroundInfo;
225   var isThemeDark = getIsThemeDark(info);
226   ntpContents.classList.toggle(CLASSES.DARK, isThemeDark);
227   if (!info) {
228     titleColor = NTP_DESIGN.titleColor;
229     return;
230   }
232   if (!info.usingDefaultTheme && info.textColorRgba) {
233     titleColor = convertToRRGGBBAAColor(info.textColorRgba);
234   } else {
235     titleColor = isThemeDark ?
236         NTP_DESIGN.titleColorAgainstDark : NTP_DESIGN.titleColor;
237   }
239   var background = [convertToRGBAColor(info.backgroundColorRgba),
240                     info.imageUrl,
241                     info.imageTiling,
242                     info.imageHorizontalAlignment,
243                     info.imageVerticalAlignment].join(' ').trim();
245   document.body.style.background = background;
246   document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo);
247   updateThemeAttribution(info.attributionUrl);
248   setCustomThemeStyle(info);
253  * Updates the NTP based on the current theme, then rerenders all tiles.
254  * @private
255  */
256 function onThemeChange() {
257   renderTheme();
262  * Updates the NTP style according to theme.
263  * @param {Object=} opt_themeInfo The information about the theme. If it is
264  * omitted the style will be reverted to the default.
265  * @private
266  */
267 function setCustomThemeStyle(opt_themeInfo) {
268   var customStyleElement = $(IDS.CUSTOM_THEME_STYLE);
269   var head = document.head;
270   if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) {
271     ntpContents.classList.remove(CLASSES.DEFAULT_THEME);
272     var themeStyle =
273       '#attribution {' +
274       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
275       '}' +
276       '#mv-msg {' +
277       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' +
278       '}' +
279       '#mv-notice-links span {' +
280       '  color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' +
281       '}' +
282       '#mv-notice-x {' +
283       '  -webkit-filter: drop-shadow(0 0 0 ' +
284           convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' +
285       '}' +
286       '.mv-page-ready .mv-mask {' +
287       '  border: 1px solid ' +
288           convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' +
289       '}' +
290       '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' +
291       '  border-color: ' +
292           convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' +
293       '}';
295     if (customStyleElement) {
296       customStyleElement.textContent = themeStyle;
297     } else {
298       customStyleElement = document.createElement('style');
299       customStyleElement.type = 'text/css';
300       customStyleElement.id = IDS.CUSTOM_THEME_STYLE;
301       customStyleElement.textContent = themeStyle;
302       head.appendChild(customStyleElement);
303     }
305   } else {
306     ntpContents.classList.add(CLASSES.DEFAULT_THEME);
307     if (customStyleElement)
308       head.removeChild(customStyleElement);
309   }
314  * Renders the attribution if the URL is present, otherwise hides it.
315  * @param {string} url The URL of the attribution image, if any.
316  * @private
317  */
318 function updateThemeAttribution(url) {
319   if (!url) {
320     setAttributionVisibility_(false);
321     return;
322   }
324   var attributionImage = attribution.querySelector('img');
325   if (!attributionImage) {
326     attributionImage = new Image();
327     attribution.appendChild(attributionImage);
328   }
329   attributionImage.style.content = url;
330   setAttributionVisibility_(true);
335  * Sets the visibility of the theme attribution.
336  * @param {boolean} show True to show the attribution.
337  * @private
338  */
339 function setAttributionVisibility_(show) {
340   if (attribution) {
341     attribution.style.display = show ? '' : 'none';
342   }
346  /**
347  * Converts an Array of color components into RRGGBBAA format.
348  * @param {Array<number>} color Array of rgba color components.
349  * @return {string} Color string in RRGGBBAA format.
350  * @private
351  */
352 function convertToRRGGBBAAColor(color) {
353   return color.map(function(t) {
354     return ('0' + t.toString(16)).slice(-2);  // To 2-digit, 0-padded hex.
355   }).join('');
359  /**
360  * Converts an Array of color components into RGBA format "rgba(R,G,B,A)".
361  * @param {Array<number>} color Array of rgba color components.
362  * @return {string} CSS color in RGBA format.
363  * @private
364  */
365 function convertToRGBAColor(color) {
366   return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' +
367                     color[3] / 255 + ')';
372  * Called when page data change.
373  */
374 function onMostVisitedChange() {
375   reloadTiles();
380  * Fetches new data, creates, and renders tiles.
381  */
382 function reloadTiles() {
383   var pages = ntpApiHandle.mostVisited;
384   var iframe = $('mv-single').contentWindow;
386   tiles = [];
387   for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) {
388     iframe.postMessage({cmd: 'tile', rid: pages[i].rid}, '*');
389   }
390   iframe.postMessage({cmd: 'show'}, '*');
395  * Shows the blacklist notification and triggers a delay to hide it.
396  */
397 function showNotification() {
398   notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
399   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
400   notification.scrollTop;
401   notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION);
406  * Hides the blacklist notification.
407  */
408 function hideNotification() {
409   notification.classList.add(CLASSES.HIDE_NOTIFICATION);
410   notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION);
415  * Handles a click on the notification undo link by hiding the notification and
416  * informing Chrome.
417  */
418 function onUndo() {
419   hideNotification();
420   if (lastBlacklistedTile != null) {
421     ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile);
422   }
427  * Handles a click on the restore all notification link by hiding the
428  * notification and informing Chrome.
429  */
430 function onRestoreAll() {
431   hideNotification();
432   ntpApiHandle.undoAllMostVisitedDeletions();
437  * Recomputes the number of tile columns, and width of various contents based
438  * on the width of the window.
439  * @return {boolean} Whether the number of tile columns has changed.
440  */
441 function updateContentWidth() {
442   var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin;
443   // If innerWidth is zero, then use the maximum snap size.
444   var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth -
445       NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING;
446   var innerWidth = window.innerWidth || maxSnapSize;
447   // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin.
448   var availableWidth = innerWidth + NTP_DESIGN.tileMargin -
449       MIN_TOTAL_HORIZONTAL_PADDING;
450   var newNumColumns = Math.floor(availableWidth / tileRequiredWidth);
451   if (newNumColumns < MIN_NUM_COLUMNS)
452     newNumColumns = MIN_NUM_COLUMNS;
453   else if (newNumColumns > MAX_NUM_COLUMNS)
454     newNumColumns = MAX_NUM_COLUMNS;
456   if (numColumnsShown === newNumColumns)
457     return false;
459   numColumnsShown = newNumColumns;
460   var tilesContainerWidth = numColumnsShown * tileRequiredWidth;
461   $(IDS.TILES).style.width = tilesContainerWidth + 'px';
462   if (fakebox) {
463     // -2 to account for border.
464     var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2);
465     fakebox.style.width = fakeboxWidth + 'px';
466   }
467   return true;
472  * Resizes elements because the number of tile columns may need to change in
473  * response to resizing. Also shows or hides extra tiles tiles according to the
474  * new width of the page.
475  */
476 function onResize() {
477   updateContentWidth();
482  * Handles new input by disposing the NTP, according to where the input was
483  * entered.
484  */
485 function onInputStart() {
486   if (fakebox && isFakeboxFocused()) {
487     setFakeboxFocus(false);
488     setFakeboxDragFocus(false);
489     disposeNtp(true);
490   } else if (!isFakeboxFocused()) {
491     disposeNtp(false);
492   }
497  * Disposes the NTP, according to where the input was entered.
498  * @param {boolean} wasFakeboxInput True if the input was in the fakebox.
499  */
500 function disposeNtp(wasFakeboxInput) {
501   var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior;
502   if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX)
503     setFakeboxActive(false);
504   else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO)
505     setFakeboxAndLogoVisibility(false);
510  * Restores the NTP (re-enables the fakebox and unhides the logo.)
511  */
512 function restoreNtp() {
513   setFakeboxActive(true);
514   setFakeboxAndLogoVisibility(true);
519  * @param {boolean} focus True to focus the fakebox.
520  */
521 function setFakeboxFocus(focus) {
522   document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus);
526  * @param {boolean} focus True to show a dragging focus to the fakebox.
527  */
528 function setFakeboxDragFocus(focus) {
529   document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus);
533  * @return {boolean} True if the fakebox has focus.
534  */
535 function isFakeboxFocused() {
536   return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) ||
537       document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS);
542  * @param {boolean} enable True to enable the fakebox.
543  */
544 function setFakeboxActive(enable) {
545   document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable);
550  * @param {!Event} event The click event.
551  * @return {boolean} True if the click occurred in an enabled fakebox.
552  */
553 function isFakeboxClick(event) {
554   return fakebox.contains(event.target) &&
555       !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE);
560  * @param {boolean} show True to show the fakebox and logo.
561  */
562 function setFakeboxAndLogoVisibility(show) {
563   document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show);
568  * Shortcut for document.getElementById.
569  * @param {string} id of the element.
570  * @return {HTMLElement} with the id.
571  */
572 function $(id) {
573   return document.getElementById(id);
578  * Utility function which creates an element with an optional classname and
579  * appends it to the specified parent.
580  * @param {Element} parent The parent to append the new element.
581  * @param {string} name The name of the new element.
582  * @param {string=} opt_class The optional classname of the new element.
583  * @return {Element} The new element.
584  */
585 function createAndAppendElement(parent, name, opt_class) {
586   var child = document.createElement(name);
587   if (opt_class)
588     child.classList.add(opt_class);
589   parent.appendChild(child);
590   return child;
595  * @param {!Element} element The element to register the handler for.
596  * @param {number} keycode The keycode of the key to register.
597  * @param {!Function} handler The key handler to register.
598  */
599 function registerKeyHandler(element, keycode, handler) {
600   element.addEventListener('keydown', function(event) {
601     if (event.keyCode == keycode)
602       handler(event);
603   });
608  * @return {Object} the handle to the embeddedSearch API.
609  */
610 function getEmbeddedSearchApiHandle() {
611   if (window.cideb)
612     return window.cideb;
613   if (window.chrome && window.chrome.embeddedSearch)
614     return window.chrome.embeddedSearch;
615   return null;
620  * Event handler for the focus changed and blacklist messages on link elements.
621  * Used to toggle visual treatment on the tiles (depending on the message).
622  * @param {Event} event Event received.
623  */
624 function handlePostMessage(event) {
625   var cmd = event.data.cmd;
626   var args = event.data;
627   if (cmd == 'tileBlacklisted') {
628     showNotification();
629     lastBlacklistedTile = args.rid;
631     ntpApiHandle.deleteMostVisitedItem(args.rid);
632   }
637  * Prepares the New Tab Page by adding listeners, rendering the current
638  * theme, the most visited pages section, and Google-specific elements for a
639  * Google-provided page.
640  */
641 function init() {
642   notification = $(IDS.NOTIFICATION);
643   attribution = $(IDS.ATTRIBUTION);
644   ntpContents = $(IDS.NTP_CONTENTS);
646   if (configData.isGooglePage) {
647     var logo = document.createElement('div');
648     logo.id = IDS.LOGO;
649     logo.title = 'Google';
651     fakebox = document.createElement('div');
652     fakebox.id = IDS.FAKEBOX;
653     var fakeboxHtml = [];
654     fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>');
655     fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT +
656         '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">');
657     fakeboxHtml.push('<div id="cursor"></div>');
658     fakebox.innerHTML = fakeboxHtml.join('');
660     ntpContents.insertBefore(fakebox, ntpContents.firstChild);
661     ntpContents.insertBefore(logo, ntpContents.firstChild);
662   } else {
663     document.body.classList.add(CLASSES.NON_GOOGLE_PAGE);
664   }
666   // Hide notifications after fade out, so we can't focus on links via keyboard.
667   notification.addEventListener('webkitTransitionEnd', hideNotification);
669   var notificationMessage = $(IDS.NOTIFICATION_MESSAGE);
670   notificationMessage.textContent =
671       configData.translatedStrings.thumbnailRemovedNotification;
673   var undoLink = $(IDS.UNDO_LINK);
674   undoLink.addEventListener('click', onUndo);
675   registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo);
676   undoLink.textContent = configData.translatedStrings.undoThumbnailRemove;
678   var restoreAllLink = $(IDS.RESTORE_ALL_LINK);
679   restoreAllLink.addEventListener('click', onRestoreAll);
680   registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo);
681   restoreAllLink.textContent =
682       configData.translatedStrings.restoreThumbnailsShort;
684   $(IDS.ATTRIBUTION_TEXT).textContent =
685       configData.translatedStrings.attributionIntro;
687   var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON);
688   createAndAppendElement(
689       notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER);
690   notificationCloseButton.addEventListener('click', hideNotification);
692   window.addEventListener('resize', onResize);
693   updateContentWidth();
695   var topLevelHandle = getEmbeddedSearchApiHandle();
697   ntpApiHandle = topLevelHandle.newTabPage;
698   ntpApiHandle.onthemechange = onThemeChange;
699   ntpApiHandle.onmostvisitedchange = onMostVisitedChange;
701   ntpApiHandle.oninputstart = onInputStart;
702   ntpApiHandle.oninputcancel = restoreNtp;
704   if (ntpApiHandle.isInputInProgress)
705     onInputStart();
707   renderTheme();
709   searchboxApiHandle = topLevelHandle.searchBox;
711   if (fakebox) {
712     // Listener for updating the key capture state.
713     document.body.onmousedown = function(event) {
714       if (isFakeboxClick(event))
715         searchboxApiHandle.startCapturingKeyStrokes();
716       else if (isFakeboxFocused())
717         searchboxApiHandle.stopCapturingKeyStrokes();
718     };
719     searchboxApiHandle.onkeycapturechange = function() {
720       setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
721     };
722     var inputbox = $(IDS.FAKEBOX_INPUT);
723     if (inputbox) {
724       inputbox.onpaste = function(event) {
725         event.preventDefault();
726         // Send pasted text to Omnibox.
727         var text = event.clipboardData.getData('text/plain');
728         if (text)
729           searchboxApiHandle.paste(text);
730       };
731       inputbox.ondrop = function(event) {
732         event.preventDefault();
733         var text = event.dataTransfer.getData('text/plain');
734         if (text) {
735           searchboxApiHandle.paste(text);
736         }
737       };
738       inputbox.ondragenter = function() {
739         setFakeboxDragFocus(true);
740       };
741       inputbox.ondragleave = function() {
742         setFakeboxDragFocus(false);
743       };
744     }
746     // Update the fakebox style to match the current key capturing state.
747     setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled);
748   }
750   if (searchboxApiHandle.rtl) {
751     $(IDS.NOTIFICATION).dir = 'rtl';
752     // Grabbing the root HTML element.
753     document.documentElement.setAttribute('dir', 'rtl');
754     // Add class for setting alignments based on language directionality.
755     document.documentElement.classList.add(CLASSES.RTL);
756   }
758   var iframe = document.createElement('iframe');
759   iframe.id = 'mv-single';
760   var args = [];
762   if (searchboxApiHandle.rtl) {
763     args.push('rtl=1');
764   }
766   iframe.src = '//most-visited/single.html?' + args.join('&');
767   $(IDS.TILES).appendChild(iframe);
769   iframe.onload = function() {
770     reloadTiles();
771   };
773   window.addEventListener('message', handlePostMessage);
778  * Binds event listeners.
779  */
780 function listen() {
781   document.addEventListener('DOMContentLoaded', init);
784 return {
785   init: init,
786   listen: listen
790 if (!window.localNTPUnitTest) {
791   LocalNTP().listen();