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