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