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.
8 * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode.
10 * @param {Document} document Document.
11 * @param {cr.ui.ArrayDataModel} dataModel Data model.
12 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
13 * @return {Element} Ribbon element.
16 function Ribbon(document
, dataModel
, selectionModel
) {
17 var self
= document
.createElement('div');
18 Ribbon
.decorate(self
, dataModel
, selectionModel
);
23 * Inherit from HTMLDivElement.
25 Ribbon
.prototype.__proto__
= HTMLDivElement
.prototype;
28 * Decorate a Ribbon instance.
30 * @param {Ribbon} self Self pointer.
31 * @param {cr.ui.ArrayDataModel} dataModel Data model.
32 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
34 Ribbon
.decorate = function(self
, dataModel
, selectionModel
) {
35 self
.__proto__
= Ribbon
.prototype;
36 self
.dataModel_
= dataModel
;
37 self
.selectionModel_
= selectionModel
;
39 self
.className
= 'ribbon';
43 * Max number of thumbnails in the ribbon.
46 Ribbon
.ITEMS_COUNT
= 5;
49 * Force redraw the ribbon.
51 Ribbon
.prototype.redraw = function() {
56 * Clear all cached data to force full redraw on the next selection change.
58 Ribbon
.prototype.reset = function() {
59 this.renderCache_
= {};
60 this.firstVisibleIndex_
= 0;
61 this.lastVisibleIndex_
= -1; // Zero thumbnails
67 Ribbon
.prototype.enable = function() {
68 this.onContentBound_
= this.onContentChange_
.bind(this);
69 this.dataModel_
.addEventListener('content', this.onContentBound_
);
71 this.onSpliceBound_
= this.onSplice_
.bind(this);
72 this.dataModel_
.addEventListener('splice', this.onSpliceBound_
);
74 this.onSelectionBound_
= this.onSelection_
.bind(this);
75 this.selectionModel_
.addEventListener('change', this.onSelectionBound_
);
84 Ribbon
.prototype.disable = function() {
85 this.dataModel_
.removeEventListener('content', this.onContentBound_
);
86 this.dataModel_
.removeEventListener('splice', this.onSpliceBound_
);
87 this.selectionModel_
.removeEventListener('change', this.onSelectionBound_
);
89 this.removeVanishing_();
90 this.textContent
= '';
94 * Data model splice handler.
95 * @param {Event} event Event.
98 Ribbon
.prototype.onSplice_ = function(event
) {
99 if (event
.removed
.length
> 1) {
100 console
.error('Cannot remove multiple items.');
104 if (event
.removed
.length
> 0 && event
.added
.length
> 0) {
105 console
.error('Replacing is not implemented.');
109 if (event
.added
.length
> 0) {
110 for (var i
= 0; i
< event
.added
.length
; i
++) {
111 var index
= this.dataModel_
.indexOf(event
.added
[i
]);
114 var element
= this.renderThumbnail_(index
);
115 var nextItem
= this.dataModel_
.item(index
+ 1);
117 nextItem
&& this.renderCache_
[nextItem
.getEntry().toURL()];
118 this.insertBefore(element
, nextElement
);
123 var removed
= this.renderCache_
[event
.removed
[0].getEntry().toURL()];
124 if (!removed
|| !removed
.parentNode
|| !removed
.hasAttribute('selected')) {
125 console
.error('Can only remove the selected item');
129 var persistentNodes
= this.querySelectorAll('.ribbon-image:not([vanishing])');
130 if (this.lastVisibleIndex_
< this.dataModel_
.length
) { // Not at the end.
131 var lastNode
= persistentNodes
[persistentNodes
.length
- 1];
132 if (lastNode
.nextSibling
) {
133 // Pull back a vanishing node from the right.
134 lastNode
.nextSibling
.removeAttribute('vanishing');
136 // Push a new item at the right end.
137 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_
));
140 // No items to the right, move the window to the left.
141 this.lastVisibleIndex_
--;
142 if (this.firstVisibleIndex_
) {
143 this.firstVisibleIndex_
--;
144 var firstNode
= persistentNodes
[0];
145 if (firstNode
.previousSibling
) {
146 // Pull back a vanishing node from the left.
147 firstNode
.previousSibling
.removeAttribute('vanishing');
149 // Push a new item at the left end.
150 var newThumbnail
= this.renderThumbnail_(this.firstVisibleIndex_
);
151 newThumbnail
.style
.marginLeft
= -(this.clientHeight
- 2) + 'px';
152 this.insertBefore(newThumbnail
, this.firstChild
);
153 setTimeout(function() {
154 newThumbnail
.style
.marginLeft
= '0';
160 removed
.removeAttribute('selected');
161 removed
.setAttribute('vanishing', 'smooth');
162 this.scheduleRemove_();
166 * Selection change handler.
169 Ribbon
.prototype.onSelection_ = function() {
170 var indexes
= this.selectionModel_
.selectedIndexes
;
171 if (indexes
.length
== 0)
172 return; // Ignore temporary empty selection.
173 var selectedIndex
= indexes
[0];
175 var length
= this.dataModel_
.length
;
177 // TODO(dgozman): use margin instead of 2 here.
178 var itemWidth
= this.clientHeight
- 2;
179 var fullItems
= Math
.min(Ribbon
.ITEMS_COUNT
, length
);
180 var right
= Math
.floor((fullItems
- 1) / 2);
182 var fullWidth
= fullItems
* itemWidth
;
183 this.style
.width
= fullWidth
+ 'px';
185 var lastIndex
= selectedIndex
+ right
;
186 lastIndex
= Math
.max(lastIndex
, fullItems
- 1);
187 lastIndex
= Math
.min(lastIndex
, length
- 1);
188 var firstIndex
= lastIndex
- fullItems
+ 1;
190 if (this.firstVisibleIndex_
!= firstIndex
||
191 this.lastVisibleIndex_
!= lastIndex
) {
193 if (this.lastVisibleIndex_
== -1) {
194 this.firstVisibleIndex_
= firstIndex
;
195 this.lastVisibleIndex_
= lastIndex
;
198 this.removeVanishing_();
200 this.textContent
= '';
201 var startIndex
= Math
.min(firstIndex
, this.firstVisibleIndex_
);
202 // All the items except the first one treated equally.
203 for (var index
= startIndex
+ 1;
204 index
<= Math
.max(lastIndex
, this.lastVisibleIndex_
);
206 // Only add items that are in either old or the new viewport.
207 if (this.lastVisibleIndex_
< index
&& index
< firstIndex
||
208 lastIndex
< index
&& index
< this.firstVisibleIndex_
)
210 var box
= this.renderThumbnail_(index
);
211 box
.style
.marginLeft
= '0';
212 this.appendChild(box
);
213 if (index
< firstIndex
|| index
> lastIndex
) {
214 // If the node is not in the new viewport we only need it while
215 // the animation is playing out.
216 box
.setAttribute('vanishing', 'slide');
220 var slideCount
= this.childNodes
.length
+ 1 - Ribbon
.ITEMS_COUNT
;
221 var margin
= itemWidth
* slideCount
;
222 var startBox
= this.renderThumbnail_(startIndex
);
223 if (startIndex
== firstIndex
) {
224 // Sliding to the right.
225 startBox
.style
.marginLeft
= -margin
+ 'px';
227 this.insertBefore(startBox
, this.firstChild
);
229 this.appendChild(startBox
);
230 setTimeout(function() {
231 startBox
.style
.marginLeft
= '0';
234 // Sliding to the left. Start item will become invisible and should be
235 // removed afterwards.
236 startBox
.setAttribute('vanishing', 'slide');
237 startBox
.style
.marginLeft
= '0';
239 this.insertBefore(startBox
, this.firstChild
);
241 this.appendChild(startBox
);
242 setTimeout(function() {
243 startBox
.style
.marginLeft
= -margin
+ 'px';
247 ImageUtil
.setClass(this, 'fade-left',
248 firstIndex
> 0 && selectedIndex
!= firstIndex
);
250 ImageUtil
.setClass(this, 'fade-right',
251 lastIndex
< length
- 1 && selectedIndex
!= lastIndex
);
253 this.firstVisibleIndex_
= firstIndex
;
254 this.lastVisibleIndex_
= lastIndex
;
256 this.scheduleRemove_();
259 var oldSelected
= this.querySelector('[selected]');
261 oldSelected
.removeAttribute('selected');
264 this.renderCache_
[this.dataModel_
.item(selectedIndex
).getEntry().toURL()];
266 newSelected
.setAttribute('selected', true);
270 * Schedule the removal of thumbnails marked as vanishing.
273 Ribbon
.prototype.scheduleRemove_ = function() {
274 if (this.removeTimeout_
)
275 clearTimeout(this.removeTimeout_
);
277 this.removeTimeout_
= setTimeout(function() {
278 this.removeTimeout_
= null;
279 this.removeVanishing_();
284 * Remove all thumbnails marked as vanishing.
287 Ribbon
.prototype.removeVanishing_ = function() {
288 if (this.removeTimeout_
) {
289 clearTimeout(this.removeTimeout_
);
290 this.removeTimeout_
= 0;
292 var vanishingNodes
= this.querySelectorAll('[vanishing]');
293 for (var i
= 0; i
!= vanishingNodes
.length
; i
++) {
294 vanishingNodes
[i
].removeAttribute('vanishing');
295 this.removeChild(vanishingNodes
[i
]);
300 * Create a DOM element for a thumbnail.
302 * @param {number} index Item index.
303 * @return {Element} Newly created element.
306 Ribbon
.prototype.renderThumbnail_ = function(index
) {
307 var item
= this.dataModel_
.item(index
);
308 var url
= item
.getEntry().toURL();
310 var cached
= this.renderCache_
[url
];
312 var img
= cached
.querySelector('img');
314 img
.classList
.add('cached');
318 var thumbnail
= this.ownerDocument
.createElement('div');
319 thumbnail
.className
= 'ribbon-image';
320 thumbnail
.addEventListener('click', function() {
321 var index
= this.dataModel_
.indexOf(item
);
322 this.selectionModel_
.unselectAll();
323 this.selectionModel_
.setIndexSelected(index
, true);
326 util
.createChild(thumbnail
, 'image-wrapper');
328 this.setThumbnailImage_(thumbnail
, item
);
330 // TODO: Implement LRU eviction.
331 // Never evict the thumbnails that are currently in the DOM because we rely
332 // on this cache to find them by URL.
333 this.renderCache_
[url
] = thumbnail
;
338 * Set the thumbnail image.
340 * @param {Element} thumbnail Thumbnail element.
341 * @param {Gallery.Item} item Gallery item.
344 Ribbon
.prototype.setThumbnailImage_ = function(thumbnail
, item
) {
345 var loader
= new ThumbnailLoader(
347 ThumbnailLoader
.LoaderType
.IMAGE
,
350 thumbnail
.querySelector('.image-wrapper'),
351 ThumbnailLoader
.FillMode
.FILL
/* fill */,
352 ThumbnailLoader
.OptimizationMode
.NEVER_DISCARD
);
356 * Content change handler.
358 * @param {Event} event Event.
361 Ribbon
.prototype.onContentChange_ = function(event
) {
362 var url
= event
.item
.getEntry().toURL();
363 if (event
.oldEntry
.toURL() !== url
)
364 this.remapCache_(event
.oldEntry
.toURL(), url
);
366 var thumbnail
= this.renderCache_
[url
];
367 if (thumbnail
&& event
.item
)
368 this.setThumbnailImage_(thumbnail
, event
.item
);
372 * Update the thumbnail element cache.
374 * @param {string} oldUrl Old url.
375 * @param {string} newUrl New url.
378 Ribbon
.prototype.remapCache_ = function(oldUrl
, newUrl
) {
379 if (oldUrl
!= newUrl
&& (oldUrl
in this.renderCache_
)) {
380 this.renderCache_
[newUrl
] = this.renderCache_
[oldUrl
];
381 delete this.renderCache_
[oldUrl
];