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.
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
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.
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.
31 // The visuals of that tile are handled externally by the page itself.
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.
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.
44 // A NTP Tile has finished loading (successfully or failing).
50 * Total number of tiles to show at any time. If the host page doesn't send
51 * enough tiles, we fill them blank.
54 var NUMBER_OF_TILES = 8;
58 * Whether to use icons instead of thumbnails.
61 var USE_ICONS = false;
65 * Number of lines to display in titles.
68 var NUM_TITLE_LINES = 1;
72 * The origin of this request.
75 var DOMAIN_ORIGIN = '{{ORIGIN}}';
79 * Counter for DOM elements that we are waiting to finish loading.
82 var loadedCounter = 1;
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.
94 * List of parameters passed by query args.
100 * Url to ping when suggestions have been shown.
102 var impressionUrl = null;
106 * Log an event on the NTP.
107 * @param {number} eventType Event from LOG_TYPE.
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.
119 var countLoad = function() {
121 if (loadedCounter <= 0) {
123 logEvent(LOG_TYPE.NTP_TILE_LOADED);
124 window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN);
131 * Handles postMessages coming from the host page to the iframe.
132 * Mostly, it dispatches every command to handleCommand.
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]);
140 handleCommand(event.data);
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
150 var handleCommand = function(data) {
155 } else if (cmd == 'show') {
157 hideOverflowTiles(data);
158 } else if (cmd == 'updateTheme') {
160 } else if (cmd == 'tilesVisible') {
161 hideOverflowTiles(data);
163 console.error('Unknown command: ' + JSON.stringify(data));
168 var updateTheme = function(info) {
171 if (info.tileBorderColor) {
172 themeStyle.push('.thumb-ntp .mv-tile {' +
173 'border: 1px solid ' + info.tileBorderColor + '; }');
175 if (info.tileHoverBorderColor) {
176 themeStyle.push('.thumb-ntp .mv-tile:hover {' +
177 'border-color: ' + info.tileHoverBorderColor + '; }');
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); }');
201 if (info.tileTitleColor) {
202 themeStyle.push('body { color: ' + info.tileTitleColor + '; }');
205 document.querySelector('#custom-theme').textContent = themeStyle.join('\n');
210 * Hides extra tiles that don't fit on screen.
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);
222 * Removes all old instances of #mv-tiles that are pending for deletion.
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]);
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.
237 var showTiles = function() {
238 // Store the tiles on the current closure.
241 // Create empty tiles until we have NUMBER_OF_TILES.
242 while (cur.childNodes.length < NUMBER_OF_TILES) {
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');
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) {
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;
270 // Make sure the tiles variable contain the next tileset we may use.
271 tiles = document.createElement('div');
274 if (navigator.sendBeacon) {
275 navigator.sendBeacon(impressionUrl);
277 // if sendBeacon is not enabled, we fallback to "a ping".
278 var a = document.createElement('a');
280 a.ping = impressionUrl;
283 impressionUrl = null;
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.
294 var addTile = function(args) {
296 var data = chrome.embeddedSearch.searchBox.getMostVisitedItemData(args.rid);
298 if (!data.faviconUrl) {
299 data.faviconUrl = 'chrome-search://favicon/size/16@' +
300 window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid;
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);
308 tiles.appendChild(renderTile(null));
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
317 * @param {Element} tile DOM node of the tile we want to remove.
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'))},
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.
336 var renderTile = function(data) {
337 var tile = document.createElement('a');
340 tile.className = 'mv-empty-tile';
344 logEvent(LOG_TYPE.NTP_TILE);
346 tile.className = 'mv-tile';
347 tile.setAttribute('data-tid', data.tid);
348 var tooltip = queryArgs['removeTooltip'] || '';
351 html.push('<div class="mv-favicon"></div>');
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;
363 tile.addEventListener('click', function(ev) {
364 if (navigator.sendBeacon) {
365 navigator.sendBeacon(data.pingUrl);
367 // if sendBeacon is not enabled, we fallback to "a ping".
368 var a = document.createElement('a');
370 a.ping = data.pingUrl;
375 // For local suggestions, we use navigateContentWindow instead of the default
376 // action, since it includes support for file:// urls.
378 tile.addEventListener('click', function(ev) {
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,
389 tile.addEventListener('keydown', function(event) {
390 if (event.keyCode == 46 /* DELETE */ ||
391 event.keyCode == 8 /* BACKSPACE */) {
392 event.preventDefault();
393 event.stopPropagation();
395 } else if (event.keyCode == 13 /* ENTER */ ||
396 event.keyCode == 32 /* SPACE */) {
397 event.preventDefault();
399 } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) {
400 var tiles = document.querySelectorAll('#mv-tiles .mv-tile');
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++) {
410 if (tile.offsetTop == this.offsetTop &&
411 tile.offsetLeft < this.offsetLeft) {
412 if (!nextTile || tile.offsetLeft > nextTile.offsetLeft) {
418 if (event.keyCode == 38 /* UP */) {
419 for (var i = 0; i < tiles.length; i++) {
421 if (tile.offsetTop < this.offsetTop &&
422 tile.offsetLeft == this.offsetLeft) {
423 if (!nextTile || tile.offsetTop > nextTile.offsetTop) {
429 if (event.keyCode == 39 /* RIGHT */) {
430 for (var i = 0; i < tiles.length; i++) {
432 if (tile.offsetTop == this.offsetTop &&
433 tile.offsetLeft > this.offsetLeft) {
434 if (!nextTile || tile.offsetLeft < nextTile.offsetLeft) {
440 if (event.keyCode == 40 /* DOWN */) {
441 for (var i = 0; i < tiles.length; i++) {
443 if (tile.offsetTop > this.offsetTop &&
444 tile.offsetLeft == this.offsetLeft) {
445 if (!nextTile || tile.offsetTop < nextTile.offsetTop) {
457 // TODO(fserb): remove this or at least change to mouseenter.
458 tile.addEventListener('mouseover', function() {
459 logEvent(LOG_TYPE.NTP_MOUSEOVER);
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');
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');
477 img.addEventListener('load', countLoad);
478 img.addEventListener('load', function(ev) {
479 thumb.classList.add('large-icon-outer');
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);
487 thumb.appendChild(img);
488 logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);
490 thumb.classList.add('failed-img');
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
497 // - a string: URL that loaded correctly.
498 // This is populated by acceptImage/rejectImage and loadBestImage
499 // decides the best one to load.
501 var thumb = tile.querySelector('.mv-thumb');
502 var img = document.createElement('img');
505 var loadBestImage = function() {
509 for (var i = 0; i < results.length; ++i) {
510 if (results[i] === null) {
513 if (results[i] != false) {
514 img.src = results[i];
519 thumb.classList.add('failed-img');
520 thumb.removeChild(img);
521 logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
525 var acceptImage = function(idx, url) {
526 return function(ev) {
532 var rejectImage = function(idx) {
533 return function(ev) {
534 results[idx] = false;
539 img.title = data.title;
540 img.classList.add('thumbnail');
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);
549 thumb.appendChild(img);
550 logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);
552 if (data.thumbnailUrl) {
553 img.src = data.thumbnailUrl;
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) {
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);
567 rejectImage(i)(null);
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.
579 fi.addEventListener('load', countLoad);
580 fi.addEventListener('error', countLoad);
581 fi.addEventListener('error', function(ev) {
582 favicon.classList.add('failed-favicon');
584 favicon.appendChild(fi);
586 favicon.classList.add('failed-favicon');
590 var mvx = tile.querySelector('.mv-x');
591 mvx.addEventListener('click', function(ev) {
595 ev.stopPropagation();
603 * Do some initialization and parses the query arguments passed to the iframe.
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('&');
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]);
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);
623 NUM_TITLE_LINES = ntl;
626 // Duplicating NTP_DESIGN.mainClass.
627 document.querySelector('#most-visited').classList.add(
628 USE_ICONS ? 'icon-ntp' : 'thumb-ntp');
631 if (queryArgs['rtl'] == '1') {
632 var html = document.querySelector('html');
636 window.addEventListener('message', handlePostMessage);
640 window.addEventListener('DOMContentLoaded', init);