Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / ui / file_manager / gallery / js / ribbon.js
blob7b829eeff699d8ac00086ff1629078f98e383541
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  * @param {!ThumbnailModel} thumbnailModel
12  * @extends {HTMLDivElement}
13  * @constructor
14  * @suppress {checkStructDictInheritance}
15  * @struct
16  */
17 function Ribbon(document, dataModel, selectionModel, thumbnailModel) {
18   if (this instanceof Ribbon) {
19     return Ribbon.call(/** @type {Ribbon} */ (document.createElement('div')),
20         document, dataModel, selectionModel, thumbnailModel);
21   }
23   this.__proto__ = Ribbon.prototype;
24   this.className = 'ribbon';
26   /**
27    * @private {!cr.ui.ArrayDataModel}
28    * @const
29    */
30   this.dataModel_ = dataModel;
32   /**
33    * @private {!cr.ui.ListSelectionModel}
34    * @const
35    */
36   this.selectionModel_ = selectionModel;
38   /**
39    * @private {!ThumbnailModel}
40    * @const
41    */
42   this.thumbnailModel_ = thumbnailModel;
44   /**
45    * @type {!Object}
46    * @private
47    */
48   this.renderCache_ = {};
50   /**
51    * @type {number}
52    * @private
53    */
54   this.firstVisibleIndex_ = 0;
56   /**
57    * @type {number}
58    * @private
59    */
60   this.lastVisibleIndex_ = -1;
62   /**
63    * @type {?function(!Event)}
64    * @private
65    */
66   this.onContentBound_ = null;
68   /**
69    * @type {?function(!Event)}
70    * @private
71    */
72   this.onSpliceBound_ = null;
74   /**
75    * @type {?function(!Event)}
76    * @private
77    */
78   this.onSelectionBound_ = null;
80   /**
81    * @type {?number}
82    * @private
83    */
84   this.removeTimeout_ = null;
86   return this;
89 /**
90  * Inherit from HTMLDivElement.
91  */
92 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
94 /**
95  * Max number of thumbnails in the ribbon.
96  * @type {number}
97  * @const
98  */
99 Ribbon.ITEMS_COUNT = 5;
102  * Force redraw the ribbon.
103  */
104 Ribbon.prototype.redraw = function() {
105   this.onSelection_();
109  * Clear all cached data to force full redraw on the next selection change.
110  */
111 Ribbon.prototype.reset = function() {
112   this.renderCache_ = {};
113   this.firstVisibleIndex_ = 0;
114   this.lastVisibleIndex_ = -1;  // Zero thumbnails
118  * Enable the ribbon.
119  */
120 Ribbon.prototype.enable = function() {
121   this.onContentBound_ = this.onContentChange_.bind(this);
122   this.dataModel_.addEventListener('content', this.onContentBound_);
124   this.onSpliceBound_ = this.onSplice_.bind(this);
125   this.dataModel_.addEventListener('splice', this.onSpliceBound_);
127   this.onSelectionBound_ = this.onSelection_.bind(this);
128   this.selectionModel_.addEventListener('change', this.onSelectionBound_);
130   this.reset();
131   this.redraw();
135  * Disable ribbon.
136  */
137 Ribbon.prototype.disable = function() {
138   this.dataModel_.removeEventListener('content', this.onContentBound_);
139   this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
140   this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
142   this.removeVanishing_();
143   this.textContent = '';
147  * Data model splice handler.
148  * @param {!Event} event Event.
149  * @private
150  */
151 Ribbon.prototype.onSplice_ = function(event) {
152   if (event.removed.length > 0 && event.added.length > 0) {
153     console.error('Replacing is not implemented.');
154     return;
155   }
157   if (event.added.length > 0) {
158     for (var i = 0; i < event.added.length; i++) {
159       var index = this.dataModel_.indexOf(event.added[i]);
160       if (index === -1)
161         continue;
162       var element = this.renderThumbnail_(index);
163       var nextItem = this.dataModel_.item(index + 1);
164       var nextElement =
165           nextItem && this.renderCache_[nextItem.getEntry().toURL()];
166       this.insertBefore(element, nextElement);
167     }
168     return;
169   }
171   var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
172   if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
173     var lastNode = persistentNodes[persistentNodes.length - 1];
174     if (lastNode.nextSibling) {
175       // Pull back a vanishing node from the right.
176       lastNode.nextSibling.removeAttribute('vanishing');
177     } else {
178       // Push a new item at the right end.
179       this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
180     }
181   } else {
182     // No items to the right, move the window to the left.
183     this.lastVisibleIndex_--;
184     if (this.firstVisibleIndex_) {
185       this.firstVisibleIndex_--;
186       var firstNode = persistentNodes[0];
187       if (firstNode.previousSibling) {
188         // Pull back a vanishing node from the left.
189         firstNode.previousSibling.removeAttribute('vanishing');
190       } else {
191         // Push a new item at the left end.
192         if (this.firstVisibleIndex_ < this.dataModel_.length) {
193           var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
194           newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
195           this.insertBefore(newThumbnail, this.firstChild);
196           setTimeout(function() {
197             newThumbnail.style.marginLeft = '0';
198           }, 0);
199         }
200       }
201     }
202   }
204   var removed = false;
205   for (var i = 0; i < event.removed.length; i++) {
206     var removedDom = this.renderCache_[event.removed[i].getEntry().toURL()];
207     if (removedDom) {
208       removedDom.removeAttribute('selected');
209       removedDom.setAttribute('vanishing', 'smooth');
210       removed = true;
211     }
212   }
214   if (removed)
215     this.scheduleRemove_();
217   this.onSelection_();
221  * Selection change handler.
222  * @private
223  */
224 Ribbon.prototype.onSelection_ = function() {
225   var indexes = this.selectionModel_.selectedIndexes;
226   if (indexes.length == 0)
227     return;  // Ignore temporary empty selection.
228   var selectedIndex = indexes[0];
230   var length = this.dataModel_.length;
232   // TODO(dgozman): use margin instead of 2 here.
233   var itemWidth = this.clientHeight - 2;
234   var fullItems = Math.min(Ribbon.ITEMS_COUNT, length);
235   var right = Math.floor((fullItems - 1) / 2);
237   var fullWidth = fullItems * itemWidth;
238   this.style.width = fullWidth + 'px';
240   var lastIndex = selectedIndex + right;
241   lastIndex = Math.max(lastIndex, fullItems - 1);
242   lastIndex = Math.min(lastIndex, length - 1);
243   var firstIndex = lastIndex - fullItems + 1;
245   if (this.firstVisibleIndex_ != firstIndex ||
246       this.lastVisibleIndex_ != lastIndex) {
248     if (this.lastVisibleIndex_ == -1) {
249       this.firstVisibleIndex_ = firstIndex;
250       this.lastVisibleIndex_ = lastIndex;
251     }
253     this.removeVanishing_();
255     this.textContent = '';
256     var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
257     // All the items except the first one treated equally.
258     for (var index = startIndex + 1;
259          index <= Math.max(lastIndex, this.lastVisibleIndex_);
260          ++index) {
261       // Only add items that are in either old or the new viewport.
262       if (this.lastVisibleIndex_ < index && index < firstIndex ||
263           lastIndex < index && index < this.firstVisibleIndex_)
264         continue;
265       var box = this.renderThumbnail_(index);
266       box.style.marginLeft = '0';
267       this.appendChild(box);
268       if (index < firstIndex || index > lastIndex) {
269         // If the node is not in the new viewport we only need it while
270         // the animation is playing out.
271         box.setAttribute('vanishing', 'slide');
272       }
273     }
275     var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
276     var margin = itemWidth * slideCount;
277     var startBox = this.renderThumbnail_(startIndex);
278     if (startIndex == firstIndex) {
279       // Sliding to the right.
280       startBox.style.marginLeft = -margin + 'px';
281       if (this.firstChild)
282         this.insertBefore(startBox, this.firstChild);
283       else
284         this.appendChild(startBox);
285       setTimeout(function() {
286         startBox.style.marginLeft = '0';
287       }, 0);
288     } else {
289       // Sliding to the left. Start item will become invisible and should be
290       // removed afterwards.
291       startBox.setAttribute('vanishing', 'slide');
292       startBox.style.marginLeft = '0';
293       if (this.firstChild)
294         this.insertBefore(startBox, this.firstChild);
295       else
296         this.appendChild(startBox);
297       setTimeout(function() {
298         startBox.style.marginLeft = -margin + 'px';
299       }, 0);
300     }
302     ImageUtil.setClass(this, 'fade-left',
303         firstIndex > 0 && selectedIndex != firstIndex);
305     ImageUtil.setClass(this, 'fade-right',
306         lastIndex < length - 1 && selectedIndex != lastIndex);
308     this.firstVisibleIndex_ = firstIndex;
309     this.lastVisibleIndex_ = lastIndex;
311     this.scheduleRemove_();
312   }
314   var oldSelected = this.querySelector('[selected]');
315   if (oldSelected)
316     oldSelected.removeAttribute('selected');
318   var newSelected =
319       this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
320   if (newSelected)
321     newSelected.setAttribute('selected', true);
325  * Schedule the removal of thumbnails marked as vanishing.
326  * @private
327  */
328 Ribbon.prototype.scheduleRemove_ = function() {
329   if (this.removeTimeout_)
330     clearTimeout(this.removeTimeout_);
332   this.removeTimeout_ = setTimeout(function() {
333     this.removeTimeout_ = null;
334     this.removeVanishing_();
335   }.bind(this), 200);
339  * Remove all thumbnails marked as vanishing.
340  * @private
341  */
342 Ribbon.prototype.removeVanishing_ = function() {
343   if (this.removeTimeout_) {
344     clearTimeout(this.removeTimeout_);
345     this.removeTimeout_ = 0;
346   }
347   var vanishingNodes = this.querySelectorAll('[vanishing]');
348   for (var i = 0; i != vanishingNodes.length; i++) {
349     vanishingNodes[i].removeAttribute('vanishing');
350     this.removeChild(vanishingNodes[i]);
351   }
355  * Create a DOM element for a thumbnail.
357  * @param {number} index Item index.
358  * @return {!Element} Newly created element.
359  * @private
360  */
361 Ribbon.prototype.renderThumbnail_ = function(index) {
362   var item = assertInstanceof(this.dataModel_.item(index), Gallery.Item);
363   var url = item.getEntry().toURL();
365   var cached = this.renderCache_[url];
366   if (cached) {
367     var img = cached.querySelector('img');
368     if (img)
369       img.classList.add('cached');
370     return cached;
371   }
373   var thumbnail = assertInstanceof(this.ownerDocument.createElement('div'),
374       HTMLDivElement);
375   thumbnail.className = 'ribbon-image';
376   thumbnail.addEventListener('click', function() {
377     var index = this.dataModel_.indexOf(item);
378     this.selectionModel_.unselectAll();
379     this.selectionModel_.setIndexSelected(index, true);
380   }.bind(this));
382   util.createChild(thumbnail, 'image-wrapper');
384   this.setThumbnailImage_(thumbnail, item);
386   // TODO: Implement LRU eviction.
387   // Never evict the thumbnails that are currently in the DOM because we rely
388   // on this cache to find them by URL.
389   this.renderCache_[url] = thumbnail;
390   return thumbnail;
394  * Set the thumbnail image.
396  * @param {!Element} thumbnail Thumbnail element.
397  * @param {!Gallery.Item} item Gallery item.
398  * @private
399  */
400 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
401   if (!item.getThumbnailMetadataItem())
402     return;
403   this.thumbnailModel_.get([item.getEntry()]).then(function(metadataList) {
404     var loader = new ThumbnailLoader(
405         item.getEntry(),
406         ThumbnailLoader.LoaderType.IMAGE,
407         metadataList[0]);
408     loader.load(
409         thumbnail.querySelector('.image-wrapper'),
410         ThumbnailLoader.FillMode.FILL /* fill */,
411         ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
412   });
416  * Content change handler.
418  * @param {!Event} event Event.
419  * @private
420  */
421 Ribbon.prototype.onContentChange_ = function(event) {
422   var url = event.item.getEntry().toURL();
423   if (event.oldEntry.toURL() !== url)
424     this.remapCache_(event.oldEntry.toURL(), url);
426   var thumbnail = this.renderCache_[url];
427   if (thumbnail && event.item)
428     this.setThumbnailImage_(thumbnail, event.item);
432  * Update the thumbnail element cache.
434  * @param {string} oldUrl Old url.
435  * @param {string} newUrl New url.
436  * @private
437  */
438 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
439   if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
440     this.renderCache_[newUrl] = this.renderCache_[oldUrl];
441     delete this.renderCache_[oldUrl];
442   }