summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/local_ntp/most_visited_single.js
blob: 98ebea550ab65ebc90f8f3670e9a4dc651119aa1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
/* Copyright 2015 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file. */

 // Single iframe for NTP tiles.
(function() {
'use strict';


/**
 * The different types of events that are logged from the NTP.  This enum is
 * used to transfer information from the NTP JavaScript to the renderer and is
 * not used as a UMA enum histogram's logged value.
 * Note: Keep in sync with common/ntp_logging_events.h
 * @enum {number}
 * @const
 */
var LOG_TYPE = {
  // The suggestion is coming from the server. Unused here.
  NTP_SERVER_SIDE_SUGGESTION: 0,
  // The suggestion is coming from the client.
  NTP_CLIENT_SIDE_SUGGESTION: 1,
  // Indicates a tile was rendered, no matter if it's a thumbnail, a gray tile
  // or an external tile.
  NTP_TILE: 2,
  // The tile uses a local thumbnail image.
  NTP_THUMBNAIL_TILE: 3,
  // Used when no thumbnail is specified and a gray tile with the domain is used
  // as the main tile. Unused here.
  NTP_GRAY_TILE: 4,
  // The visuals of that tile are handled externally by the page itself.
  // Unused here.
  NTP_EXTERNAL_TILE: 5,
  // There was an error in loading both the thumbnail image and the fallback
  // (if it was provided), resulting in a gray tile.
  NTP_THUMBNAIL_ERROR: 6,
  // Used a gray tile with the domain as the fallback for a failed thumbnail.
  // Unused here.
  NTP_GRAY_TILE_FALLBACK: 7,
  // The visuals of that tile's fallback are handled externally. Unused here.
  NTP_EXTERNAL_TILE_FALLBACK: 8,
  // The user moused over an NTP tile.
  NTP_MOUSEOVER: 9,
  // A NTP Tile has finished loading (successfully or failing).
  NTP_TILE_LOADED: 10,
};


/**
 * Total number of tiles to show at any time. If the host page doesn't send
 * enough tiles, we fill them blank.
 * @const {number}
 */
var NUMBER_OF_TILES = 8;


/**
 * Whether to use icons instead of thumbnails.
 * @type {boolean}
 */
var USE_ICONS = false;


/**
 * Number of lines to display in titles.
 * @type {number}
 */
var NUM_TITLE_LINES = 1;


/**
 * The origin of this request.
 * @const {string}
 */
var DOMAIN_ORIGIN = '{{ORIGIN}}';


/**
 * Counter for DOM elements that we are waiting to finish loading.
 * @type {number}
 */
var loadedCounter = 1;


/**
 * DOM element containing the tiles we are going to present next.
 * Works as a double-buffer that is shown when we receive a "show" postMessage.
 * @type {Element}
 */
var tiles = null;


/**
 * List of parameters passed by query args.
 * @type {Object}
 */
var queryArgs = {};

/**
 * Url to ping when suggestions have been shown.
 */
var impressionUrl = null;


/**
 * Log an event on the NTP.
 * @param {number} eventType Event from LOG_TYPE.
 */
var logEvent = function(eventType) {
  chrome.embeddedSearch.newTabPage.logEvent(eventType);
};


/**
 * Down counts the DOM elements that we are waiting for the page to load.
 * When we get to 0, we send a message to the parent window.
 * This is usually used as an EventListener of onload/onerror.
 */
var countLoad = function() {
  loadedCounter -= 1;
  if (loadedCounter <= 0) {
    showTiles();
    logEvent(LOG_TYPE.NTP_TILE_LOADED);
    window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN);
    loadedCounter = 1;
  }
};


/**
 * Handles postMessages coming from the host page to the iframe.
 * Mostly, it dispatches every command to handleCommand.
 */
var handlePostMessage = function(event) {
  if (event.data instanceof Array) {
    for (var i = 0; i < event.data.length; ++i) {
      handleCommand(event.data[i]);
    }
  } else {
    handleCommand(event.data);
  }
};


/**
 * Handles a single command coming from the host page to the iframe.
 * We try to keep the logic here to a minimum and just dispatch to the relevant
 * functions.
 */
var handleCommand = function(data) {
  var cmd = data.cmd;

  if (cmd == 'tile') {
    addTile(data);
  } else if (cmd == 'show') {
    countLoad();
    hideOverflowTiles(data);
  } else if (cmd == 'updateTheme') {
    updateTheme(data);
  } else if (cmd == 'tilesVisible') {
    hideOverflowTiles(data);
  } else {
    console.error('Unknown command: ' + JSON.stringify(data));
  }
};


var updateTheme = function(info) {
  var themeStyle = [];

  if (info.tileBorderColor) {
    themeStyle.push('.thumb-ntp .mv-tile {' +
        'border: 1px solid ' + info.tileBorderColor + '; }');
  }
  if (info.tileHoverBorderColor) {
    themeStyle.push('.thumb-ntp .mv-tile:hover {' +
        'border-color: ' + info.tileHoverBorderColor + '; }');
  }
  if (info.isThemeDark) {
    themeStyle.push('.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' +
        'background: rgb(51,51,51); }');
    themeStyle.push('.thumb-ntp .mv-thumb.failed-img { ' +
        'background-color: #555; }');
    themeStyle.push('.thumb-ntp .mv-thumb.failed-img::after { ' +
        'border-color: #333; }');
    themeStyle.push('.thumb-ntp .mv-x { ' +
        'background: linear-gradient(to left, ' +
        'rgb(51,51,51) 60%, transparent); }');
    themeStyle.push('html[dir=rtl] .thumb-ntp .mv-x { ' +
        'background: linear-gradient(to right, ' +
        'rgb(51,51,51) 60%, transparent); }');
    themeStyle.push('.thumb-ntp .mv-x::after { ' +
        'background-color: rgba(255,255,255,0.7); }');
    themeStyle.push('.thumb-ntp .mv-x:hover::after { ' +
        'background-color: #fff; }');
    themeStyle.push('.thumb-ntp .mv-x:active::after { ' +
        'background-color: rgba(255,255,255,0.5); }');
    themeStyle.push('.icon-ntp .mv-tile:focus { ' +
        'background: rgba(255,255,255,0.2); }');
  }
  if (info.tileTitleColor) {
    themeStyle.push('body { color: ' + info.tileTitleColor + '; }');
  }

  document.querySelector('#custom-theme').textContent = themeStyle.join('\n');
};


/**
 * Hides extra tiles that don't fit on screen.
 */
var hideOverflowTiles = function(data) {
  var tileAndEmptyTileList = document.querySelectorAll(
      '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile');
  for (var i = 0; i < tileAndEmptyTileList.length; ++i) {
    tileAndEmptyTileList[i].classList.toggle('hidden', i >= data.maxVisible);
  }
};


/**
 * Removes all old instances of #mv-tiles that are pending for deletion.
 */
var removeAllOldTiles = function() {
  var parent = document.querySelector('#most-visited');
  var oldList = parent.querySelectorAll('.mv-tiles-old');
  for (var i = 0; i < oldList.length; ++i) {
    parent.removeChild(oldList[i]);
  }
};


/**
 * Called when the host page has finished sending us tile information and
 * we are ready to show the new tiles and drop the old ones.
 */
var showTiles = function() {
  // Store the tiles on the current closure.
  var cur = tiles;

  // Create empty tiles until we have NUMBER_OF_TILES.
  while (cur.childNodes.length < NUMBER_OF_TILES) {
    addTile({});
  }

  var parent = document.querySelector('#most-visited');

  // Mark old tile DIV for removal after the transition animation is done.
  var old = parent.querySelector('#mv-tiles');
  if (old) {
    old.removeAttribute('id');
    old.classList.add('mv-tiles-old');
    old.style.opacity = 0.0;
    cur.addEventListener('webkitTransitionEnd', function(ev) {
      if (ev.target === cur) {
        removeAllOldTiles();
      }
    });
  }

  // Add new tileset.
  cur.id = 'mv-tiles';
  parent.appendChild(cur);
  // We want the CSS transition to trigger, so need to add to the DOM before
  // setting the style.
  setTimeout(function() {
    cur.style.opacity = 1.0;
  }, 0);

  // Make sure the tiles variable contain the next tileset we may use.
  tiles = document.createElement('div');

  if (impressionUrl) {
    if (navigator.sendBeacon) {
      navigator.sendBeacon(impressionUrl);
    } else {
      // if sendBeacon is not enabled, we fallback to "a ping".
      var a = document.createElement('a');
      a.href = '#';
      a.ping = impressionUrl;
      a.click();
    }
    impressionUrl = null;
  }
};


/**
 * Called when the host page wants to add a suggestion tile.
 * For Most Visited, it grabs the data from Chrome and pass on.
 * For host page generated it just passes the data.
 * @param {object} args Data for the tile to be rendered.
 */
var addTile = function(args) {
  if (args.rid) {
    var data = chrome.embeddedSearch.searchBox.getMostVisitedItemData(args.rid);
    data.tid = data.rid;
    if (!data.faviconUrl) {
      data.faviconUrl = 'chrome-search://favicon/size/16@' +
          window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid;
    }
    tiles.appendChild(renderTile(data));
  } else if (args.id) {
    tiles.appendChild(renderTile(args));
  } else {
    tiles.appendChild(renderTile(null));
  }
};


/**
 * Called when the user decided to add a tile to the blacklist.
 * It sets of the animation for the blacklist and sends the blacklisted id
 * to the host page.
 * @param {Element} tile DOM node of the tile we want to remove.
 */
var blacklistTile = function(tile) {
  tile.classList.add('blacklisted');
  tile.addEventListener('webkitTransitionEnd', function(ev) {
    if (ev.propertyName != 'width') return;

    window.parent.postMessage({cmd: 'tileBlacklisted',
                               tid: Number(tile.getAttribute('data-tid'))},
                              DOMAIN_ORIGIN);
  });
};


/**
 * Renders a MostVisited tile to the DOM.
 * @param {object} data Object containing rid, url, title, favicon, thumbnail.
 *     data is null if you want to construct an empty tile.
 */
var renderTile = function(data) {
  var tile = document.createElement('a');

  if (data == null) {
    tile.className = 'mv-empty-tile';
    return tile;
  }

  logEvent(LOG_TYPE.NTP_TILE);

  tile.className = 'mv-tile';
  tile.setAttribute('data-tid', data.tid);
  var tooltip = queryArgs['removeTooltip'] || '';
  var html = [];
  if (!USE_ICONS) {
    html.push('<div class="mv-favicon"></div>');
  }
  html.push('<div class="mv-title"></div><div class="mv-thumb"></div>');
  html.push('<div title="' + tooltip + '" class="mv-x"></div>');
  tile.innerHTML = html.join('');

  tile.href = data.url;
  tile.title = data.title;
  if (data.impressionUrl) {
    impressionUrl = data.impressionUrl;
  }
  if (data.pingUrl) {
    tile.addEventListener('click', function(ev) {
      if (navigator.sendBeacon) {
        navigator.sendBeacon(data.pingUrl);
      } else {
        // if sendBeacon is not enabled, we fallback to "a ping".
        var a = document.createElement('a');
        a.href = '#';
        a.ping = data.pingUrl;
        a.click();
      }
    });
  }
  // For local suggestions, we use navigateContentWindow instead of the default
  // action, since it includes support for file:// urls.
  if (data.rid) {
    tile.addEventListener('click', function(ev) {
      ev.preventDefault();
      var disp = chrome.embeddedSearch.newTabPage.getDispositionFromClick(
        ev.button == 1,  // MIDDLE BUTTON
        ev.altKey, ev.ctrlKey, ev.metaKey, ev.shiftKey);

      window.chrome.embeddedSearch.newTabPage.navigateContentWindow(this.href,
                                                                    disp);
    });
  }

  tile.addEventListener('keydown', function(event) {
    if (event.keyCode == 46 /* DELETE */ ||
        event.keyCode == 8 /* BACKSPACE */) {
      event.preventDefault();
      event.stopPropagation();
      blacklistTile(this);
    } else if (event.keyCode == 13 /* ENTER */ ||
               event.keyCode == 32 /* SPACE */) {
      event.preventDefault();
      this.click();
    } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) {
      var tiles = document.querySelectorAll('#mv-tiles .mv-tile');
      var nextTile = null;
      // Use the location of the tile to find the next one in the
      // appropriate direction.
      // For LEFT and UP we keep iterating until we find the last element
      // that fulfills the conditions.
      // For RIGHT and DOWN we accept the first element that works.
      if (event.keyCode == 37 /* LEFT */) {
        for (var i = 0; i < tiles.length; i++) {
          var tile = tiles[i];
          if (tile.offsetTop == this.offsetTop &&
              tile.offsetLeft < this.offsetLeft) {
            if (!nextTile || tile.offsetLeft > nextTile.offsetLeft) {
              nextTile = tile;
            }
          }
        }
      }
      if (event.keyCode == 38 /* UP */) {
        for (var i = 0; i < tiles.length; i++) {
          var tile = tiles[i];
          if (tile.offsetTop < this.offsetTop &&
              tile.offsetLeft == this.offsetLeft) {
            if (!nextTile || tile.offsetTop > nextTile.offsetTop) {
              nextTile = tile;
            }
          }
        }
      }
      if (event.keyCode == 39 /* RIGHT */) {
        for (var i = 0; i < tiles.length; i++) {
          var tile = tiles[i];
          if (tile.offsetTop == this.offsetTop &&
              tile.offsetLeft > this.offsetLeft) {
            if (!nextTile || tile.offsetLeft < nextTile.offsetLeft) {
              nextTile = tile;
            }
          }
        }
      }
      if (event.keyCode == 40 /* DOWN */) {
        for (var i = 0; i < tiles.length; i++) {
          var tile = tiles[i];
          if (tile.offsetTop > this.offsetTop &&
              tile.offsetLeft == this.offsetLeft) {
            if (!nextTile || tile.offsetTop < nextTile.offsetTop) {
              nextTile = tile;
            }
          }
        }
      }

      if (nextTile) {
        nextTile.focus();
      }
    }
  });
  // TODO(fserb): remove this or at least change to mouseenter.
  tile.addEventListener('mouseover', function() {
    logEvent(LOG_TYPE.NTP_MOUSEOVER);
  });

  var title = tile.querySelector('.mv-title');
  title.innerText = data.title;
  title.style.direction = data.direction || 'ltr';
  if (NUM_TITLE_LINES > 1) {
    title.classList.add('multiline');
  }

  if (USE_ICONS) {
    var thumb = tile.querySelector('.mv-thumb');
    if (data.largeIconUrl) {
      var img = document.createElement('img');
      img.title = data.title;
      img.src = data.largeIconUrl;
      img.classList.add('large-icon');
      loadedCounter += 1;
      img.addEventListener('load', countLoad);
      img.addEventListener('load', function(ev) {
        thumb.classList.add('large-icon-outer');
      });
      img.addEventListener('error', countLoad);
      img.addEventListener('error', function(ev) {
        thumb.classList.add('failed-img');
        thumb.removeChild(img);
        logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
      });
      thumb.appendChild(img);
      logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);
    } else {
      thumb.classList.add('failed-img');
    }
  } else { // THUMBNAILS
    // We keep track of the outcome of loading possible thumbnails for this
    // tile. Possible values:
    //   - null: waiting for load/error
    //   - false: error
    //   - a string: URL that loaded correctly.
    // This is populated by acceptImage/rejectImage and loadBestImage
    // decides the best one to load.
    var results = [];
    var thumb = tile.querySelector('.mv-thumb');
    var img = document.createElement('img');
    var loaded = false;

    var loadBestImage = function() {
      if (loaded) {
        return;
      }
      for (var i = 0; i < results.length; ++i) {
        if (results[i] === null) {
          return;
        }
        if (results[i] != false) {
          img.src = results[i];
          loaded = true;
          return;
        }
      }
      thumb.classList.add('failed-img');
      thumb.removeChild(img);
      logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
      countLoad();
    };

    var acceptImage = function(idx, url) {
      return function(ev) {
        results[idx] = url;
        loadBestImage();
      };
    };

    var rejectImage = function(idx) {
      return function(ev) {
        results[idx] = false;
        loadBestImage();
      };
    };

    img.title = data.title;
    img.classList.add('thumbnail');
    loadedCounter += 1;
    img.addEventListener('load', countLoad);
    img.addEventListener('error', countLoad);
    img.addEventListener('error', function(ev) {
      thumb.classList.add('failed-img');
      thumb.removeChild(img);
      logEvent(LOG_TYPE.NTP_THUMBNAIL_ERROR);
    });
    thumb.appendChild(img);
    logEvent(LOG_TYPE.NTP_THUMBNAIL_TILE);

    if (data.thumbnailUrl) {
      img.src = data.thumbnailUrl;
    } else {
      // Get all thumbnailUrls for the tile.
      // They are ordered from best one to be used to worst.
      for (var i = 0; i < data.thumbnailUrls.length; ++i) {
        results.push(null);
      }
      for (var i = 0; i < data.thumbnailUrls.length; ++i) {
        if (data.thumbnailUrls[i]) {
          var image = new Image();
          image.src = data.thumbnailUrls[i];
          image.onload = acceptImage(i, data.thumbnailUrls[i]);
          image.onerror = rejectImage(i);
        } else {
          rejectImage(i)(null);
        }
      }
    }

    var favicon = tile.querySelector('.mv-favicon');
    if (data.faviconUrl) {
      var fi = document.createElement('img');
      fi.src = data.faviconUrl;
      // Set the title to empty so screen readers won't say the image name.
      fi.title = '';
      loadedCounter += 1;
      fi.addEventListener('load', countLoad);
      fi.addEventListener('error', countLoad);
      fi.addEventListener('error', function(ev) {
        favicon.classList.add('failed-favicon');
      });
      favicon.appendChild(fi);
    } else {
      favicon.classList.add('failed-favicon');
    }
  }

  var mvx = tile.querySelector('.mv-x');
  mvx.addEventListener('click', function(ev) {
    removeAllOldTiles();
    blacklistTile(tile);
    ev.preventDefault();
    ev.stopPropagation();
  });

  return tile;
};


/**
 * Do some initialization and parses the query arguments passed to the iframe.
 */
var init = function() {
  // Creates a new DOM element to hold the tiles.
  tiles = document.createElement('div');

  // Parse query arguments.
  var query = window.location.search.substring(1).split('&');
  queryArgs = {};
  for (var i = 0; i < query.length; ++i) {
    var val = query[i].split('=');
    if (val[0] == '') continue;
    queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]);
  }

  // Apply class for icon NTP, if specified.
  USE_ICONS = queryArgs['icons'] == '1';
  if ('ntl' in queryArgs) {
    var ntl = parseInt(queryArgs['ntl'], 10);
    if (isFinite(ntl))
      NUM_TITLE_LINES = ntl;
  }

  // Duplicating NTP_DESIGN.mainClass.
  document.querySelector('#most-visited').classList.add(
      USE_ICONS ? 'icon-ntp' : 'thumb-ntp');

  // Enable RTL.
  if (queryArgs['rtl'] == '1') {
    var html = document.querySelector('html');
    html.dir = 'rtl';
  }

  window.addEventListener('message', handlePostMessage);
};


window.addEventListener('DOMContentLoaded', init);
})();