Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / ribbon.js
1 // Copyright 2014 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 'use strict';
6
7 /**
8  * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode.
9  *
10  * @param {Document} document Document.
11  * @param {cr.ui.ArrayDataModel} dataModel Data model.
12  * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
13  * @return {Element} Ribbon element.
14  * @constructor
15  */
16 function Ribbon(document, dataModel, selectionModel) {
17   var self = document.createElement('div');
18   Ribbon.decorate(self, dataModel, selectionModel);
19   return self;
20 }
21
22 /**
23  * Inherit from HTMLDivElement.
24  */
25 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
26
27 /**
28  * Decorate a Ribbon instance.
29  *
30  * @param {Ribbon} self Self pointer.
31  * @param {cr.ui.ArrayDataModel} dataModel Data model.
32  * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
33  */
34 Ribbon.decorate = function(self, dataModel, selectionModel) {
35   self.__proto__ = Ribbon.prototype;
36   self.dataModel_ = dataModel;
37   self.selectionModel_ = selectionModel;
38
39   self.className = 'ribbon';
40 };
41
42 /**
43  * Max number of thumbnails in the ribbon.
44  * @type {number}
45  */
46 Ribbon.ITEMS_COUNT = 5;
47
48 /**
49  * Force redraw the ribbon.
50  */
51 Ribbon.prototype.redraw = function() {
52   this.onSelection_();
53 };
54
55 /**
56  * Clear all cached data to force full redraw on the next selection change.
57  */
58 Ribbon.prototype.reset = function() {
59   this.renderCache_ = {};
60   this.firstVisibleIndex_ = 0;
61   this.lastVisibleIndex_ = -1;  // Zero thumbnails
62 };
63
64 /**
65  * Enable the ribbon.
66  */
67 Ribbon.prototype.enable = function() {
68   this.onContentBound_ = this.onContentChange_.bind(this);
69   this.dataModel_.addEventListener('content', this.onContentBound_);
70
71   this.onSpliceBound_ = this.onSplice_.bind(this);
72   this.dataModel_.addEventListener('splice', this.onSpliceBound_);
73
74   this.onSelectionBound_ = this.onSelection_.bind(this);
75   this.selectionModel_.addEventListener('change', this.onSelectionBound_);
76
77   this.reset();
78   this.redraw();
79 };
80
81 /**
82  * Disable ribbon.
83  */
84 Ribbon.prototype.disable = function() {
85   this.dataModel_.removeEventListener('content', this.onContentBound_);
86   this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
87   this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
88
89   this.removeVanishing_();
90   this.textContent = '';
91 };
92
93 /**
94  * Data model splice handler.
95  * @param {Event} event Event.
96  * @private
97  */
98 Ribbon.prototype.onSplice_ = function(event) {
99   if (event.removed.length > 1) {
100     console.error('Cannot remove multiple items.');
101     return;
102   }
103
104   if (event.removed.length > 0 && event.added.length > 0) {
105     console.error('Replacing is not implemented.');
106     return;
107   }
108
109   if (event.added.length > 0) {
110     for (var i = 0; i < event.added.length; i++) {
111       var index = this.dataModel_.indexOf(event.added[i]);
112       if (index === -1)
113         continue;
114       var element = this.renderThumbnail_(index);
115       var nextItem = this.dataModel_.item(index + 1);
116       var nextElement =
117           nextItem && this.renderCache_[nextItem.getEntry().toURL()];
118       this.insertBefore(element, nextElement);
119     }
120     return;
121   }
122
123   var removed = this.renderCache_[event.removed[0].getEntry().toURL()];
124   if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) {
125     console.error('Can only remove the selected item');
126     return;
127   }
128
129   var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
130   if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
131     var lastNode = persistentNodes[persistentNodes.length - 1];
132     if (lastNode.nextSibling) {
133       // Pull back a vanishing node from the right.
134       lastNode.nextSibling.removeAttribute('vanishing');
135     } else {
136       // Push a new item at the right end.
137       this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
138     }
139   } else {
140     // No items to the right, move the window to the left.
141     this.lastVisibleIndex_--;
142     if (this.firstVisibleIndex_) {
143       this.firstVisibleIndex_--;
144       var firstNode = persistentNodes[0];
145       if (firstNode.previousSibling) {
146         // Pull back a vanishing node from the left.
147         firstNode.previousSibling.removeAttribute('vanishing');
148       } else {
149         // Push a new item at the left end.
150         var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
151         newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
152         this.insertBefore(newThumbnail, this.firstChild);
153         setTimeout(function() {
154           newThumbnail.style.marginLeft = '0';
155         }, 0);
156       }
157     }
158   }
159
160   removed.removeAttribute('selected');
161   removed.setAttribute('vanishing', 'smooth');
162   this.scheduleRemove_();
163 };
164
165 /**
166  * Selection change handler.
167  * @private
168  */
169 Ribbon.prototype.onSelection_ = function() {
170   var indexes = this.selectionModel_.selectedIndexes;
171   if (indexes.length == 0)
172     return;  // Ignore temporary empty selection.
173   var selectedIndex = indexes[0];
174
175   var length = this.dataModel_.length;
176
177   // TODO(dgozman): use margin instead of 2 here.
178   var itemWidth = this.clientHeight - 2;
179   var fullItems = Math.min(Ribbon.ITEMS_COUNT, length);
180   var right = Math.floor((fullItems - 1) / 2);
181
182   var fullWidth = fullItems * itemWidth;
183   this.style.width = fullWidth + 'px';
184
185   var lastIndex = selectedIndex + right;
186   lastIndex = Math.max(lastIndex, fullItems - 1);
187   lastIndex = Math.min(lastIndex, length - 1);
188   var firstIndex = lastIndex - fullItems + 1;
189
190   if (this.firstVisibleIndex_ != firstIndex ||
191       this.lastVisibleIndex_ != lastIndex) {
192
193     if (this.lastVisibleIndex_ == -1) {
194       this.firstVisibleIndex_ = firstIndex;
195       this.lastVisibleIndex_ = lastIndex;
196     }
197
198     this.removeVanishing_();
199
200     this.textContent = '';
201     var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
202     // All the items except the first one treated equally.
203     for (var index = startIndex + 1;
204          index <= Math.max(lastIndex, this.lastVisibleIndex_);
205          ++index) {
206       // Only add items that are in either old or the new viewport.
207       if (this.lastVisibleIndex_ < index && index < firstIndex ||
208           lastIndex < index && index < this.firstVisibleIndex_)
209         continue;
210       var box = this.renderThumbnail_(index);
211       box.style.marginLeft = '0';
212       this.appendChild(box);
213       if (index < firstIndex || index > lastIndex) {
214         // If the node is not in the new viewport we only need it while
215         // the animation is playing out.
216         box.setAttribute('vanishing', 'slide');
217       }
218     }
219
220     var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
221     var margin = itemWidth * slideCount;
222     var startBox = this.renderThumbnail_(startIndex);
223     if (startIndex == firstIndex) {
224       // Sliding to the right.
225       startBox.style.marginLeft = -margin + 'px';
226       if (this.firstChild)
227         this.insertBefore(startBox, this.firstChild);
228       else
229         this.appendChild(startBox);
230       setTimeout(function() {
231         startBox.style.marginLeft = '0';
232       }, 0);
233     } else {
234       // Sliding to the left. Start item will become invisible and should be
235       // removed afterwards.
236       startBox.setAttribute('vanishing', 'slide');
237       startBox.style.marginLeft = '0';
238       if (this.firstChild)
239         this.insertBefore(startBox, this.firstChild);
240       else
241         this.appendChild(startBox);
242       setTimeout(function() {
243         startBox.style.marginLeft = -margin + 'px';
244       }, 0);
245     }
246
247     ImageUtil.setClass(this, 'fade-left',
248         firstIndex > 0 && selectedIndex != firstIndex);
249
250     ImageUtil.setClass(this, 'fade-right',
251         lastIndex < length - 1 && selectedIndex != lastIndex);
252
253     this.firstVisibleIndex_ = firstIndex;
254     this.lastVisibleIndex_ = lastIndex;
255
256     this.scheduleRemove_();
257   }
258
259   var oldSelected = this.querySelector('[selected]');
260   if (oldSelected)
261     oldSelected.removeAttribute('selected');
262
263   var newSelected =
264       this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
265   if (newSelected)
266     newSelected.setAttribute('selected', true);
267 };
268
269 /**
270  * Schedule the removal of thumbnails marked as vanishing.
271  * @private
272  */
273 Ribbon.prototype.scheduleRemove_ = function() {
274   if (this.removeTimeout_)
275     clearTimeout(this.removeTimeout_);
276
277   this.removeTimeout_ = setTimeout(function() {
278     this.removeTimeout_ = null;
279     this.removeVanishing_();
280   }.bind(this), 200);
281 };
282
283 /**
284  * Remove all thumbnails marked as vanishing.
285  * @private
286  */
287 Ribbon.prototype.removeVanishing_ = function() {
288   if (this.removeTimeout_) {
289     clearTimeout(this.removeTimeout_);
290     this.removeTimeout_ = 0;
291   }
292   var vanishingNodes = this.querySelectorAll('[vanishing]');
293   for (var i = 0; i != vanishingNodes.length; i++) {
294     vanishingNodes[i].removeAttribute('vanishing');
295     this.removeChild(vanishingNodes[i]);
296   }
297 };
298
299 /**
300  * Create a DOM element for a thumbnail.
301  *
302  * @param {number} index Item index.
303  * @return {Element} Newly created element.
304  * @private
305  */
306 Ribbon.prototype.renderThumbnail_ = function(index) {
307   var item = this.dataModel_.item(index);
308   var url = item.getEntry().toURL();
309
310   var cached = this.renderCache_[url];
311   if (cached) {
312     var img = cached.querySelector('img');
313     if (img)
314       img.classList.add('cached');
315     return cached;
316   }
317
318   var thumbnail = this.ownerDocument.createElement('div');
319   thumbnail.className = 'ribbon-image';
320   thumbnail.addEventListener('click', function() {
321     var index = this.dataModel_.indexOf(item);
322     this.selectionModel_.unselectAll();
323     this.selectionModel_.setIndexSelected(index, true);
324   }.bind(this));
325
326   util.createChild(thumbnail, 'image-wrapper');
327
328   this.setThumbnailImage_(thumbnail, item);
329
330   // TODO: Implement LRU eviction.
331   // Never evict the thumbnails that are currently in the DOM because we rely
332   // on this cache to find them by URL.
333   this.renderCache_[url] = thumbnail;
334   return thumbnail;
335 };
336
337 /**
338  * Set the thumbnail image.
339  *
340  * @param {Element} thumbnail Thumbnail element.
341  * @param {Gallery.Item} item Gallery item.
342  * @private
343  */
344 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
345   var loader = new ThumbnailLoader(
346       item.getEntry(),
347       ThumbnailLoader.LoaderType.IMAGE,
348       item.getMetadata());
349   loader.load(
350       thumbnail.querySelector('.image-wrapper'),
351       ThumbnailLoader.FillMode.FILL /* fill */,
352       ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
353 };
354
355 /**
356  * Content change handler.
357  *
358  * @param {Event} event Event.
359  * @private
360  */
361 Ribbon.prototype.onContentChange_ = function(event) {
362   var url = event.item.getEntry().toURL();
363   if (event.oldEntry.toURL() !== url)
364     this.remapCache_(event.oldEntry.toURL(), url);
365
366   var thumbnail = this.renderCache_[url];
367   if (thumbnail && event.item)
368     this.setThumbnailImage_(thumbnail, event.item);
369 };
370
371 /**
372  * Update the thumbnail element cache.
373  *
374  * @param {string} oldUrl Old url.
375  * @param {string} newUrl New url.
376  * @private
377  */
378 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
379   if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
380     this.renderCache_[newUrl] = this.renderCache_[oldUrl];
381     delete this.renderCache_[oldUrl];
382   }
383 };