Fixed a bug that ribbon doesn't disappear in edit mode.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / ribbon.js
blob5926a32f3eeffd63cde772fc6608cf5f46e444b9
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.
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  * @extends {HTMLDivElement}
12  * @constructor
13  * @suppress {checkStructDictInheritance}
14  * @struct
15  */
16 function Ribbon(document, dataModel, selectionModel) {
17   if (this instanceof Ribbon) {
18     return Ribbon.call(/** @type {Ribbon} */ (document.createElement('div')),
19         document, dataModel, selectionModel);
20   }
22   this.__proto__ = Ribbon.prototype;
23   this.className = 'ribbon';
25   /**
26    * @type {!cr.ui.ArrayDataModel}
27    * @private
28    */
29   this.dataModel_ = dataModel;
31   /**
32    * @type {!cr.ui.ListSelectionModel}
33    * @private
34    */
35   this.selectionModel_ = selectionModel;
37   /**
38    * @type {!Object}
39    * @private
40    */
41   this.renderCache_ = {};
43   /**
44    * @type {number}
45    * @private
46    */
47   this.firstVisibleIndex_ = 0;
49   /**
50    * @type {number}
51    * @private
52    */
53   this.lastVisibleIndex_ = -1;
55   /**
56    * @type {?function(!Event)}
57    * @private
58    */
59   this.onContentBound_ = null;
61   /**
62    * @type {?function(!Event)}
63    * @private
64    */
65   this.onSpliceBound_ = null;
67   /**
68    * @type {?function(!Event)}
69    * @private
70    */
71   this.onSelectionBound_ = null;
73   /**
74    * @type {?number}
75    * @private
76    */
77   this.removeTimeout_ = null;
79   return this;
82 /**
83  * Inherit from HTMLDivElement.
84  */
85 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
87 /**
88  * Max number of thumbnails in the ribbon.
89  * @type {number}
90  * @const
91  */
92 Ribbon.ITEMS_COUNT = 5;
94 /**
95  * Force redraw the ribbon.
96  */
97 Ribbon.prototype.redraw = function() {
98   this.onSelection_();
102  * Clear all cached data to force full redraw on the next selection change.
103  */
104 Ribbon.prototype.reset = function() {
105   this.renderCache_ = {};
106   this.firstVisibleIndex_ = 0;
107   this.lastVisibleIndex_ = -1;  // Zero thumbnails
111  * Enable the ribbon.
112  */
113 Ribbon.prototype.enable = function() {
114   this.onContentBound_ = this.onContentChange_.bind(this);
115   this.dataModel_.addEventListener('content', this.onContentBound_);
117   this.onSpliceBound_ = this.onSplice_.bind(this);
118   this.dataModel_.addEventListener('splice', this.onSpliceBound_);
120   this.onSelectionBound_ = this.onSelection_.bind(this);
121   this.selectionModel_.addEventListener('change', this.onSelectionBound_);
123   this.reset();
124   this.redraw();
128  * Disable ribbon.
129  */
130 Ribbon.prototype.disable = function() {
131   this.dataModel_.removeEventListener('content', this.onContentBound_);
132   this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
133   this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
135   this.removeVanishing_();
136   this.textContent = '';
140  * Data model splice handler.
141  * @param {!Event} event Event.
142  * @private
143  */
144 Ribbon.prototype.onSplice_ = function(event) {
145   if (event.removed.length > 0 && event.added.length > 0) {
146     console.error('Replacing is not implemented.');
147     return;
148   }
150   if (event.added.length > 0) {
151     for (var i = 0; i < event.added.length; i++) {
152       var index = this.dataModel_.indexOf(event.added[i]);
153       if (index === -1)
154         continue;
155       var element = this.renderThumbnail_(index);
156       var nextItem = this.dataModel_.item(index + 1);
157       var nextElement =
158           nextItem && this.renderCache_[nextItem.getEntry().toURL()];
159       this.insertBefore(element, nextElement);
160     }
161     return;
162   }
164   var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
165   if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
166     var lastNode = persistentNodes[persistentNodes.length - 1];
167     if (lastNode.nextSibling) {
168       // Pull back a vanishing node from the right.
169       lastNode.nextSibling.removeAttribute('vanishing');
170     } else {
171       // Push a new item at the right end.
172       this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
173     }
174   } else {
175     // No items to the right, move the window to the left.
176     this.lastVisibleIndex_--;
177     if (this.firstVisibleIndex_) {
178       this.firstVisibleIndex_--;
179       var firstNode = persistentNodes[0];
180       if (firstNode.previousSibling) {
181         // Pull back a vanishing node from the left.
182         firstNode.previousSibling.removeAttribute('vanishing');
183       } else {
184         // Push a new item at the left end.
185         var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
186         newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
187         this.insertBefore(newThumbnail, this.firstChild);
188         setTimeout(function() {
189           newThumbnail.style.marginLeft = '0';
190         }, 0);
191       }
192     }
193   }
195   var removed = false;
196   for (var i = 0; i < event.removed.length; i++) {
197     var removedDom = this.renderCache_[event.removed[i].getEntry().toURL()];
198     if (removedDom) {
199       removedDom.removeAttribute('selected');
200       removedDom.setAttribute('vanishing', 'smooth');
201       removed = true;
202     }
203   }
205   if (removed)
206     this.scheduleRemove_();
208   this.onSelection_();
212  * Selection change handler.
213  * @private
214  */
215 Ribbon.prototype.onSelection_ = function() {
216   var indexes = this.selectionModel_.selectedIndexes;
217   if (indexes.length == 0)
218     return;  // Ignore temporary empty selection.
219   var selectedIndex = indexes[0];
221   var length = this.dataModel_.length;
223   // TODO(dgozman): use margin instead of 2 here.
224   var itemWidth = this.clientHeight - 2;
225   var fullItems = Math.min(Ribbon.ITEMS_COUNT, length);
226   var right = Math.floor((fullItems - 1) / 2);
228   var fullWidth = fullItems * itemWidth;
229   this.style.width = fullWidth + 'px';
231   var lastIndex = selectedIndex + right;
232   lastIndex = Math.max(lastIndex, fullItems - 1);
233   lastIndex = Math.min(lastIndex, length - 1);
234   var firstIndex = lastIndex - fullItems + 1;
236   if (this.firstVisibleIndex_ != firstIndex ||
237       this.lastVisibleIndex_ != lastIndex) {
239     if (this.lastVisibleIndex_ == -1) {
240       this.firstVisibleIndex_ = firstIndex;
241       this.lastVisibleIndex_ = lastIndex;
242     }
244     this.removeVanishing_();
246     this.textContent = '';
247     var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
248     // All the items except the first one treated equally.
249     for (var index = startIndex + 1;
250          index <= Math.max(lastIndex, this.lastVisibleIndex_);
251          ++index) {
252       // Only add items that are in either old or the new viewport.
253       if (this.lastVisibleIndex_ < index && index < firstIndex ||
254           lastIndex < index && index < this.firstVisibleIndex_)
255         continue;
256       var box = this.renderThumbnail_(index);
257       box.style.marginLeft = '0';
258       this.appendChild(box);
259       if (index < firstIndex || index > lastIndex) {
260         // If the node is not in the new viewport we only need it while
261         // the animation is playing out.
262         box.setAttribute('vanishing', 'slide');
263       }
264     }
266     var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
267     var margin = itemWidth * slideCount;
268     var startBox = this.renderThumbnail_(startIndex);
269     if (startIndex == firstIndex) {
270       // Sliding to the right.
271       startBox.style.marginLeft = -margin + 'px';
272       if (this.firstChild)
273         this.insertBefore(startBox, this.firstChild);
274       else
275         this.appendChild(startBox);
276       setTimeout(function() {
277         startBox.style.marginLeft = '0';
278       }, 0);
279     } else {
280       // Sliding to the left. Start item will become invisible and should be
281       // removed afterwards.
282       startBox.setAttribute('vanishing', 'slide');
283       startBox.style.marginLeft = '0';
284       if (this.firstChild)
285         this.insertBefore(startBox, this.firstChild);
286       else
287         this.appendChild(startBox);
288       setTimeout(function() {
289         startBox.style.marginLeft = -margin + 'px';
290       }, 0);
291     }
293     ImageUtil.setClass(this, 'fade-left',
294         firstIndex > 0 && selectedIndex != firstIndex);
296     ImageUtil.setClass(this, 'fade-right',
297         lastIndex < length - 1 && selectedIndex != lastIndex);
299     this.firstVisibleIndex_ = firstIndex;
300     this.lastVisibleIndex_ = lastIndex;
302     this.scheduleRemove_();
303   }
305   var oldSelected = this.querySelector('[selected]');
306   if (oldSelected)
307     oldSelected.removeAttribute('selected');
309   var newSelected =
310       this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
311   if (newSelected)
312     newSelected.setAttribute('selected', true);
316  * Schedule the removal of thumbnails marked as vanishing.
317  * @private
318  */
319 Ribbon.prototype.scheduleRemove_ = function() {
320   if (this.removeTimeout_)
321     clearTimeout(this.removeTimeout_);
323   this.removeTimeout_ = setTimeout(function() {
324     this.removeTimeout_ = null;
325     this.removeVanishing_();
326   }.bind(this), 200);
330  * Remove all thumbnails marked as vanishing.
331  * @private
332  */
333 Ribbon.prototype.removeVanishing_ = function() {
334   if (this.removeTimeout_) {
335     clearTimeout(this.removeTimeout_);
336     this.removeTimeout_ = 0;
337   }
338   var vanishingNodes = this.querySelectorAll('[vanishing]');
339   for (var i = 0; i != vanishingNodes.length; i++) {
340     vanishingNodes[i].removeAttribute('vanishing');
341     this.removeChild(vanishingNodes[i]);
342   }
346  * Create a DOM element for a thumbnail.
348  * @param {number} index Item index.
349  * @return {!Element} Newly created element.
350  * @private
351  */
352 Ribbon.prototype.renderThumbnail_ = function(index) {
353   var item = assertInstanceof(this.dataModel_.item(index), Gallery.Item);
354   var url = item.getEntry().toURL();
356   var cached = this.renderCache_[url];
357   if (cached) {
358     var img = cached.querySelector('img');
359     if (img)
360       img.classList.add('cached');
361     return cached;
362   }
364   var thumbnail = assertInstanceof(this.ownerDocument.createElement('div'),
365       HTMLDivElement);
366   thumbnail.className = 'ribbon-image';
367   thumbnail.addEventListener('click', function() {
368     var index = this.dataModel_.indexOf(item);
369     this.selectionModel_.unselectAll();
370     this.selectionModel_.setIndexSelected(index, true);
371   }.bind(this));
373   util.createChild(thumbnail, 'image-wrapper');
375   this.setThumbnailImage_(thumbnail, item);
377   // TODO: Implement LRU eviction.
378   // Never evict the thumbnails that are currently in the DOM because we rely
379   // on this cache to find them by URL.
380   this.renderCache_[url] = thumbnail;
381   return thumbnail;
385  * Set the thumbnail image.
387  * @param {!Element} thumbnail Thumbnail element.
388  * @param {!Gallery.Item} item Gallery item.
389  * @private
390  */
391 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
392   var loader = new ThumbnailLoader(
393       item.getEntry(),
394       ThumbnailLoader.LoaderType.IMAGE,
395       item.getMetadata());
396   loader.load(
397       thumbnail.querySelector('.image-wrapper'),
398       ThumbnailLoader.FillMode.FILL /* fill */,
399       ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
403  * Content change handler.
405  * @param {!Event} event Event.
406  * @private
407  */
408 Ribbon.prototype.onContentChange_ = function(event) {
409   var url = event.item.getEntry().toURL();
410   if (event.oldEntry.toURL() !== url)
411     this.remapCache_(event.oldEntry.toURL(), url);
413   var thumbnail = this.renderCache_[url];
414   if (thumbnail && event.item)
415     this.setThumbnailImage_(thumbnail, event.item);
419  * Update the thumbnail element cache.
421  * @param {string} oldUrl Old url.
422  * @param {string} newUrl New url.
423  * @private
424  */
425 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
426   if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
427     this.renderCache_[newUrl] = this.renderCache_[oldUrl];
428     delete this.renderCache_[oldUrl];
429   }