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 var newThumbnail
= this.renderThumbnail_(this.firstVisibleIndex_
);
193 newThumbnail
.style
.marginLeft
= -(this.clientHeight
- 2) + 'px';
194 this.insertBefore(newThumbnail
, this.firstChild
);
195 setTimeout(function() {
196 newThumbnail
.style
.marginLeft
= '0';
203 for (var i
= 0; i
< event
.removed
.length
; i
++) {
204 var removedDom
= this.renderCache_
[event
.removed
[i
].getEntry().toURL()];
206 removedDom
.removeAttribute('selected');
207 removedDom
.setAttribute('vanishing', 'smooth');
213 this.scheduleRemove_();
219 * Selection change handler.
222 Ribbon
.prototype.onSelection_ = function() {
223 var indexes
= this.selectionModel_
.selectedIndexes
;
224 if (indexes
.length
== 0)
225 return; // Ignore temporary empty selection.
226 var selectedIndex
= indexes
[0];
228 var length
= this.dataModel_
.length
;
230 // TODO(dgozman): use margin instead of 2 here.
231 var itemWidth
= this.clientHeight
- 2;
232 var fullItems
= Math
.min(Ribbon
.ITEMS_COUNT
, length
);
233 var right
= Math
.floor((fullItems
- 1) / 2);
235 var fullWidth
= fullItems
* itemWidth
;
236 this.style
.width
= fullWidth
+ 'px';
238 var lastIndex
= selectedIndex
+ right
;
239 lastIndex
= Math
.max(lastIndex
, fullItems
- 1);
240 lastIndex
= Math
.min(lastIndex
, length
- 1);
241 var firstIndex
= lastIndex
- fullItems
+ 1;
243 if (this.firstVisibleIndex_
!= firstIndex
||
244 this.lastVisibleIndex_
!= lastIndex
) {
246 if (this.lastVisibleIndex_
== -1) {
247 this.firstVisibleIndex_
= firstIndex
;
248 this.lastVisibleIndex_
= lastIndex
;
251 this.removeVanishing_();
253 this.textContent
= '';
254 var startIndex
= Math
.min(firstIndex
, this.firstVisibleIndex_
);
255 // All the items except the first one treated equally.
256 for (var index
= startIndex
+ 1;
257 index
<= Math
.max(lastIndex
, this.lastVisibleIndex_
);
259 // Only add items that are in either old or the new viewport.
260 if (this.lastVisibleIndex_
< index
&& index
< firstIndex
||
261 lastIndex
< index
&& index
< this.firstVisibleIndex_
)
263 var box
= this.renderThumbnail_(index
);
264 box
.style
.marginLeft
= '0';
265 this.appendChild(box
);
266 if (index
< firstIndex
|| index
> lastIndex
) {
267 // If the node is not in the new viewport we only need it while
268 // the animation is playing out.
269 box
.setAttribute('vanishing', 'slide');
273 var slideCount
= this.childNodes
.length
+ 1 - Ribbon
.ITEMS_COUNT
;
274 var margin
= itemWidth
* slideCount
;
275 var startBox
= this.renderThumbnail_(startIndex
);
276 if (startIndex
== firstIndex
) {
277 // Sliding to the right.
278 startBox
.style
.marginLeft
= -margin
+ 'px';
280 this.insertBefore(startBox
, this.firstChild
);
282 this.appendChild(startBox
);
283 setTimeout(function() {
284 startBox
.style
.marginLeft
= '0';
287 // Sliding to the left. Start item will become invisible and should be
288 // removed afterwards.
289 startBox
.setAttribute('vanishing', 'slide');
290 startBox
.style
.marginLeft
= '0';
292 this.insertBefore(startBox
, this.firstChild
);
294 this.appendChild(startBox
);
295 setTimeout(function() {
296 startBox
.style
.marginLeft
= -margin
+ 'px';
300 ImageUtil
.setClass(this, 'fade-left',
301 firstIndex
> 0 && selectedIndex
!= firstIndex
);
303 ImageUtil
.setClass(this, 'fade-right',
304 lastIndex
< length
- 1 && selectedIndex
!= lastIndex
);
306 this.firstVisibleIndex_
= firstIndex
;
307 this.lastVisibleIndex_
= lastIndex
;
309 this.scheduleRemove_();
312 var oldSelected
= this.querySelector('[selected]');
314 oldSelected
.removeAttribute('selected');
317 this.renderCache_
[this.dataModel_
.item(selectedIndex
).getEntry().toURL()];
319 newSelected
.setAttribute('selected', true);
323 * Schedule the removal of thumbnails marked as vanishing.
326 Ribbon
.prototype.scheduleRemove_ = function() {
327 if (this.removeTimeout_
)
328 clearTimeout(this.removeTimeout_
);
330 this.removeTimeout_
= setTimeout(function() {
331 this.removeTimeout_
= null;
332 this.removeVanishing_();
337 * Remove all thumbnails marked as vanishing.
340 Ribbon
.prototype.removeVanishing_ = function() {
341 if (this.removeTimeout_
) {
342 clearTimeout(this.removeTimeout_
);
343 this.removeTimeout_
= 0;
345 var vanishingNodes
= this.querySelectorAll('[vanishing]');
346 for (var i
= 0; i
!= vanishingNodes
.length
; i
++) {
347 vanishingNodes
[i
].removeAttribute('vanishing');
348 this.removeChild(vanishingNodes
[i
]);
353 * Create a DOM element for a thumbnail.
355 * @param {number} index Item index.
356 * @return {!Element} Newly created element.
359 Ribbon
.prototype.renderThumbnail_ = function(index
) {
360 var item
= assertInstanceof(this.dataModel_
.item(index
), Gallery
.Item
);
361 var url
= item
.getEntry().toURL();
363 var cached
= this.renderCache_
[url
];
365 var img
= cached
.querySelector('img');
367 img
.classList
.add('cached');
371 var thumbnail
= assertInstanceof(this.ownerDocument
.createElement('div'),
373 thumbnail
.className
= 'ribbon-image';
374 thumbnail
.addEventListener('click', function() {
375 var index
= this.dataModel_
.indexOf(item
);
376 this.selectionModel_
.unselectAll();
377 this.selectionModel_
.setIndexSelected(index
, true);
380 util
.createChild(thumbnail
, 'image-wrapper');
382 this.setThumbnailImage_(thumbnail
, item
);
384 // TODO: Implement LRU eviction.
385 // Never evict the thumbnails that are currently in the DOM because we rely
386 // on this cache to find them by URL.
387 this.renderCache_
[url
] = thumbnail
;
392 * Set the thumbnail image.
394 * @param {!Element} thumbnail Thumbnail element.
395 * @param {!Gallery.Item} item Gallery item.
398 Ribbon
.prototype.setThumbnailImage_ = function(thumbnail
, item
) {
399 this.thumbnailModel_
.get([item
.getEntry()]).then(function(metadataList
) {
400 var loader
= new ThumbnailLoader(
402 ThumbnailLoader
.LoaderType
.IMAGE
,
405 thumbnail
.querySelector('.image-wrapper'),
406 ThumbnailLoader
.FillMode
.FILL
/* fill */,
407 ThumbnailLoader
.OptimizationMode
.NEVER_DISCARD
);
412 * Content change handler.
414 * @param {!Event} event Event.
417 Ribbon
.prototype.onContentChange_ = function(event
) {
418 var url
= event
.item
.getEntry().toURL();
419 if (event
.oldEntry
.toURL() !== url
)
420 this.remapCache_(event
.oldEntry
.toURL(), url
);
422 var thumbnail
= this.renderCache_
[url
];
423 if (thumbnail
&& event
.item
)
424 this.setThumbnailImage_(thumbnail
, event
.item
);
428 * Update the thumbnail element cache.
430 * @param {string} oldUrl Old url.
431 * @param {string} newUrl New url.
434 Ribbon
.prototype.remapCache_ = function(oldUrl
, newUrl
) {
435 if (oldUrl
!= newUrl
&& (oldUrl
in this.renderCache_
)) {
436 this.renderCache_
[newUrl
] = this.renderCache_
[oldUrl
];
437 delete this.renderCache_
[oldUrl
];