Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / most_visited_single.js
blob6c51584b5437f40e9d0a1a054a16fc8e65d66e6a
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. */
5  // Single iframe for NTP tiles.
6 (function() {
7 'use strict';
10 /**
11  * The different types of events that are logged from the NTP.  This enum is
12  * used to transfer information from the NTP JavaScript to the renderer and is
13  * not used as a UMA enum histogram's logged value.
14  * Note: Keep in sync with common/ntp_logging_events.h
15  * @enum {number}
16  * @const
17  */
18 var LOG_TYPE = {
19   // The suggestion is coming from the server. Unused here.
20   NTP_SERVER_SIDE_SUGGESTION: 0,
21   // The suggestion is coming from the client.
22   NTP_CLIENT_SIDE_SUGGESTION: 1,
23   // Indicates a tile was rendered, no matter if it's a thumbnail, a gray tile
24   // or an external tile.
25   NTP_TILE: 2,
26   // The tile uses a local thumbnail image.
27   NTP_THUMBNAIL_TILE: 3,
28   // Used when no thumbnail is specified and a gray tile with the domain is used
29   // as the main tile. Unused here.
30   NTP_GRAY_TILE: 4,
31   // The visuals of that tile are handled externally by the page itself.
32   // Unused here.
33   NTP_EXTERNAL_TILE: 5,
34   // There was an error in loading both the thumbnail image and the fallback
35   // (if it was provided), resulting in a gray tile.
36   NTP_THUMBNAIL_ERROR: 6,
37   // Used a gray tile with the domain as the fallback for a failed thumbnail.
38   // Unused here.
39   NTP_GRAY_TILE_FALLBACK: 7,
40   // The visuals of that tile's fallback are handled externally. Unused here.
41   NTP_EXTERNAL_TILE_FALLBACK: 8,
42   // The user moused over an NTP tile.
43   NTP_MOUSEOVER: 9,
44   // A NTP Tile has finished loading (successfully or failing).
45   NTP_TILE_LOADED: 10,
49 /**
50  * Total number of tiles to show at any time. If the host page doesn't send
51  * enough tiles, we fill them blank.
52  * @const {number}
53  */
54 var NUMBER_OF_TILES = 8;
57 /**
58  * Whether to use icons instead of thumbnails.
59  * @type {boolean}
60  */
61 var USE_ICONS = false;
64 /**
65  * Number of lines to display in titles.
66  * @type {number}
67  */
68 var NUM_TITLE_LINES = 1;
71 /**
72  * The origin of this request.
73  * @const {string}
74  */
75 var DOMAIN_ORIGIN = '{{ORIGIN}}';
78 /**
79  * Counter for DOM elements that we are waiting to finish loading.
80  * @type {number}
81  */
82 var loadedCounter = 1;
85 /**
86  * DOM element containing the tiles we are going to present next.
87  * Works as a double-buffer that is shown when we receive a "show" postMessage.
88  * @type {Element}
89  */
90 var tiles = null;
93 /**
94  * List of parameters passed by query args.
95  * @type {Object}
96  */
97 var queryArgs = {};
99 /**
100  * Url to ping when suggestions have been shown.
101  */
102 var impressionUrl = null;
106  * Log an event on the NTP.
107  * @param {number} eventType Event from LOG_TYPE.
108  */
109 var logEvent = function(eventType) {
110   chrome.embeddedSearch.newTabPage.logEvent(eventType);
115  * Down counts the DOM elements that we are waiting for the page to load.
116  * When we get to 0, we send a message to the parent window.
117  * This is usually used as an EventListener of onload/onerror.
118  */
119 var countLoad = function() {
120   loadedCounter -= 1;
121   if (loadedCounter <= 0) {
122     showTiles();
123     logEvent(LOG_TYPE.NTP_TILE_LOADED);
124     window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN);
125     loadedCounter = 1;
126   }
131  * Handles postMessages coming from the host page to the iframe.
132  * Mostly, it dispatches every command to handleCommand.
133  */
134 var handlePostMessage = function(event) {
135   if (event.data instanceof Array) {
136     for (var i = 0; i < event.data.length; ++i) {
137       handleCommand(event.data[i]);
138     }
139   } else {
140     handleCommand(event.data);
141   }
146  * Handles a single command coming from the host page to the iframe.
147  * We try to keep the logic here to a minimum and just dispatch to the relevant
148  * functions.
149  */
150 var handleCommand = function(data) {
151   var cmd = data.cmd;
153   if (cmd == 'tile') {
154     addTile(data);
155   } else if (cmd == 'show') {
156     countLoad();
157     hideOverflowTiles(data);
158   } else if (cmd == 'updateTheme') {
159     updateTheme(data);
160   } else if (cmd == 'tilesVisible') {
161     hideOverflowTiles(data);
162   } else {
163     console.error('Unknown command: ' + JSON.stringify(data));
164   }
168 var updateTheme = function(info) {
169   var themeStyle = [];
171   if (info.tileBorderColor) {
172     themeStyle.push('.thumb-ntp .mv-tile {' +
173         'border: 1px solid ' + info.tileBorderColor + '; }');
174   }
175   if (info.tileHoverBorderColor) {
176     themeStyle.push('.thumb-ntp .mv-tile:hover {' +
177         'border-color: ' + info.tileHoverBorderColor + '; }');
178   }
179   if (info.isThemeDark) {
180     themeStyle.push('.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' +
181         'background: rgb(51,51,51); }');
182     themeStyle.push('.thumb-ntp .mv-thumb.failed-img { ' +
183         'background-color: #555; }');
184     themeStyle.push('.thumb-ntp .mv-thumb.failed-img::after { ' +
185         'border-color: #333; }');
186     themeStyle.push('.thumb-ntp .mv-x { ' +
187         'background: linear-gradient(to left, ' +
188         'rgb(51,51,51) 60%, transparent); }');
189     themeStyle.push('html[dir=rtl] .thumb-ntp .mv-x { ' +
190         'background: linear-gradient(to right, ' +
191         'rgb(51,51,51) 60%, transparent); }');
192     themeStyle.push('.thumb-ntp .mv-x::after { ' +
193         'background-color: rgba(255,255,255,0.7); }');
194     themeStyle.push('.thumb-ntp .mv-x:hover::after { ' +
195         'background-color: #fff; }');
196     themeStyle.push('.thumb-ntp .mv-x:active::after { ' +
197         'background-color: rgba(255,255,255,0.5); }');
198     themeStyle.push('.icon-ntp .mv-tile:focus { ' +
199         'background: rgba(255,255,255,0.2); }');
200   }
201   if (info.tileTitleColor) {
202     themeStyle.push('body { color: ' + info.tileTitleColor + '; }');
203   }
205   document.querySelector('#custom-theme').textContent = themeStyle.join('\n');
210  * Hides extra tiles that don't fit on screen.
211  */
212 var hideOverflowTiles = function(data) {
213   var tileAndEmptyTileList = document.querySelectorAll(
214       '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile');
215   for (var i = 0; i < tileAndEmptyTileList.length; ++i) {
216     tileAndEmptyTileList[i].classList.toggle('hidden', i >= data.maxVisible);
217   }
222  * Removes all old instances of #mv-tiles that are pending for deletion.
223  */
224 var removeAllOldTiles = function() {
225   var parent = document.querySelector('#most-visited');
226   var oldList = parent.querySelectorAll('.mv-tiles-old');
227   for (var i = 0; i < oldList.length; ++i) {
228     parent.removeChild(oldList[i]);
229   }
234  * Called when the host page has finished sending us tile information and
235  * we are ready to show the new tiles and drop the old ones.
236  */
237 var showTiles = function() {
238   // Store the tiles on the current closure.
239   var cur = tiles;
241   // Create empty tiles until we have NUMBER_OF_TILES.
242   while (cur.childNodes.length < NUMBER_OF_TILES) {
243     addTile({});
244   }
246   var parent = document.querySelector('#most-visited');
248   // Mark old tile DIV for removal after the transition animation is done.
249   var old = parent.querySelector('#mv-tiles');
250   if (old) {
251     old.removeAttribute('id');
252     old.classList.add('mv-tiles-old');
253     old.style.opacity = 0.0;
254     cur.addEventListener('webkitTransitionEnd', function(ev) {
255       if (ev.target === cur) {
256         removeAllOldTiles();
257       }
258     });
259   }
261   // Add new tileset.
262   cur.id = 'mv-tiles';
263   parent.appendChild(cur);
264   // We want the CSS transition to trigger, so need to add to the DOM before
265   // setting the style.
266   setTimeout(function() {
267     cur.style.opacity = 1.0;
268   }, 0);
270   // Make sure the tiles variable contain the next tileset we may use.
271   tiles = document.createElement('div');
273   if (impressionUrl) {
274     if (navigator.sendBeacon) {
275       navigator.sendBeacon(impressionUrl);
276     } else {
277       // if sendBeacon is not enabled, we fallback to "a ping".
278       var a = document.createElement('a');
279       a.href = '#';
280       a.ping = impressionUrl;
281       a.click();
282     }
283     impressionUrl = null;
284   }
289  * Called when the host page wants to add a suggestion tile.
290  * For Most Visited, it grabs the data from Chrome and pass on.
291  * For host page generated it just passes the data.
292  * @param {object} args Data for the tile to be rendered.
293  */
294 var addTile = function(args) {
295   if (args.rid) {
296     var data = chrome.embeddedSearch.searchBox.getMostVisitedItemData(args.rid);
297     data.tid = data.rid;
298     if (!data.faviconUrl) {
299       data.faviconUrl = 'chrome-search://favicon/size/16@' +
300           window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid;
301     }
302     tiles.appendChild(renderTile(data));
303     logEvent(LOG_TYPE.NTP_CLIENT_SIDE_SUGGESTION);
304   } else if (args.id) {
305     tiles.appendChild(renderTile(args));
306     logEvent(LOG_TYPE.NTP_SERVER_SIDE_SUGGESTION);
307   } else {
308     tiles.appendChild(renderTile(null));
309   }
314  * Called when the user decided to add a tile to the blacklist.
315  * It sets of the animation for the blacklist and sends the blacklisted id
316  * to the host page.
317  * @param {Element} tile DOM node of the tile we want to remove.
318  */
319 var blacklistTile = function(tile) {
320   tile.classList.add('blacklisted');
321   tile.addEventListener('webkitTransitionEnd', function(ev) {
322     if (ev.propertyName != 'width') return;
324     window.parent.postMessage({cmd: 'tileBlacklisted',
325                                tid: Number(tile.getAttribute('data-tid'))},
326                               DOMAIN_ORIGIN);
327   });
332  * Renders a MostVisited tile to the DOM.
333  * @param {object} data Object containing rid, url, title, favicon, thumbnail.
334  *     data is null if you want to construct an empty tile.
335  */
336 var renderTile = function(data) {
337   var tile = document.createElement('a');
339   if (data == null) {
340     tile.className = 'mv-empty-tile';
341     return tile;
342   }
344   logEvent(LOG_TYPE.NTP_TILE);
346   tile.className = 'mv-tile';
347   tile.setAttribute('data-tid', data.tid);
348   var tooltip = queryArgs['removeTooltip'] || '';
349   var html = [];
350   if (!USE_ICONS) {
351     html.push('<div class="mv-favicon"></div>');
352   }
353   html.push('<div class="mv-title"></div><div class="mv-thumb"></div>');
354   html.push('<div title="' + tooltip + '" class="mv-x"></div>');
355   tile.innerHTML = html.join('');
357   tile.href = data.url;
358   tile.title = data.title;
359   if (data.impressionUrl) {
360     impressionUrl = data.impressionUrl;
361   }
362   if (data.pingUrl) {
363     tile.addEventListener('click', function(ev) {
364       if (navigator.sendBeacon) {
365         navigator.sendBeacon(data.pingUrl);
366       } else {
367         // if sendBeacon is not enabled, we fallback to "a ping".
368         var a = document.createElement('a');
369         a.href = '#';
370         a.ping = data.pingUrl;
371         a.click();
372       }
373     });
374   }
375   // For local suggestions, we use navigateContentWindow instead of the default
376   // action, since it includes support for file:// urls.
377   if (data.rid) {
378     tile.addEventListener('click', function(ev) {
379       ev.preventDefault();
380       var disp = chrome.embeddedSearch.newTabPage.getDispositionFromClick(
381         ev.button == 1,  // MIDDLE BUTTON
382         ev.altKey, ev.ctrlKey, ev.metaKey, ev.shiftKey);
384       window.chrome.embeddedSearch.newTabPage.navigateContentWindow(this.href,
385                                                                     disp);
386     });
387   }
389   tile.addEventListener('keydown', function(event) {
390     if (event.keyCode == 46 /* DELETE */ ||
391         event.keyCode == 8 /* BACKSPACE */) {
392       event.preventDefault();
393       event.stopPropagation();
394       blacklistTile(this);
395     } else if (event.keyCode == 13 /* ENTER */ ||
396                event.keyCode == 32 /* SPACE */) {
397       event.preventDefault();
398       this.click();
399     } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) {
400       var tiles = document.querySelectorAll('#mv-tiles .mv-tile');
401       var nextTile = null;
402       // Use the location of the tile to find the next one in the
403       // appropriate direction.
404       // For LEFT and UP we keep iterating until we find the last element
405       // that fulfills the conditions.
406       // For RIGHT and DOWN we accept the first element that works.
407       if (event.keyCode == 37 /* LEFT */) {
408         for (var i = 0; i < tiles.length; i++) {
409           var tile = tiles[i];
410           if (tile.offsetTop == this.offsetTop &&
411               tile.offsetLeft < this.offsetLeft) {
412             if (!nextTile || tile.offsetLeft > nextTile.offsetLeft) {
413               nextTile = tile;
414             }
415           }
416         }
417       }
418       if (event.keyCode == 38 /* UP */) {
419         for (var i = 0; i < tiles.length; i++) {
420           var tile = tiles[i];
421           if (tile.offsetTop < this.offsetTop &&
422               tile.offsetLeft == this.offsetLeft) {
423             if (!nextTile || tile.offsetTop > nextTile.offsetTop) {
424               nextTile = tile;
425             }
426           }
427         }
428       }
429       if (event.keyCode == 39 /* RIGHT */) {
430         for (var i = 0; i < tiles.length; i++) {
431           var tile = tiles[i];
432           if (tile.offsetTop == this.offsetTop &&
433               tile.offsetLeft > this.offsetLeft) {
434             if (!nextTile || tile.offsetLeft < nextTile.offsetLeft) {
435               nextTile = tile;
436             }
437           }
438         }
439       }
440       if (event.keyCode == 40 /* DOWN */) {
441         for (var i = 0; i < tiles.length; i++) {
442           var tile = tiles[i];
443           if (tile.offsetTop > this.offsetTop &&
444               tile.offsetLeft == this.offsetLeft) {
445             if (!nextTile || tile.offsetTop < nextTile.offsetTop) {
446               nextTile = tile;
447             }
448           }
449         }
450       }
452       if (nextTile) {
453         nextTile.focus();
454       }
455     }
456   });
457   // TODO(fserb): remove this or at least change to mouseenter.
458   tile.addEventListener('mouseover', function() {
459     logEvent(LOG_TYPE.NTP_MOUSEOVER);
460   });
462   var title = tile.querySelector('.mv-title');
463   title.innerText = data.title;
464   title.style.direction = data.direction || 'ltr';
465   if (NUM_TITLE_LINES > 1) {
466     title.classList.add('multiline');
467   }
469   if (USE_ICONS) {
470     var thumb = tile.querySelector('.mv-thumb');
471     if (data.largeIconUrl) {
472       var img = document.createElement('img');
473       img.title = data.title;
474       img.src = data.largeIconUrl;
475       img.classList.add('large-icon');
476       loadedCounter += 1;
477       img.addEventListener('load', countLoad);
478       img.addEventListener('load', function(ev) {
479         thumb.classList.add('large-icon-outer');
480       });
481       img.addEventListener('error', countLoad);
482       img.addEventListener('error', function(ev) {
483         thumb.classList.add('failed-img');
484         thumb.removeChild(img);
485         logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
486       });
487       thumb.appendChild(img);
488       logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);
489     } else {
490       thumb.classList.add('failed-img');
491     }
492   } else { // THUMBNAILS
493     // We keep track of the outcome of loading possible thumbnails for this
494     // tile. Possible values:
495     //   - null: waiting for load/error
496     //   - false: error
497     //   - a string: URL that loaded correctly.
498     // This is populated by acceptImage/rejectImage and loadBestImage
499     // decides the best one to load.
500     var results = [];
501     var thumb = tile.querySelector('.mv-thumb');
502     var img = document.createElement('img');
503     var loaded = false;
505     var loadBestImage = function() {
506       if (loaded) {
507         return;
508       }
509       for (var i = 0; i < results.length; ++i) {
510         if (results[i] === null) {
511           return;
512         }
513         if (results[i] != false) {
514           img.src = results[i];
515           loaded = true;
516           return;
517         }
518       }
519       thumb.classList.add('failed-img');
520       thumb.removeChild(img);
521       logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
522       countLoad();
523     };
525     var acceptImage = function(idx, url) {
526       return function(ev) {
527         results[idx] = url;
528         loadBestImage();
529       };
530     };
532     var rejectImage = function(idx) {
533       return function(ev) {
534         results[idx] = false;
535         loadBestImage();
536       };
537     };
539     img.title = data.title;
540     img.classList.add('thumbnail');
541     loadedCounter += 1;
542     img.addEventListener('load', countLoad);
543     img.addEventListener('error', countLoad);
544     img.addEventListener('error', function(ev) {
545       thumb.classList.add('failed-img');
546       thumb.removeChild(img);
547       logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
548     });
549     thumb.appendChild(img);
550     logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);
552     if (data.thumbnailUrl) {
553       img.src = data.thumbnailUrl;
554     } else {
555       // Get all thumbnailUrls for the tile.
556       // They are ordered from best one to be used to worst.
557       for (var i = 0; i < data.thumbnailUrls.length; ++i) {
558         results.push(null);
559       }
560       for (var i = 0; i < data.thumbnailUrls.length; ++i) {
561         if (data.thumbnailUrls[i]) {
562           var image = new Image();
563           image.src = data.thumbnailUrls[i];
564           image.onload = acceptImage(i, data.thumbnailUrls[i]);
565           image.onerror = rejectImage(i);
566         } else {
567           rejectImage(i)(null);
568         }
569       }
570     }
572     var favicon = tile.querySelector('.mv-favicon');
573     if (data.faviconUrl) {
574       var fi = document.createElement('img');
575       fi.src = data.faviconUrl;
576       // Set the title to empty so screen readers won't say the image name.
577       fi.title = '';
578       loadedCounter += 1;
579       fi.addEventListener('load', countLoad);
580       fi.addEventListener('error', countLoad);
581       fi.addEventListener('error', function(ev) {
582         favicon.classList.add('failed-favicon');
583       });
584       favicon.appendChild(fi);
585     } else {
586       favicon.classList.add('failed-favicon');
587     }
588   }
590   var mvx = tile.querySelector('.mv-x');
591   mvx.addEventListener('click', function(ev) {
592     removeAllOldTiles();
593     blacklistTile(tile);
594     ev.preventDefault();
595     ev.stopPropagation();
596   });
598   return tile;
603  * Do some initialization and parses the query arguments passed to the iframe.
604  */
605 var init = function() {
606   // Creates a new DOM element to hold the tiles.
607   tiles = document.createElement('div');
609   // Parse query arguments.
610   var query = window.location.search.substring(1).split('&');
611   queryArgs = {};
612   for (var i = 0; i < query.length; ++i) {
613     var val = query[i].split('=');
614     if (val[0] == '') continue;
615     queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]);
616   }
618   // Apply class for icon NTP, if specified.
619   USE_ICONS = queryArgs['icons'] == '1';
620   if ('ntl' in queryArgs) {
621     var ntl = parseInt(queryArgs['ntl'], 10);
622     if (isFinite(ntl))
623       NUM_TITLE_LINES = ntl;
624   }
626   // Duplicating NTP_DESIGN.mainClass.
627   document.querySelector('#most-visited').classList.add(
628       USE_ICONS ? 'icon-ntp' : 'thumb-ntp');
630   // Enable RTL.
631   if (queryArgs['rtl'] == '1') {
632     var html = document.querySelector('html');
633     html.dir = 'rtl';
634   }
636   window.addEventListener('message', handlePostMessage);
640 window.addEventListener('DOMContentLoaded', init);
641 })();