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.
101 * Log an event on the NTP.
102 * @param {number} eventType Event from LOG_TYPE.
104 var logEvent = function(eventType
) {
105 chrome
.embeddedSearch
.newTabPage
.logEvent(eventType
);
110 * Down counts the DOM elements that we are waiting for the page to load.
111 * When we get to 0, we send a message to the parent window.
112 * This is usually used as an EventListener of onload/onerror.
114 var countLoad = function() {
116 if (loadedCounter
<= 0) {
118 logEvent(LOG_TYPE
.NTP_TILE_LOADED
);
119 window
.parent
.postMessage({cmd
: 'loaded'}, DOMAIN_ORIGIN
);
126 * Handles postMessages coming from the host page to the iframe.
127 * Mostly, it dispatches every command to handleCommand.
129 var handlePostMessage = function(event
) {
130 if (event
.data
instanceof Array
) {
131 for (var i
= 0; i
< event
.data
.length
; ++i
) {
132 handleCommand(event
.data
[i
]);
135 handleCommand(event
.data
);
141 * Handles a single command coming from the host page to the iframe.
142 * We try to keep the logic here to a minimum and just dispatch to the relevant
145 var handleCommand = function(data
) {
150 } else if (cmd
== 'show') {
152 hideOverflowTiles(data
);
153 } else if (cmd
== 'updateTheme') {
155 } else if (cmd
== 'tilesVisible') {
156 hideOverflowTiles(data
);
158 console
.error('Unknown command: ' + JSON
.stringify(data
));
163 var updateTheme = function(info
) {
166 if (info
.tileBorderColor
) {
167 themeStyle
.push('.thumb-ntp .mv-tile {' +
168 'border: 1px solid ' + info
.tileBorderColor
+ '; }');
170 if (info
.tileHoverBorderColor
) {
171 themeStyle
.push('.thumb-ntp .mv-tile:hover {' +
172 'border-color: ' + info
.tileHoverBorderColor
+ '; }');
174 if (info
.isThemeDark
) {
175 themeStyle
.push('.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' +
176 'background: rgb(51,51,51); }');
177 themeStyle
.push('.thumb-ntp .mv-thumb.failed-img { ' +
178 'background-color: #555; }');
179 themeStyle
.push('.thumb-ntp .mv-thumb.failed-img::after { ' +
180 'border-color: #333; }');
181 themeStyle
.push('.thumb-ntp .mv-x { ' +
182 'background: linear-gradient(to left, ' +
183 'rgb(51,51,51) 60%, transparent); }');
184 themeStyle
.push('html[dir=rtl] .thumb-ntp .mv-x { ' +
185 'background: linear-gradient(to right, ' +
186 'rgb(51,51,51) 60%, transparent); }');
187 themeStyle
.push('.thumb-ntp .mv-x::after { ' +
188 'background-color: rgba(255,255,255,0.7); }');
189 themeStyle
.push('.thumb-ntp .mv-x:hover::after { ' +
190 'background-color: #fff; }');
191 themeStyle
.push('.thumb-ntp .mv-x:active::after { ' +
192 'background-color: rgba(255,255,255,0.5); }');
193 themeStyle
.push('.icon-ntp .mv-tile:focus { ' +
194 'background: rgba(255,255,255,0.2); }');
196 if (info
.tileTitleColor
) {
197 themeStyle
.push('body { color: ' + info
.tileTitleColor
+ '; }');
200 document
.querySelector('#custom-theme').textContent
= themeStyle
.join('\n');
205 * Hides extra tiles that don't fit on screen.
207 var hideOverflowTiles = function(data
) {
208 var tileAndEmptyTileList
= document
.querySelectorAll(
209 '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile');
210 for (var i
= 0; i
< tileAndEmptyTileList
.length
; ++i
) {
211 tileAndEmptyTileList
[i
].classList
.toggle('hidden', i
>= data
.maxVisible
);
217 * Removes all old instances of #mv-tiles that are pending for deletion.
219 var removeAllOldTiles = function() {
220 var parent
= document
.querySelector('#most-visited');
221 var oldList
= parent
.querySelectorAll('.mv-tiles-old');
222 for (var i
= 0; i
< oldList
.length
; ++i
) {
223 parent
.removeChild(oldList
[i
]);
229 * Called when the host page has finished sending us tile information and
230 * we are ready to show the new tiles and drop the old ones.
232 var showTiles = function() {
233 // Store the tiles on the current closure.
236 // Create empty tiles until we have NUMBER_OF_TILES.
237 while (cur
.childNodes
.length
< NUMBER_OF_TILES
) {
241 var parent
= document
.querySelector('#most-visited');
243 // Mark old tile DIV for removal after the transition animation is done.
244 var old
= parent
.querySelector('#mv-tiles');
246 old
.removeAttribute('id');
247 old
.classList
.add('mv-tiles-old');
248 old
.style
.opacity
= 0.0;
249 cur
.addEventListener('webkitTransitionEnd', function(ev
) {
250 if (ev
.target
=== cur
) {
258 parent
.appendChild(cur
);
259 // We want the CSS transition to trigger, so need to add to the DOM before
260 // setting the style.
261 setTimeout(function() {
262 cur
.style
.opacity
= 1.0;
265 // Make sure the tiles variable contain the next tileset we may use.
266 tiles
= document
.createElement('div');
271 * Called when the host page wants to add a suggestion tile.
272 * For Most Visited, it grabs the data from Chrome and pass on.
273 * For host page generated it just passes the data.
274 * @param {object} args Data for the tile to be rendered.
276 var addTile = function(args
) {
278 var data
= chrome
.embeddedSearch
.searchBox
.getMostVisitedItemData(args
.rid
);
280 if (!data
.thumbnailUrls
) {
281 data
.thumbnailUrls
= [data
.thumbnailUrl
];
283 if (!data
.faviconUrl
) {
284 data
.faviconUrl
= 'chrome-search://favicon/size/16@' +
285 window
.devicePixelRatio
+ 'x/' + data
.renderViewId
+ '/' + data
.tid
;
287 tiles
.appendChild(renderTile(data
));
288 logEvent(LOG_TYPE
.NTP_CLIENT_SIDE_SUGGESTION
);
289 } else if (args
.id
) {
290 tiles
.appendChild(renderTile(args
));
291 logEvent(LOG_TYPE
.NTP_SERVER_SIDE_SUGGESTION
);
293 tiles
.appendChild(renderTile(null));
299 * Called when the user decided to add a tile to the blacklist.
300 * It sets of the animation for the blacklist and sends the blacklisted id
302 * @param {Element} tile DOM node of the tile we want to remove.
304 var blacklistTile = function(tile
) {
305 tile
.classList
.add('blacklisted');
306 tile
.addEventListener('webkitTransitionEnd', function(ev
) {
307 if (ev
.propertyName
!= 'width') return;
309 window
.parent
.postMessage({cmd
: 'tileBlacklisted',
310 tid
: Number(tile
.getAttribute('data-tid'))},
317 * Renders a MostVisited tile to the DOM.
318 * @param {object} data Object containing rid, url, title, favicon, thumbnail.
319 * data is null if you want to construct an empty tile.
321 var renderTile = function(data
) {
322 var tile
= document
.createElement('a');
325 tile
.className
= 'mv-empty-tile';
329 logEvent(LOG_TYPE
.NTP_TILE
);
331 tile
.className
= 'mv-tile';
332 tile
.setAttribute('data-tid', data
.tid
);
333 var tooltip
= queryArgs
['removeTooltip'] || '';
336 html
.push('<div class="mv-favicon"></div>');
338 html
.push('<div class="mv-title"></div><div class="mv-thumb"></div>');
339 html
.push('<div title="' + tooltip
+ '" class="mv-x"></div>');
340 tile
.innerHTML
= html
.join('');
342 tile
.href
= data
.url
;
343 tile
.title
= data
.title
;
345 tile
.addEventListener('click', function(ev
) {
346 if (navigator
.sendBeacon
) {
347 navigator
.sendBeacon(data
.pingUrl
);
349 // if sendBeacon is not enabled, we fallback to "a ping".
350 var a
= document
.createElement('a');
352 a
.ping
= data
.pingUrl
;
357 // For local suggestions, we use navigateContentWindow instead of the default
358 // action, since it includes support for file:// urls.
360 tile
.addEventListener('click', function(ev
) {
362 var disp
= chrome
.embeddedSearch
.newTabPage
.getDispositionFromClick(
363 ev
.button
== 1, // MIDDLE BUTTON
364 ev
.altKey
, ev
.ctrlKey
, ev
.metaKey
, ev
.shiftKey
);
366 window
.chrome
.embeddedSearch
.newTabPage
.navigateContentWindow(this.href
,
371 tile
.addEventListener('keydown', function(event
) {
372 if (event
.keyCode
== 46 /* DELETE */ ||
373 event
.keyCode
== 8 /* BACKSPACE */) {
374 event
.preventDefault();
375 event
.stopPropagation();
377 } else if (event
.keyCode
== 13 /* ENTER */ ||
378 event
.keyCode
== 32 /* SPACE */) {
379 event
.preventDefault();
381 } else if (event
.keyCode
>= 37 && event
.keyCode
<= 40 /* ARROWS */) {
382 var tiles
= document
.querySelectorAll('#mv-tiles .mv-tile');
384 // Use the location of the tile to find the next one in the
385 // appropriate direction.
386 // For LEFT and UP we keep iterating until we find the last element
387 // that fulfills the conditions.
388 // For RIGHT and DOWN we accept the first element that works.
389 if (event
.keyCode
== 37 /* LEFT */) {
390 for (var i
= 0; i
< tiles
.length
; i
++) {
392 if (tile
.offsetTop
== this.offsetTop
&&
393 tile
.offsetLeft
< this.offsetLeft
) {
394 if (!nextTile
|| tile
.offsetLeft
> nextTile
.offsetLeft
) {
400 if (event
.keyCode
== 38 /* UP */) {
401 for (var i
= 0; i
< tiles
.length
; i
++) {
403 if (tile
.offsetTop
< this.offsetTop
&&
404 tile
.offsetLeft
== this.offsetLeft
) {
405 if (!nextTile
|| tile
.offsetTop
> nextTile
.offsetTop
) {
411 if (event
.keyCode
== 39 /* RIGHT */) {
412 for (var i
= 0; i
< tiles
.length
; i
++) {
414 if (tile
.offsetTop
== this.offsetTop
&&
415 tile
.offsetLeft
> this.offsetLeft
) {
416 if (!nextTile
|| tile
.offsetLeft
< nextTile
.offsetLeft
) {
422 if (event
.keyCode
== 40 /* DOWN */) {
423 for (var i
= 0; i
< tiles
.length
; i
++) {
425 if (tile
.offsetTop
> this.offsetTop
&&
426 tile
.offsetLeft
== this.offsetLeft
) {
427 if (!nextTile
|| tile
.offsetTop
< nextTile
.offsetTop
) {
439 // TODO(fserb): remove this or at least change to mouseenter.
440 tile
.addEventListener('mouseover', function() {
441 logEvent(LOG_TYPE
.NTP_MOUSEOVER
);
444 var title
= tile
.querySelector('.mv-title');
445 title
.innerText
= data
.title
;
446 title
.style
.direction
= data
.direction
|| 'ltr';
447 if (NUM_TITLE_LINES
> 1) {
448 title
.classList
.add('multiline');
452 var thumb
= tile
.querySelector('.mv-thumb');
453 if (data
.largeIconUrl
) {
454 var img
= document
.createElement('img');
455 img
.title
= data
.title
;
456 img
.src
= data
.largeIconUrl
;
457 img
.classList
.add('large-icon');
459 img
.addEventListener('load', countLoad
);
460 img
.addEventListener('load', function(ev
) {
461 thumb
.classList
.add('large-icon-outer');
463 img
.addEventListener('error', countLoad
);
464 img
.addEventListener('error', function(ev
) {
465 thumb
.classList
.add('failed-img');
466 thumb
.removeChild(img
);
467 logEvent(LOG_TYPE
.NTP_THUMBNAIL_ERROR
);
469 thumb
.appendChild(img
);
470 logEvent(LOG_TYPE
.NTP_THUMBNAIL_TILE
);
472 thumb
.classList
.add('failed-img');
474 } else { // THUMBNAILS
475 // We keep track of the outcome of loading possible thumbnails for this
476 // tile. Possible values:
477 // - null: waiting for load/error
479 // - a string: URL that loaded correctly.
480 // This is populated by acceptImage/rejectImage and loadBestImage
481 // decides the best one to load.
483 var thumb
= tile
.querySelector('.mv-thumb');
484 var img
= document
.createElement('img');
487 var loadBestImage = function() {
491 for (var i
= 0; i
< results
.length
; ++i
) {
492 if (results
[i
] === null) {
495 if (results
[i
] != false) {
496 img
.src
= results
[i
];
501 thumb
.classList
.add('failed-img');
502 thumb
.removeChild(img
);
503 logEvent(LOG_TYPE
.NTP_THUMBNAIL_ERROR
);
507 var acceptImage = function(idx
, url
) {
508 return function(ev
) {
514 var rejectImage = function(idx
) {
515 return function(ev
) {
516 results
[idx
] = false;
521 img
.title
= data
.title
;
522 img
.classList
.add('thumbnail');
524 img
.addEventListener('load', countLoad
);
525 img
.addEventListener('error', countLoad
);
526 img
.addEventListener('error', function(ev
) {
527 thumb
.classList
.add('failed-img');
528 thumb
.removeChild(img
);
529 logEvent(LOG_TYPE
.NTP_THUMBNAIL_ERROR
);
531 thumb
.appendChild(img
);
532 logEvent(LOG_TYPE
.NTP_THUMBNAIL_TILE
);
534 // Get all thumbnailUrls for the tile.
535 // They are ordered from best one to be used to worst.
536 for (var i
= 0; i
< data
.thumbnailUrls
.length
; ++i
) {
539 for (var i
= 0; i
< data
.thumbnailUrls
.length
; ++i
) {
540 if (data
.thumbnailUrls
[i
]) {
541 var image
= new Image();
542 image
.src
= data
.thumbnailUrls
[i
];
543 image
.onload
= acceptImage(i
, data
.thumbnailUrls
[i
]);
544 image
.onerror
= rejectImage(i
);
546 rejectImage(i
)(null);
550 var favicon
= tile
.querySelector('.mv-favicon');
551 if (data
.faviconUrl
) {
552 var fi
= document
.createElement('img');
553 fi
.src
= data
.faviconUrl
;
554 // Set the title to empty so screen readers won't say the image name.
557 fi
.addEventListener('load', countLoad
);
558 fi
.addEventListener('error', countLoad
);
559 fi
.addEventListener('error', function(ev
) {
560 favicon
.classList
.add('failed-favicon');
562 favicon
.appendChild(fi
);
564 favicon
.classList
.add('failed-favicon');
568 var mvx
= tile
.querySelector('.mv-x');
569 mvx
.addEventListener('click', function(ev
) {
573 ev
.stopPropagation();
581 * Do some initialization and parses the query arguments passed to the iframe.
583 var init = function() {
584 // Creates a new DOM element to hold the tiles.
585 tiles
= document
.createElement('div');
587 // Parse query arguments.
588 var query
= window
.location
.search
.substring(1).split('&');
590 for (var i
= 0; i
< query
.length
; ++i
) {
591 var val
= query
[i
].split('=');
592 if (val
[0] == '') continue;
593 queryArgs
[decodeURIComponent(val
[0])] = decodeURIComponent(val
[1]);
596 // Apply class for icon NTP, if specified.
597 USE_ICONS
= queryArgs
['icons'] == '1';
598 if ('ntl' in queryArgs
) {
599 var ntl
= parseInt(queryArgs
['ntl'], 10);
601 NUM_TITLE_LINES
= ntl
;
604 // Duplicating NTP_DESIGN.mainClass.
605 document
.querySelector('#most-visited').classList
.add(
606 USE_ICONS
? 'icon-ntp' : 'thumb-ntp');
609 if (queryArgs
['rtl'] == '1') {
610 var html
= document
.querySelector('html');
614 window
.addEventListener('message', handlePostMessage
);
618 window
.addEventListener('DOMContentLoaded', init
);