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