Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / local_ntp / most_visited_single.js
blob012abdb02fdf315a84a283e4b54fa2da95563c8f
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
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}
54 var NUMBER_OF_TILES = 8;
57 /**
58 * Whether to use icons instead of thumbnails.
59 * @type {boolean}
61 var USE_ICONS = false;
64 /**
65 * Number of lines to display in titles.
66 * @type {number}
68 var NUM_TITLE_LINES = 1;
71 /**
72 * The origin of this request.
73 * @const {string}
75 var DOMAIN_ORIGIN = '{{ORIGIN}}';
78 /**
79 * Counter for DOM elements that we are waiting to finish loading.
80 * @type {number}
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}
90 var tiles = null;
93 /**
94 * List of parameters passed by query args.
95 * @type {Object}
97 var queryArgs = {};
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() {
115 loadedCounter -= 1;
116 if (loadedCounter <= 0) {
117 showTiles();
118 logEvent(LOG_TYPE.NTP_TILE_LOADED);
119 window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN);
120 loadedCounter = 1;
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]);
134 } else {
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
143 * functions.
145 var handleCommand = function(data) {
146 var cmd = data.cmd;
148 if (cmd == 'tile') {
149 addTile(data);
150 } else if (cmd == 'show') {
151 countLoad();
152 hideOverflowTiles(data);
153 } else if (cmd == 'updateTheme') {
154 updateTheme(data);
155 } else if (cmd == 'tilesVisible') {
156 hideOverflowTiles(data);
157 } else {
158 console.error('Unknown command: ' + JSON.stringify(data));
163 var updateTheme = function(info) {
164 var themeStyle = [];
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.
234 var cur = tiles;
236 // Create empty tiles until we have NUMBER_OF_TILES.
237 while (cur.childNodes.length < NUMBER_OF_TILES) {
238 addTile({});
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');
245 if (old) {
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) {
251 removeAllOldTiles();
256 // Add new tileset.
257 cur.id = 'mv-tiles';
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;
263 }, 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) {
277 if (args.rid) {
278 var data = chrome.embeddedSearch.searchBox.getMostVisitedItemData(args.rid);
279 data.tid = data.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);
292 } else {
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
301 * to the host page.
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'))},
311 DOMAIN_ORIGIN);
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');
324 if (data == null) {
325 tile.className = 'mv-empty-tile';
326 return tile;
329 logEvent(LOG_TYPE.NTP_TILE);
331 tile.className = 'mv-tile';
332 tile.setAttribute('data-tid', data.tid);
333 var tooltip = queryArgs['removeTooltip'] || '';
334 var html = [];
335 if (!USE_ICONS) {
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;
344 if (data.pingUrl) {
345 tile.addEventListener('click', function(ev) {
346 if (navigator.sendBeacon) {
347 navigator.sendBeacon(data.pingUrl);
348 } else {
349 // if sendBeacon is not enabled, we fallback to "a ping".
350 var a = document.createElement('a');
351 a.href = '#';
352 a.ping = data.pingUrl;
353 a.click();
357 // For local suggestions, we use navigateContentWindow instead of the default
358 // action, since it includes support for file:// urls.
359 if (data.rid) {
360 tile.addEventListener('click', function(ev) {
361 ev.preventDefault();
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,
367 disp);
371 tile.addEventListener('keydown', function(event) {
372 if (event.keyCode == 46 /* DELETE */ ||
373 event.keyCode == 8 /* BACKSPACE */) {
374 event.preventDefault();
375 event.stopPropagation();
376 blacklistTile(this);
377 } else if (event.keyCode == 13 /* ENTER */ ||
378 event.keyCode == 32 /* SPACE */) {
379 event.preventDefault();
380 this.click();
381 } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) {
382 var tiles = document.querySelectorAll('#mv-tiles .mv-tile');
383 var nextTile = null;
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++) {
391 var tile = tiles[i];
392 if (tile.offsetTop == this.offsetTop &&
393 tile.offsetLeft < this.offsetLeft) {
394 if (!nextTile || tile.offsetLeft > nextTile.offsetLeft) {
395 nextTile = tile;
400 if (event.keyCode == 38 /* UP */) {
401 for (var i = 0; i < tiles.length; i++) {
402 var tile = tiles[i];
403 if (tile.offsetTop < this.offsetTop &&
404 tile.offsetLeft == this.offsetLeft) {
405 if (!nextTile || tile.offsetTop > nextTile.offsetTop) {
406 nextTile = tile;
411 if (event.keyCode == 39 /* RIGHT */) {
412 for (var i = 0; i < tiles.length; i++) {
413 var tile = tiles[i];
414 if (tile.offsetTop == this.offsetTop &&
415 tile.offsetLeft > this.offsetLeft) {
416 if (!nextTile || tile.offsetLeft < nextTile.offsetLeft) {
417 nextTile = tile;
422 if (event.keyCode == 40 /* DOWN */) {
423 for (var i = 0; i < tiles.length; i++) {
424 var tile = tiles[i];
425 if (tile.offsetTop > this.offsetTop &&
426 tile.offsetLeft == this.offsetLeft) {
427 if (!nextTile || tile.offsetTop < nextTile.offsetTop) {
428 nextTile = tile;
434 if (nextTile) {
435 nextTile.focus();
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');
451 if (USE_ICONS) {
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');
458 loadedCounter += 1;
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);
471 } else {
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
478 // - false: error
479 // - a string: URL that loaded correctly.
480 // This is populated by acceptImage/rejectImage and loadBestImage
481 // decides the best one to load.
482 var results = [];
483 var thumb = tile.querySelector('.mv-thumb');
484 var img = document.createElement('img');
485 var loaded = false;
487 var loadBestImage = function() {
488 if (loaded) {
489 return;
491 for (var i = 0; i < results.length; ++i) {
492 if (results[i] === null) {
493 return;
495 if (results[i] != false) {
496 img.src = results[i];
497 loaded = true;
498 return;
501 thumb.classList.add('failed-img');
502 thumb.removeChild(img);
503 logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
504 countLoad();
507 var acceptImage = function(idx, url) {
508 return function(ev) {
509 results[idx] = url;
510 loadBestImage();
514 var rejectImage = function(idx) {
515 return function(ev) {
516 results[idx] = false;
517 loadBestImage();
521 img.title = data.title;
522 img.classList.add('thumbnail');
523 loadedCounter += 1;
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) {
537 results.push(null);
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);
545 } else {
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.
555 fi.title = '';
556 loadedCounter += 1;
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);
563 } else {
564 favicon.classList.add('failed-favicon');
568 var mvx = tile.querySelector('.mv-x');
569 mvx.addEventListener('click', function(ev) {
570 removeAllOldTiles();
571 blacklistTile(tile);
572 ev.preventDefault();
573 ev.stopPropagation();
576 return tile;
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('&');
589 queryArgs = {};
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);
600 if (isFinite(ntl))
601 NUM_TITLE_LINES = ntl;
604 // Duplicating NTP_DESIGN.mainClass.
605 document.querySelector('#most-visited').classList.add(
606 USE_ICONS ? 'icon-ntp' : 'thumb-ntp');
608 // Enable RTL.
609 if (queryArgs['rtl'] == '1') {
610 var html = document.querySelector('html');
611 html.dir = 'rtl';
614 window.addEventListener('message', handlePostMessage);
618 window.addEventListener('DOMContentLoaded', init);
619 })();