Home | History | Annotate | Download | only in 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_.getOne(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   if (event.oldEntry.toURL() !== url)
    348     this.remapCache_(event.oldEntry.toURL(), url);
    349 
    350   var thumbnail = this.renderCache_[url];
    351   if (thumbnail && event.metadata)
    352     this.setThumbnailImage_(thumbnail, event.item.getEntry(), event.metadata);
    353 };
    354 
    355 /**
    356  * Update the thumbnail element cache.
    357  *
    358  * @param {string} oldUrl Old url.
    359  * @param {string} newUrl New url.
    360  * @private
    361  */
    362 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
    363   if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
    364     this.renderCache_[newUrl] = this.renderCache_[oldUrl];
    365     delete this.renderCache_[oldUrl];
    366   }
    367 };
    368