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