1// Copyright (c) 2011 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('ntp4', function() {
6  'use strict';
7
8  var TilePage = ntp4.TilePage;
9
10  /**
11   * Creates a new Most Visited object for tiling.
12   * @constructor
13   * @extends {HTMLAnchorElement}
14   */
15  function MostVisited() {
16    var el = cr.doc.createElement('a');
17    el.__proto__ = MostVisited.prototype;
18    el.initialize();
19
20    return el;
21  }
22
23  MostVisited.prototype = {
24    __proto__: HTMLAnchorElement.prototype,
25
26    initialize: function() {
27      this.reset();
28
29      this.addEventListener('click', this.handleClick_.bind(this));
30      this.addEventListener('keydown', this.handleKeyDown_.bind(this));
31    },
32
33    /**
34     * Clears the DOM hierarchy for this node, setting it back to the default
35     * for a blank thumbnail.
36     */
37    reset: function() {
38      this.className = 'most-visited filler';
39      // TODO(estade): why do we need edit-mode-border?
40      this.innerHTML =
41          '<div class="edit-mode-border fills-parent">' +
42            '<div class="edit-bar-wrapper">' +
43              '<div class="edit-bar">' +
44                '<div class="pin"></div>' +
45                '<div class="spacer"></div>' +
46                '<div class="remove"></div>' +
47              '</div>' +
48            '</div>' +
49            '<span class="thumbnail-wrapper fills-parent">' +
50              '<span class="thumbnail fills-parent">' +
51                // thumbnail-shield provides a gradient fade effect.
52                '<div class="thumbnail-shield fills-parent"></div>' +
53              '</span>' +
54              '<span class="title"></span>' +
55            '</span>' +
56          '</div>';
57
58      this.tabIndex = -1;
59    },
60
61    /**
62     * Update the appearance of this tile according to |data|.
63     * @param {Object} data A dictionary of relevant data for the page.
64     */
65    updateForData: function(data) {
66      if (data.filler) {
67        this.reset();
68        return;
69      }
70
71      this.data_ = data;
72      this.tabIndex = 0;
73      this.classList.remove('filler');
74
75      var title = this.querySelector('.title');
76      title.textContent = data.title;
77      var faviconUrl = data.faviconUrl || 'chrome://favicon/' + data.url;
78      title.style.backgroundImage = url(faviconUrl);
79      title.dir = data.direction;
80
81      var thumbnailUrl = data.thumbnailUrl || 'chrome://thumb/' + data.url;
82      this.querySelector('.thumbnail').style.backgroundImage =
83          url(thumbnailUrl);
84
85      this.href = data.url;
86
87      this.updatePinnedState_();
88    },
89
90    /**
91     * Handles a click on the tile.
92     * @param {Event} e The click event.
93     */
94    handleClick_: function(e) {
95      var target = e.target;
96      if (target.classList.contains('pin')) {
97        this.togglePinned_();
98        e.preventDefault();
99      } else if (target.classList.contains('remove')) {
100        this.blacklist_();
101        e.preventDefault();
102      } else {
103        var index = Array.prototype.indexOf.call(this.parentNode.children,
104                                                 this);
105        if (index != -1)
106          chrome.send('metrics', ['NTP_MostVisited' + index]);
107      }
108    },
109
110    /**
111     * Allow blacklisting most visited site using the keyboard.
112     */
113    handleKeyDown_: function(e) {
114      if (!IS_MAC && e.keyCode == 46 || // Del
115          IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
116        this.blacklist_();
117      }
118    },
119
120    /**
121     * Changes the visual state of the page and updates the model.
122     */
123    togglePinned_: function() {
124      var data = this.data_;
125      data.pinned = !data.pinned;
126      if (data.pinned) {
127        chrome.send('addPinnedURL', [
128          data.url,
129          data.title,
130          data.faviconUrl || '',
131          data.thumbnailUrl || '',
132          // TODO(estade): should not need to convert index to string.
133          String(data.index)
134        ]);
135      } else {
136        chrome.send('removePinnedURL', [data.url]);
137      }
138
139      this.updatePinnedState_();
140    },
141
142    /**
143     * Updates the DOM for the current pinned state.
144     */
145    updatePinnedState_: function() {
146      if (this.data_.pinned) {
147        this.classList.add('pinned');
148        this.querySelector('.pin').title = templateData.unpinthumbnailtooltip;
149      } else {
150        this.classList.remove('pinned');
151        this.querySelector('.pin').title = templateData.pinthumbnailtooltip;
152      }
153    },
154
155    /**
156     * Permanently removes a page from Most Visited.
157     */
158    blacklist_: function() {
159      chrome.send('blacklistURLFromMostVisited', [url]);
160      this.reset();
161      // TODO(estade): request a replacement site.
162    },
163
164    /**
165     * Set the size and position of the most visited tile.
166     * @param {number} size The total size of |this|.
167     * @param {number} x The x-position.
168     * @param {number} y The y-position.
169     *     animate.
170     */
171    setBounds: function(size, x, y) {
172      this.style.width = size + 'px';
173      this.style.height = heightForWidth(size) + 'px';
174      this.style.left = x + 'px';
175      this.style.top = y + 'px';
176    },
177  };
178
179  var mostVisitedPageGridValues = {
180    // The fewest tiles we will show in a row.
181    minColCount: 2,
182    // The most tiles we will show in a row.
183    maxColCount: 4,
184
185    // TODO(estade): Change these to real values.
186    // The smallest a tile can be.
187    minTileWidth: 200,
188    // The biggest a tile can be.
189    maxTileWidth: 240,
190  };
191  TilePage.initGridValues(mostVisitedPageGridValues);
192
193  /**
194   * Calculates the height for a Most Visited tile for a given width. The size
195   * is based on the thumbnail, which should have a 212:132 ratio (the rest of
196   * the arithmetic accounts for padding).
197   * @return {number} The height.
198   */
199  function heightForWidth(width) {
200    return (width - 6) * 132 / 212 + 29;
201  }
202
203  var THUMBNAIL_COUNT = 8;
204
205  /**
206   * Creates a new MostVisitedPage object.
207   * @param {string} name The display name for the page.
208   * @constructor
209   * @extends {TilePage}
210   */
211  function MostVisitedPage(name) {
212    var el = new TilePage(name, mostVisitedPageGridValues);
213    el.__proto__ = MostVisitedPage.prototype;
214    el.initialize();
215
216    return el;
217  }
218
219  MostVisitedPage.prototype = {
220    __proto__: TilePage.prototype,
221
222    initialize: function() {
223      this.classList.add('most-visited-page');
224
225      this.data_ = null;
226      this.mostVisitedTiles_ = this.getElementsByClassName('most-visited');
227    },
228
229    /**
230     * Create blank (filler) tiles.
231     * @private
232     */
233    createTiles_: function() {
234      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
235        this.appendTile(new MostVisited());
236      }
237    },
238
239    /**
240     * Update the tiles after a change to |data_|.
241     */
242    updateTiles_: function() {
243      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
244        var page = this.data_[i];
245        page.index = i;
246        var tile = this.mostVisitedTiles_[i];
247
248        if (i >= this.data_.length)
249          tile.reset();
250        else
251          tile.updateForData(page);
252      }
253    },
254
255    /**
256     * Array of most visited data objects.
257     * @type {Array}
258     */
259    get data() {
260      return this.data_;
261    },
262    set data(data) {
263      // The first time data is set, create the tiles.
264      if (!this.data_)
265        this.createTiles_();
266
267      // We append the class name with the "filler" so that we can style fillers
268      // differently.
269      this.data_ = data.slice(0, THUMBNAIL_COUNT);
270      this.updateTiles_();
271    },
272
273    /** @inheritDoc */
274    heightForWidth: heightForWidth,
275  };
276
277  return {
278    MostVisitedPage: MostVisitedPage,
279  };
280});
281