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 * @extends {HTMLDivElement}
13 * @suppress {checkStructDictInheritance}
16 function Ribbon(document, dataModel, selectionModel) {
17 if (this instanceof Ribbon) {
18 return Ribbon.call(/** @type {Ribbon} */ (document.createElement('div')),
19 document, dataModel, selectionModel);
22 this.__proto__ = Ribbon.prototype;
23 this.className = 'ribbon';
26 * @type {!cr.ui.ArrayDataModel}
29 this.dataModel_ = dataModel;
32 * @type {!cr.ui.ListSelectionModel}
35 this.selectionModel_ = selectionModel;
41 this.renderCache_ = {};
47 this.firstVisibleIndex_ = 0;
53 this.lastVisibleIndex_ = -1;
56 * @type {?function(!Event)}
59 this.onContentBound_ = null;
62 * @type {?function(!Event)}
65 this.onSpliceBound_ = null;
68 * @type {?function(!Event)}
71 this.onSelectionBound_ = null;
77 this.removeTimeout_ = null;
83 * Inherit from HTMLDivElement.
85 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
88 * Max number of thumbnails in the ribbon.
92 Ribbon.ITEMS_COUNT = 5;
95 * Force redraw the ribbon.
97 Ribbon.prototype.redraw = function() {
102 * Clear all cached data to force full redraw on the next selection change.
104 Ribbon.prototype.reset = function() {
105 this.renderCache_ = {};
106 this.firstVisibleIndex_ = 0;
107 this.lastVisibleIndex_ = -1; // Zero thumbnails
113 Ribbon.prototype.enable = function() {
114 this.onContentBound_ = this.onContentChange_.bind(this);
115 this.dataModel_.addEventListener('content', this.onContentBound_);
117 this.onSpliceBound_ = this.onSplice_.bind(this);
118 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
120 this.onSelectionBound_ = this.onSelection_.bind(this);
121 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
130 Ribbon.prototype.disable = function() {
131 this.dataModel_.removeEventListener('content', this.onContentBound_);
132 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
133 this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
135 this.removeVanishing_();
136 this.textContent = '';
140 * Data model splice handler.
141 * @param {!Event} event Event.
144 Ribbon.prototype.onSplice_ = function(event) {
145 if (event.removed.length > 0 && event.added.length > 0) {
146 console.error('Replacing is not implemented.');
150 if (event.added.length > 0) {
151 for (var i = 0; i < event.added.length; i++) {
152 var index = this.dataModel_.indexOf(event.added[i]);
155 var element = this.renderThumbnail_(index);
156 var nextItem = this.dataModel_.item(index + 1);
158 nextItem && this.renderCache_[nextItem.getEntry().toURL()];
159 this.insertBefore(element, nextElement);
164 var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
165 if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
166 var lastNode = persistentNodes[persistentNodes.length - 1];
167 if (lastNode.nextSibling) {
168 // Pull back a vanishing node from the right.
169 lastNode.nextSibling.removeAttribute('vanishing');
171 // Push a new item at the right end.
172 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
175 // No items to the right, move the window to the left.
176 this.lastVisibleIndex_--;
177 if (this.firstVisibleIndex_) {
178 this.firstVisibleIndex_--;
179 var firstNode = persistentNodes[0];
180 if (firstNode.previousSibling) {
181 // Pull back a vanishing node from the left.
182 firstNode.previousSibling.removeAttribute('vanishing');
184 // Push a new item at the left end.
185 var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
186 newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
187 this.insertBefore(newThumbnail, this.firstChild);
188 setTimeout(function() {
189 newThumbnail.style.marginLeft = '0';
196 for (var i = 0; i < event.removed.length; i++) {
197 var removedDom = this.renderCache_[event.removed[i].getEntry().toURL()];
199 removedDom.removeAttribute('selected');
200 removedDom.setAttribute('vanishing', 'smooth');
206 this.scheduleRemove_();
212 * Selection change handler.
215 Ribbon.prototype.onSelection_ = function() {
216 var indexes = this.selectionModel_.selectedIndexes;
217 if (indexes.length == 0)
218 return; // Ignore temporary empty selection.
219 var selectedIndex = indexes[0];
221 var length = this.dataModel_.length;
223 // TODO(dgozman): use margin instead of 2 here.
224 var itemWidth = this.clientHeight - 2;
225 var fullItems = Math.min(Ribbon.ITEMS_COUNT, length);
226 var right = Math.floor((fullItems - 1) / 2);
228 var fullWidth = fullItems * itemWidth;
229 this.style.width = fullWidth + 'px';
231 var lastIndex = selectedIndex + right;
232 lastIndex = Math.max(lastIndex, fullItems - 1);
233 lastIndex = Math.min(lastIndex, length - 1);
234 var firstIndex = lastIndex - fullItems + 1;
236 if (this.firstVisibleIndex_ != firstIndex ||
237 this.lastVisibleIndex_ != lastIndex) {
239 if (this.lastVisibleIndex_ == -1) {
240 this.firstVisibleIndex_ = firstIndex;
241 this.lastVisibleIndex_ = lastIndex;
244 this.removeVanishing_();
246 this.textContent = '';
247 var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
248 // All the items except the first one treated equally.
249 for (var index = startIndex + 1;
250 index <= Math.max(lastIndex, this.lastVisibleIndex_);
252 // Only add items that are in either old or the new viewport.
253 if (this.lastVisibleIndex_ < index && index < firstIndex ||
254 lastIndex < index && index < this.firstVisibleIndex_)
256 var box = this.renderThumbnail_(index);
257 box.style.marginLeft = '0';
258 this.appendChild(box);
259 if (index < firstIndex || index > lastIndex) {
260 // If the node is not in the new viewport we only need it while
261 // the animation is playing out.
262 box.setAttribute('vanishing', 'slide');
266 var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
267 var margin = itemWidth * slideCount;
268 var startBox = this.renderThumbnail_(startIndex);
269 if (startIndex == firstIndex) {
270 // Sliding to the right.
271 startBox.style.marginLeft = -margin + 'px';
273 this.insertBefore(startBox, this.firstChild);
275 this.appendChild(startBox);
276 setTimeout(function() {
277 startBox.style.marginLeft = '0';
280 // Sliding to the left. Start item will become invisible and should be
281 // removed afterwards.
282 startBox.setAttribute('vanishing', 'slide');
283 startBox.style.marginLeft = '0';
285 this.insertBefore(startBox, this.firstChild);
287 this.appendChild(startBox);
288 setTimeout(function() {
289 startBox.style.marginLeft = -margin + 'px';
293 ImageUtil.setClass(this, 'fade-left',
294 firstIndex > 0 && selectedIndex != firstIndex);
296 ImageUtil.setClass(this, 'fade-right',
297 lastIndex < length - 1 && selectedIndex != lastIndex);
299 this.firstVisibleIndex_ = firstIndex;
300 this.lastVisibleIndex_ = lastIndex;
302 this.scheduleRemove_();
305 var oldSelected = this.querySelector('[selected]');
307 oldSelected.removeAttribute('selected');
310 this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
312 newSelected.setAttribute('selected', true);
316 * Schedule the removal of thumbnails marked as vanishing.
319 Ribbon.prototype.scheduleRemove_ = function() {
320 if (this.removeTimeout_)
321 clearTimeout(this.removeTimeout_);
323 this.removeTimeout_ = setTimeout(function() {
324 this.removeTimeout_ = null;
325 this.removeVanishing_();
330 * Remove all thumbnails marked as vanishing.
333 Ribbon.prototype.removeVanishing_ = function() {
334 if (this.removeTimeout_) {
335 clearTimeout(this.removeTimeout_);
336 this.removeTimeout_ = 0;
338 var vanishingNodes = this.querySelectorAll('[vanishing]');
339 for (var i = 0; i != vanishingNodes.length; i++) {
340 vanishingNodes[i].removeAttribute('vanishing');
341 this.removeChild(vanishingNodes[i]);
346 * Create a DOM element for a thumbnail.
348 * @param {number} index Item index.
349 * @return {!Element} Newly created element.
352 Ribbon.prototype.renderThumbnail_ = function(index) {
353 var item = assertInstanceof(this.dataModel_.item(index), Gallery.Item);
354 var url = item.getEntry().toURL();
356 var cached = this.renderCache_[url];
358 var img = cached.querySelector('img');
360 img.classList.add('cached');
364 var thumbnail = assertInstanceof(this.ownerDocument.createElement('div'),
366 thumbnail.className = 'ribbon-image';
367 thumbnail.addEventListener('click', function() {
368 var index = this.dataModel_.indexOf(item);
369 this.selectionModel_.unselectAll();
370 this.selectionModel_.setIndexSelected(index, true);
373 util.createChild(thumbnail, 'image-wrapper');
375 this.setThumbnailImage_(thumbnail, item);
377 // TODO: Implement LRU eviction.
378 // Never evict the thumbnails that are currently in the DOM because we rely
379 // on this cache to find them by URL.
380 this.renderCache_[url] = thumbnail;
385 * Set the thumbnail image.
387 * @param {!Element} thumbnail Thumbnail element.
388 * @param {!Gallery.Item} item Gallery item.
391 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
392 var loader = new ThumbnailLoader(
394 ThumbnailLoader.LoaderType.IMAGE,
397 thumbnail.querySelector('.image-wrapper'),
398 ThumbnailLoader.FillMode.FILL /* fill */,
399 ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
403 * Content change handler.
405 * @param {!Event} event Event.
408 Ribbon.prototype.onContentChange_ = function(event) {
409 var url = event.item.getEntry().toURL();
410 if (event.oldEntry.toURL() !== url)
411 this.remapCache_(event.oldEntry.toURL(), url);
413 var thumbnail = this.renderCache_[url];
414 if (thumbnail && event.item)
415 this.setThumbnailImage_(thumbnail, event.item);
419 * Update the thumbnail element cache.
421 * @param {string} oldUrl Old url.
422 * @param {string} newUrl New url.
425 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
426 if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
427 this.renderCache_[newUrl] = this.renderCache_[oldUrl];
428 delete this.renderCache_[oldUrl];