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.
6 * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode.
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}
14 * @suppress {checkStructDictInheritance}
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);
23 this.__proto__ = Ribbon.prototype;
24 this.className = 'ribbon';
27 * @private {!cr.ui.ArrayDataModel}
30 this.dataModel_ = dataModel;
33 * @private {!cr.ui.ListSelectionModel}
36 this.selectionModel_ = selectionModel;
39 * @private {!ThumbnailModel}
42 this.thumbnailModel_ = thumbnailModel;
48 this.renderCache_ = {};
54 this.firstVisibleIndex_ = 0;
60 this.lastVisibleIndex_ = -1;
63 * @type {?function(!Event)}
66 this.onContentBound_ = null;
69 * @type {?function(!Event)}
72 this.onSpliceBound_ = null;
75 * @type {?function(!Event)}
78 this.onSelectionBound_ = null;
84 this.removeTimeout_ = null;
90 * Inherit from HTMLDivElement.
92 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
95 * Max number of thumbnails in the ribbon.
99 Ribbon.ITEMS_COUNT = 5;
102 * Force redraw the ribbon.
104 Ribbon.prototype.redraw = function() {
109 * Clear all cached data to force full redraw on the next selection change.
111 Ribbon.prototype.reset = function() {
112 this.renderCache_ = {};
113 this.firstVisibleIndex_ = 0;
114 this.lastVisibleIndex_ = -1; // Zero thumbnails
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_);
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.
151 Ribbon.prototype.onSplice_ = function(event) {
152 if (event.removed.length > 0 && event.added.length > 0) {
153 console.error('Replacing is not implemented.');
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]);
162 var element = this.renderThumbnail_(index);
163 var nextItem = this.dataModel_.item(index + 1);
165 nextItem && this.renderCache_[nextItem.getEntry().toURL()];
166 this.insertBefore(element, nextElement);
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');
178 // Push a new item at the right end.
179 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
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');
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';
205 for (var i = 0; i < event.removed.length; i++) {
206 var removedDom = this.renderCache_[event.removed[i].getEntry().toURL()];
208 removedDom.removeAttribute('selected');
209 removedDom.setAttribute('vanishing', 'smooth');
215 this.scheduleRemove_();
221 * Selection change handler.
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;
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_);
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_)
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');
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';
282 this.insertBefore(startBox, this.firstChild);
284 this.appendChild(startBox);
285 setTimeout(function() {
286 startBox.style.marginLeft = '0';
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';
294 this.insertBefore(startBox, this.firstChild);
296 this.appendChild(startBox);
297 setTimeout(function() {
298 startBox.style.marginLeft = -margin + 'px';
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_();
314 var oldSelected = this.querySelector('[selected]');
316 oldSelected.removeAttribute('selected');
319 this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
321 newSelected.setAttribute('selected', true);
325 * Schedule the removal of thumbnails marked as vanishing.
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_();
339 * Remove all thumbnails marked as vanishing.
342 Ribbon.prototype.removeVanishing_ = function() {
343 if (this.removeTimeout_) {
344 clearTimeout(this.removeTimeout_);
345 this.removeTimeout_ = 0;
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]);
355 * Create a DOM element for a thumbnail.
357 * @param {number} index Item index.
358 * @return {!Element} Newly created element.
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];
367 var img = cached.querySelector('img');
369 img.classList.add('cached');
373 var thumbnail = assertInstanceof(this.ownerDocument.createElement('div'),
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);
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;
394 * Set the thumbnail image.
396 * @param {!Element} thumbnail Thumbnail element.
397 * @param {!Gallery.Item} item Gallery item.
400 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
401 if (!item.getThumbnailMetadataItem())
403 this.thumbnailModel_.get([item.getEntry()]).then(function(metadataList) {
404 var loader = new ThumbnailLoader(
406 ThumbnailLoader.LoaderType.IMAGE,
409 thumbnail.querySelector('.image-wrapper'),
410 ThumbnailLoader.FillMode.FILL /* fill */,
411 ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
416 * Content change handler.
418 * @param {!Event} event Event.
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.
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];