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