• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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.
4
5cr.define('ntp', function() {
6  'use strict';
7
8  var TilePage = ntp.TilePage;
9
10  /**
11   * A counter for generating unique tile IDs.
12   */
13  var tileID = 0;
14
15  /**
16   * Creates a new Most Visited object for tiling.
17   * @constructor
18   * @extends {HTMLAnchorElement}
19   */
20  function MostVisited() {
21    var el = cr.doc.createElement('a');
22    el.__proto__ = MostVisited.prototype;
23    el.initialize();
24
25    return el;
26  }
27
28  MostVisited.prototype = {
29    __proto__: HTMLAnchorElement.prototype,
30
31    initialize: function() {
32      this.reset();
33
34      this.addEventListener('click', this.handleClick_);
35      this.addEventListener('keydown', this.handleKeyDown_);
36      this.addEventListener('mouseover', this.handleMouseOver_);
37    },
38
39    get index() {
40      assert(this.tile);
41      return this.tile.index;
42    },
43
44    get data() {
45      return this.data_;
46    },
47
48    /**
49     * Clears the DOM hierarchy for this node, setting it back to the default
50     * for a blank thumbnail.
51     */
52    reset: function() {
53      this.className = 'most-visited filler real';
54      this.innerHTML =
55          '<span class="thumbnail-wrapper fills-parent">' +
56            '<div class="close-button"></div>' +
57            '<span class="thumbnail fills-parent">' +
58              // thumbnail-shield provides a gradient fade effect.
59              '<div class="thumbnail-shield fills-parent"></div>' +
60            '</span>' +
61            '<span class="favicon"></span>' +
62          '</span>' +
63          '<div class="color-stripe"></div>' +
64          '<span class="title"></span>';
65
66      this.querySelector('.close-button').title =
67          loadTimeData.getString('removethumbnailtooltip');
68
69      this.tabIndex = -1;
70      this.data_ = null;
71      this.removeAttribute('id');
72      this.title = '';
73    },
74
75    /**
76     * Update the appearance of this tile according to |data|.
77     * @param {Object} data A dictionary of relevant data for the page.
78     */
79    updateForData: function(data) {
80      if (this.classList.contains('blacklisted') && data) {
81        // Animate appearance of new tile.
82        this.classList.add('new-tile-contents');
83      }
84      this.classList.remove('blacklisted');
85
86      if (!data || data.filler) {
87        if (this.data_)
88          this.reset();
89        return;
90      }
91
92      var id = tileID++;
93      this.id = 'most-visited-tile-' + id;
94      this.data_ = data;
95      this.classList.add('focusable');
96
97      var faviconDiv = this.querySelector('.favicon');
98      faviconDiv.style.backgroundImage = getFaviconImageSet(data.url);
99
100      // The favicon should have the same dominant color regardless of the
101      // device pixel ratio the favicon is requested for.
102      chrome.send('getFaviconDominantColor',
103                  [getFaviconUrlForCurrentDevicePixelRatio(data.url), this.id]);
104
105      var title = this.querySelector('.title');
106      title.textContent = data.title;
107      title.dir = data.direction;
108
109      // Sets the tooltip.
110      this.title = data.title;
111
112      var thumbnailUrl = 'chrome://thumb/' + data.url;
113      this.querySelector('.thumbnail').style.backgroundImage =
114          url(thumbnailUrl);
115
116      this.href = data.url;
117
118      this.classList.remove('filler');
119    },
120
121    /**
122     * Sets the color of the favicon dominant color bar.
123     * @param {string} color The css-parsable value for the color.
124     */
125    set stripeColor(color) {
126      this.querySelector('.color-stripe').style.backgroundColor = color;
127    },
128
129    /**
130     * Handles a click on the tile.
131     * @param {Event} e The click event.
132     */
133    handleClick_: function(e) {
134      if (e.target.classList.contains('close-button')) {
135        this.blacklist_();
136        e.preventDefault();
137      } else {
138        ntp.logTimeToClick('MostVisited');
139        // Records an app launch from the most visited page (Chrome will decide
140        // whether the url is an app). TODO(estade): this only works for clicks;
141        // other actions like "open in new tab" from the context menu won't be
142        // recorded. Can this be fixed?
143        chrome.send('recordAppLaunchByURL',
144                    [encodeURIComponent(this.href),
145                     ntp.APP_LAUNCH.NTP_MOST_VISITED]);
146        // Records the index of this tile.
147        chrome.send('metricsHandler:recordInHistogram',
148                    ['NewTabPage.MostVisited', this.index, 8]);
149        // Records the action. This will be available as a time-stamped stream
150        // server-side and can be used to compute time-to-long-dwell.
151        chrome.send('metricsHandler:recordAction', ['MostVisited_Clicked']);
152        chrome.send('mostVisitedAction',
153                    [ntp.NtpFollowAction.CLICKED_TILE]);
154      }
155    },
156
157    /**
158     * Allow blacklisting most visited site using the keyboard.
159     */
160    handleKeyDown_: function(e) {
161      if (!cr.isMac && e.keyCode == 46 || // Del
162          cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
163        this.blacklist_();
164      }
165    },
166
167    /**
168     * The mouse has entered a Most Visited tile div. Only log the first
169     * mouseover event. By doing this we solve the issue with the mouseover
170     * event listener that bubbles up to the parent, which would cause it to
171     * fire multiple times even if the mouse stays within one tile.
172     */
173    handleMouseOver_: function(e) {
174      var self = this;
175      var ancestor = findAncestor(e.relatedTarget, function(node) {
176        return node == self;
177      });
178      // If ancestor is null, mouse is entering the parent element.
179      if (ancestor == null)
180        chrome.send('metricsHandler:logMouseover');
181    },
182
183    /**
184     * Permanently removes a page from Most Visited.
185     */
186    blacklist_: function() {
187      this.showUndoNotification_();
188      chrome.send('blacklistURLFromMostVisited', [this.data_.url]);
189      this.reset();
190      chrome.send('getMostVisited');
191      this.classList.add('blacklisted');
192    },
193
194    showUndoNotification_: function() {
195      var data = this.data_;
196      var self = this;
197      var doUndo = function() {
198        chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]);
199        self.updateForData(data);
200      }
201
202      var undo = {
203        action: doUndo,
204        text: loadTimeData.getString('undothumbnailremove'),
205      };
206
207      var undoAll = {
208        action: function() {
209          chrome.send('clearMostVisitedURLsBlacklist');
210        },
211        text: loadTimeData.getString('restoreThumbnailsShort'),
212      };
213
214      ntp.showNotification(
215          loadTimeData.getString('thumbnailremovednotification'),
216          [undo, undoAll]);
217    },
218
219    /**
220     * Set the size and position of the most visited tile.
221     * @param {number} size The total size of |this|.
222     * @param {number} x The x-position.
223     * @param {number} y The y-position.
224     *     animate.
225     */
226    setBounds: function(size, x, y) {
227      this.style.width = toCssPx(size);
228      this.style.height = toCssPx(heightForWidth(size));
229
230      this.style.left = toCssPx(x);
231      this.style.right = toCssPx(x);
232      this.style.top = toCssPx(y);
233    },
234
235    /**
236     * Returns whether this element can be 'removed' from chrome (i.e. whether
237     * the user can drag it onto the trash and expect something to happen).
238     * @return {boolean} True, since most visited pages can always be
239     *     blacklisted.
240     */
241    canBeRemoved: function() {
242      return true;
243    },
244
245    /**
246     * Removes this element from chrome, i.e. blacklists it.
247     */
248    removeFromChrome: function() {
249      this.blacklist_();
250      this.parentNode.classList.add('finishing-drag');
251    },
252
253    /**
254     * Called when a drag of this tile has ended (after all animations have
255     * finished).
256     */
257    finalizeDrag: function() {
258      this.parentNode.classList.remove('finishing-drag');
259    },
260
261    /**
262     * Called when a drag is starting on the tile. Updates dataTransfer with
263     * data for this tile (for dragging outside of the NTP).
264     */
265    setDragData: function(dataTransfer) {
266      dataTransfer.setData('Text', this.data_.title);
267      dataTransfer.setData('URL', this.data_.url);
268    },
269  };
270
271  var mostVisitedPageGridValues = {
272    // The fewest tiles we will show in a row.
273    minColCount: 2,
274    // The most tiles we will show in a row.
275    maxColCount: 4,
276
277    // The smallest a tile can be.
278    minTileWidth: 122,
279    // The biggest a tile can be. 212 (max thumbnail width) + 2.
280    maxTileWidth: 214,
281
282    // The padding between tiles, as a fraction of the tile width.
283    tileSpacingFraction: 1 / 8,
284  };
285  TilePage.initGridValues(mostVisitedPageGridValues);
286
287  /**
288   * Calculates the height for a Most Visited tile for a given width. The size
289   * is based on the thumbnail, which should have a 212:132 ratio.
290   * @return {number} The height.
291   */
292  function heightForWidth(width) {
293    // The 2s are for borders, the 31 is for the title.
294    return (width - 2) * 132 / 212 + 2 + 31;
295  }
296
297  var THUMBNAIL_COUNT = 8;
298
299  /**
300   * Creates a new MostVisitedPage object.
301   * @constructor
302   * @extends {TilePage}
303   */
304  function MostVisitedPage() {
305    var el = new TilePage(mostVisitedPageGridValues);
306    el.__proto__ = MostVisitedPage.prototype;
307    el.initialize();
308
309    return el;
310  }
311
312  MostVisitedPage.prototype = {
313    __proto__: TilePage.prototype,
314
315    initialize: function() {
316      this.classList.add('most-visited-page');
317      this.data_ = null;
318      this.mostVisitedTiles_ = this.getElementsByClassName('most-visited real');
319
320      this.addEventListener('carddeselected', this.handleCardDeselected_);
321      this.addEventListener('cardselected', this.handleCardSelected_);
322    },
323
324    /**
325     * Create blank (filler) tiles.
326     * @private
327     */
328    createTiles_: function() {
329      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
330        this.appendTile(new MostVisited());
331      }
332    },
333
334    /**
335     * Update the tiles after a change to |data_|.
336     */
337    updateTiles_: function() {
338      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
339        var page = this.data_[i];
340        var tile = this.mostVisitedTiles_[i];
341
342        if (i >= this.data_.length)
343          tile.reset();
344        else
345          tile.updateForData(page);
346      }
347    },
348
349    /**
350     * Handles the 'card deselected' event (i.e. the user clicked to another
351     * pane).
352     * @param {Event} e The CardChanged event.
353     */
354    handleCardDeselected_: function(e) {
355      if (!document.documentElement.classList.contains('starting-up')) {
356        chrome.send('mostVisitedAction',
357                    [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]);
358      }
359    },
360
361    /**
362     * Handles the 'card selected' event (i.e. the user clicked to select the
363     * Most Visited pane).
364     * @param {Event} e The CardChanged event.
365     */
366    handleCardSelected_: function(e) {
367      if (!document.documentElement.classList.contains('starting-up'))
368        chrome.send('mostVisitedSelected');
369    },
370
371    /**
372     * Array of most visited data objects.
373     * @type {Array}
374     */
375    get data() {
376      return this.data_;
377    },
378    set data(data) {
379      var startTime = Date.now();
380
381      // The first time data is set, create the tiles.
382      if (!this.data_) {
383        this.createTiles_();
384        this.data_ = data.slice(0, THUMBNAIL_COUNT);
385      } else {
386        this.data_ = refreshData(this.data_, data);
387      }
388
389      this.updateTiles_();
390      this.updateFocusableElement();
391      logEvent('mostVisited.layout: ' + (Date.now() - startTime));
392    },
393
394    /** @override */
395    shouldAcceptDrag: function(e) {
396      return false;
397    },
398
399    /** @override */
400    heightForWidth: heightForWidth,
401  };
402
403  /**
404   * Executed once the NTP has loaded. Checks if the Most Visited pane is
405   * shown or not. If it is shown, the 'mostVisitedSelected' message is sent
406   * to the C++ code, to record the fact that the user has seen this pane.
407   */
408  MostVisitedPage.onLoaded = function() {
409    if (ntp.getCardSlider() &&
410        ntp.getCardSlider().currentCardValue &&
411        ntp.getCardSlider().currentCardValue.classList
412        .contains('most-visited-page')) {
413      chrome.send('mostVisitedSelected');
414    }
415  }
416
417  /**
418   * We've gotten additional Most Visited data. Update our old data with the
419   * new data. The ordering of the new data is not important, except when a
420   * page is pinned. Thus we try to minimize re-ordering.
421   * @param {Array} oldData The current Most Visited page list.
422   * @param {Array} newData The new Most Visited page list.
423   * @return {Array} The merged page list that should replace the current page
424   *     list.
425   */
426  function refreshData(oldData, newData) {
427    oldData = oldData.slice(0, THUMBNAIL_COUNT);
428    newData = newData.slice(0, THUMBNAIL_COUNT);
429
430    // Copy over pinned sites directly.
431    for (var j = 0; j < newData.length; j++) {
432      if (newData[j].pinned) {
433        oldData[j] = newData[j];
434        // Mark the entry as 'updated' so we don't try to update again.
435        oldData[j].updated = true;
436        // Mark the newData page as 'used' so we don't try to re-use it.
437        newData[j].used = true;
438      }
439    }
440
441    // Look through old pages; if they exist in the newData list, keep them
442    // where they are.
443    for (var i = 0; i < oldData.length; i++) {
444      if (!oldData[i] || oldData[i].updated)
445        continue;
446
447      for (var j = 0; j < newData.length; j++) {
448        if (newData[j].used)
449          continue;
450
451        if (newData[j].url == oldData[i].url) {
452          // The background image and other data may have changed.
453          oldData[i] = newData[j];
454          oldData[i].updated = true;
455          newData[j].used = true;
456          break;
457        }
458      }
459    }
460
461    // Look through old pages that haven't been updated yet; replace them.
462    for (var i = 0; i < oldData.length; i++) {
463      if (oldData[i] && oldData[i].updated)
464        continue;
465
466      for (var j = 0; j < newData.length; j++) {
467        if (newData[j].used)
468          continue;
469
470        oldData[i] = newData[j];
471        oldData[i].updated = true;
472        newData[j].used = true;
473        break;
474      }
475
476      if (oldData[i] && !oldData[i].updated)
477        oldData[i] = null;
478    }
479
480    // Clear 'updated' flags so this function will work next time it's called.
481    for (var i = 0; i < THUMBNAIL_COUNT; i++) {
482      if (oldData[i])
483        oldData[i].updated = false;
484    }
485
486    return oldData;
487  };
488
489  return {
490    MostVisitedPage: MostVisitedPage,
491    refreshData: refreshData,
492  };
493});
494
495document.addEventListener('ntpLoaded', ntp.MostVisitedPage.onLoaded);
496