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