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 * @return {Element} Ribbon element.
14 function Ribbon(document, dataModel, selectionModel) {
15 var self = document.createElement('div');
16 Ribbon.decorate(self, dataModel, selectionModel);
21 * Inherit from HTMLDivElement.
23 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
26 * Decorate a Ribbon instance.
28 * @param {Ribbon} self Self pointer.
29 * @param {cr.ui.ArrayDataModel} dataModel Data model.
30 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
32 Ribbon.decorate = function(self, dataModel, selectionModel) {
33 self.__proto__ = Ribbon.prototype;
34 self.dataModel_ = dataModel;
35 self.selectionModel_ = selectionModel;
37 self.className = 'ribbon';
41 * Max number of thumbnails in the ribbon.
44 Ribbon.ITEMS_COUNT = 5;
47 * Force redraw the ribbon.
49 Ribbon.prototype.redraw = function() {
54 * Clear all cached data to force full redraw on the next selection change.
56 Ribbon.prototype.reset = function() {
57 this.renderCache_ = {};
58 this.firstVisibleIndex_ = 0;
59 this.lastVisibleIndex_ = -1; // Zero thumbnails
65 Ribbon.prototype.enable = function() {
66 this.onContentBound_ = this.onContentChange_.bind(this);
67 this.dataModel_.addEventListener('content', this.onContentBound_);
69 this.onSpliceBound_ = this.onSplice_.bind(this);
70 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
72 this.onSelectionBound_ = this.onSelection_.bind(this);
73 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
82 Ribbon.prototype.disable = function() {
83 this.dataModel_.removeEventListener('content', this.onContentBound_);
84 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
85 this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
87 this.removeVanishing_();
88 this.textContent = '';
92 * Data model splice handler.
93 * @param {Event} event Event.
96 Ribbon.prototype.onSplice_ = function(event) {
97 if (event.removed.length > 1) {
98 console.error('Cannot remove multiple items.');
102 if (event.removed.length > 0 && event.added.length > 0) {
103 console.error('Replacing is not implemented.');
107 if (event.added.length > 0) {
108 for (var i = 0; i < event.added.length; i++) {
109 var index = this.dataModel_.indexOf(event.added[i]);
112 var element = this.renderThumbnail_(index);
113 var nextItem = this.dataModel_.item(index + 1);
115 nextItem && this.renderCache_[nextItem.getEntry().toURL()];
116 this.insertBefore(element, nextElement);
121 var removed = this.renderCache_[event.removed[0].getEntry().toURL()];
122 if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) {
123 console.error('Can only remove the selected item');
127 var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
128 if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
129 var lastNode = persistentNodes[persistentNodes.length - 1];
130 if (lastNode.nextSibling) {
131 // Pull back a vanishing node from the right.
132 lastNode.nextSibling.removeAttribute('vanishing');
134 // Push a new item at the right end.
135 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
138 // No items to the right, move the window to the left.
139 this.lastVisibleIndex_--;
140 if (this.firstVisibleIndex_) {
141 this.firstVisibleIndex_--;
142 var firstNode = persistentNodes[0];
143 if (firstNode.previousSibling) {
144 // Pull back a vanishing node from the left.
145 firstNode.previousSibling.removeAttribute('vanishing');
147 // Push a new item at the left end.
148 var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
149 newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
150 this.insertBefore(newThumbnail, this.firstChild);
151 setTimeout(function() {
152 newThumbnail.style.marginLeft = '0';
158 removed.removeAttribute('selected');
159 removed.setAttribute('vanishing', 'smooth');
160 this.scheduleRemove_();
164 * Selection change handler.
167 Ribbon.prototype.onSelection_ = function() {
168 var indexes = this.selectionModel_.selectedIndexes;
169 if (indexes.length == 0)
170 return; // Ignore temporary empty selection.
171 var selectedIndex = indexes[0];
173 var length = this.dataModel_.length;
175 // TODO(dgozman): use margin instead of 2 here.
176 var itemWidth = this.clientHeight - 2;
177 var fullItems = Math.min(Ribbon.ITEMS_COUNT, length);
178 var right = Math.floor((fullItems - 1) / 2);
180 var fullWidth = fullItems * itemWidth;
181 this.style.width = fullWidth + 'px';
183 var lastIndex = selectedIndex + right;
184 lastIndex = Math.max(lastIndex, fullItems - 1);
185 lastIndex = Math.min(lastIndex, length - 1);
186 var firstIndex = lastIndex - fullItems + 1;
188 if (this.firstVisibleIndex_ != firstIndex ||
189 this.lastVisibleIndex_ != lastIndex) {
191 if (this.lastVisibleIndex_ == -1) {
192 this.firstVisibleIndex_ = firstIndex;
193 this.lastVisibleIndex_ = lastIndex;
196 this.removeVanishing_();
198 this.textContent = '';
199 var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
200 // All the items except the first one treated equally.
201 for (var index = startIndex + 1;
202 index <= Math.max(lastIndex, this.lastVisibleIndex_);
204 // Only add items that are in either old or the new viewport.
205 if (this.lastVisibleIndex_ < index && index < firstIndex ||
206 lastIndex < index && index < this.firstVisibleIndex_)
208 var box = this.renderThumbnail_(index);
209 box.style.marginLeft = '0';
210 this.appendChild(box);
211 if (index < firstIndex || index > lastIndex) {
212 // If the node is not in the new viewport we only need it while
213 // the animation is playing out.
214 box.setAttribute('vanishing', 'slide');
218 var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
219 var margin = itemWidth * slideCount;
220 var startBox = this.renderThumbnail_(startIndex);
221 if (startIndex == firstIndex) {
222 // Sliding to the right.
223 startBox.style.marginLeft = -margin + 'px';
225 this.insertBefore(startBox, this.firstChild);
227 this.appendChild(startBox);
228 setTimeout(function() {
229 startBox.style.marginLeft = '0';
232 // Sliding to the left. Start item will become invisible and should be
233 // removed afterwards.
234 startBox.setAttribute('vanishing', 'slide');
235 startBox.style.marginLeft = '0';
237 this.insertBefore(startBox, this.firstChild);
239 this.appendChild(startBox);
240 setTimeout(function() {
241 startBox.style.marginLeft = -margin + 'px';
245 ImageUtil.setClass(this, 'fade-left',
246 firstIndex > 0 && selectedIndex != firstIndex);
248 ImageUtil.setClass(this, 'fade-right',
249 lastIndex < length - 1 && selectedIndex != lastIndex);
251 this.firstVisibleIndex_ = firstIndex;
252 this.lastVisibleIndex_ = lastIndex;
254 this.scheduleRemove_();
257 var oldSelected = this.querySelector('[selected]');
259 oldSelected.removeAttribute('selected');
262 this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
264 newSelected.setAttribute('selected', true);
268 * Schedule the removal of thumbnails marked as vanishing.
271 Ribbon.prototype.scheduleRemove_ = function() {
272 if (this.removeTimeout_)
273 clearTimeout(this.removeTimeout_);
275 this.removeTimeout_ = setTimeout(function() {
276 this.removeTimeout_ = null;
277 this.removeVanishing_();
282 * Remove all thumbnails marked as vanishing.
285 Ribbon.prototype.removeVanishing_ = function() {
286 if (this.removeTimeout_) {
287 clearTimeout(this.removeTimeout_);
288 this.removeTimeout_ = 0;
290 var vanishingNodes = this.querySelectorAll('[vanishing]');
291 for (var i = 0; i != vanishingNodes.length; i++) {
292 vanishingNodes[i].removeAttribute('vanishing');
293 this.removeChild(vanishingNodes[i]);
298 * Create a DOM element for a thumbnail.
300 * @param {number} index Item index.
301 * @return {Element} Newly created element.
304 Ribbon.prototype.renderThumbnail_ = function(index) {
305 var item = this.dataModel_.item(index);
306 var url = item.getEntry().toURL();
308 var cached = this.renderCache_[url];
310 var img = cached.querySelector('img');
312 img.classList.add('cached');
316 var thumbnail = this.ownerDocument.createElement('div');
317 thumbnail.className = 'ribbon-image';
318 thumbnail.addEventListener('click', function() {
319 var index = this.dataModel_.indexOf(item);
320 this.selectionModel_.unselectAll();
321 this.selectionModel_.setIndexSelected(index, true);
324 util.createChild(thumbnail, 'image-wrapper');
326 this.setThumbnailImage_(thumbnail, item);
328 // TODO: Implement LRU eviction.
329 // Never evict the thumbnails that are currently in the DOM because we rely
330 // on this cache to find them by URL.
331 this.renderCache_[url] = thumbnail;
336 * Set the thumbnail image.
338 * @param {Element} thumbnail Thumbnail element.
339 * @param {Gallery.Item} item Gallery item.
342 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
343 var loader = new ThumbnailLoader(
345 ThumbnailLoader.LoaderType.IMAGE,
348 thumbnail.querySelector('.image-wrapper'),
349 ThumbnailLoader.FillMode.FILL /* fill */,
350 ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
354 * Content change handler.
356 * @param {Event} event Event.
359 Ribbon.prototype.onContentChange_ = function(event) {
360 var url = event.item.getEntry().toURL();
361 if (event.oldEntry.toURL() !== url)
362 this.remapCache_(event.oldEntry.toURL(), url);
364 var thumbnail = this.renderCache_[url];
365 if (thumbnail && event.item)
366 this.setThumbnailImage_(thumbnail, event.item);
370 * Update the thumbnail element cache.
372 * @param {string} oldUrl Old url.
373 * @param {string} newUrl New url.
376 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
377 if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
378 this.renderCache_[newUrl] = this.renderCache_[oldUrl];
379 delete this.renderCache_[oldUrl];