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 {!HTMLElement} Ribbon element.
12 * @extends {HTMLElement}
14 * @suppress {checkStructDictInheritance}
17 function Ribbon(document
, dataModel
, selectionModel
) {
18 var self
= assertInstanceof(document
.createElement('div'), HTMLElement
);
19 Ribbon
.decorate(self
, dataModel
, selectionModel
);
24 * Inherit from HTMLDivElement.
26 Ribbon
.prototype.__proto__
= HTMLDivElement
.prototype;
29 * Decorate a Ribbon instance.
31 * @param {!HTMLElement} self Self pointer.
32 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
33 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
35 Ribbon
.decorate = function(self
, dataModel
, selectionModel
) {
36 self
.__proto__
= Ribbon
.prototype;
37 self
= /** @type {!Ribbon} */ (self
);
38 self
.dataModel_
= dataModel
;
39 self
.selectionModel_
= selectionModel
;
41 /** @type {!Object} */
42 self
.renderCache_
= {};
45 self
.firstVisibleIndex_
= 0;
48 self
.lastVisibleIndex_
= -1;
50 /** @type {?function(!Event)} */
51 self
.onContentBound_
= null;
53 /** @type {?function(!Event)} */
54 self
.onSpliceBound_
= null;
56 /** @type {?function(!Event)} */
57 self
.onSelectionBound_
= null;
59 /** @type {?number} */
60 self
.removeTimeout_
= null;
62 self
.className
= 'ribbon';
66 * Max number of thumbnails in the ribbon.
70 Ribbon
.ITEMS_COUNT
= 5;
73 * Force redraw the ribbon.
75 Ribbon
.prototype.redraw = function() {
80 * Clear all cached data to force full redraw on the next selection change.
82 Ribbon
.prototype.reset = function() {
83 this.renderCache_
= {};
84 this.firstVisibleIndex_
= 0;
85 this.lastVisibleIndex_
= -1; // Zero thumbnails
91 Ribbon
.prototype.enable = function() {
92 this.onContentBound_
= this.onContentChange_
.bind(this);
93 this.dataModel_
.addEventListener('content', this.onContentBound_
);
95 this.onSpliceBound_
= this.onSplice_
.bind(this);
96 this.dataModel_
.addEventListener('splice', this.onSpliceBound_
);
98 this.onSelectionBound_
= this.onSelection_
.bind(this);
99 this.selectionModel_
.addEventListener('change', this.onSelectionBound_
);
108 Ribbon
.prototype.disable = function() {
109 this.dataModel_
.removeEventListener('content', this.onContentBound_
);
110 this.dataModel_
.removeEventListener('splice', this.onSpliceBound_
);
111 this.selectionModel_
.removeEventListener('change', this.onSelectionBound_
);
113 this.removeVanishing_();
114 this.textContent
= '';
118 * Data model splice handler.
119 * @param {!Event} event Event.
122 Ribbon
.prototype.onSplice_ = function(event
) {
123 if (event
.removed
.length
> 1) {
124 console
.error('Cannot remove multiple items.');
128 if (event
.removed
.length
> 0 && event
.added
.length
> 0) {
129 console
.error('Replacing is not implemented.');
133 if (event
.added
.length
> 0) {
134 for (var i
= 0; i
< event
.added
.length
; i
++) {
135 var index
= this.dataModel_
.indexOf(event
.added
[i
]);
138 var element
= this.renderThumbnail_(index
);
139 var nextItem
= this.dataModel_
.item(index
+ 1);
141 nextItem
&& this.renderCache_
[nextItem
.getEntry().toURL()];
142 this.insertBefore(element
, nextElement
);
147 var removed
= this.renderCache_
[event
.removed
[0].getEntry().toURL()];
148 if (!removed
|| !removed
.parentNode
|| !removed
.hasAttribute('selected')) {
149 console
.error('Can only remove the selected item');
153 var persistentNodes
= this.querySelectorAll('.ribbon-image:not([vanishing])');
154 if (this.lastVisibleIndex_
< this.dataModel_
.length
) { // Not at the end.
155 var lastNode
= persistentNodes
[persistentNodes
.length
- 1];
156 if (lastNode
.nextSibling
) {
157 // Pull back a vanishing node from the right.
158 lastNode
.nextSibling
.removeAttribute('vanishing');
160 // Push a new item at the right end.
161 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_
));
164 // No items to the right, move the window to the left.
165 this.lastVisibleIndex_
--;
166 if (this.firstVisibleIndex_
) {
167 this.firstVisibleIndex_
--;
168 var firstNode
= persistentNodes
[0];
169 if (firstNode
.previousSibling
) {
170 // Pull back a vanishing node from the left.
171 firstNode
.previousSibling
.removeAttribute('vanishing');
173 // Push a new item at the left end.
174 var newThumbnail
= this.renderThumbnail_(this.firstVisibleIndex_
);
175 newThumbnail
.style
.marginLeft
= -(this.clientHeight
- 2) + 'px';
176 this.insertBefore(newThumbnail
, this.firstChild
);
177 setTimeout(function() {
178 newThumbnail
.style
.marginLeft
= '0';
184 removed
.removeAttribute('selected');
185 removed
.setAttribute('vanishing', 'smooth');
186 this.scheduleRemove_();
190 * Selection change handler.
193 Ribbon
.prototype.onSelection_ = function() {
194 var indexes
= this.selectionModel_
.selectedIndexes
;
195 if (indexes
.length
== 0)
196 return; // Ignore temporary empty selection.
197 var selectedIndex
= indexes
[0];
199 var length
= this.dataModel_
.length
;
201 // TODO(dgozman): use margin instead of 2 here.
202 var itemWidth
= this.clientHeight
- 2;
203 var fullItems
= Math
.min(Ribbon
.ITEMS_COUNT
, length
);
204 var right
= Math
.floor((fullItems
- 1) / 2);
206 var fullWidth
= fullItems
* itemWidth
;
207 this.style
.width
= fullWidth
+ 'px';
209 var lastIndex
= selectedIndex
+ right
;
210 lastIndex
= Math
.max(lastIndex
, fullItems
- 1);
211 lastIndex
= Math
.min(lastIndex
, length
- 1);
212 var firstIndex
= lastIndex
- fullItems
+ 1;
214 if (this.firstVisibleIndex_
!= firstIndex
||
215 this.lastVisibleIndex_
!= lastIndex
) {
217 if (this.lastVisibleIndex_
== -1) {
218 this.firstVisibleIndex_
= firstIndex
;
219 this.lastVisibleIndex_
= lastIndex
;
222 this.removeVanishing_();
224 this.textContent
= '';
225 var startIndex
= Math
.min(firstIndex
, this.firstVisibleIndex_
);
226 // All the items except the first one treated equally.
227 for (var index
= startIndex
+ 1;
228 index
<= Math
.max(lastIndex
, this.lastVisibleIndex_
);
230 // Only add items that are in either old or the new viewport.
231 if (this.lastVisibleIndex_
< index
&& index
< firstIndex
||
232 lastIndex
< index
&& index
< this.firstVisibleIndex_
)
234 var box
= this.renderThumbnail_(index
);
235 box
.style
.marginLeft
= '0';
236 this.appendChild(box
);
237 if (index
< firstIndex
|| index
> lastIndex
) {
238 // If the node is not in the new viewport we only need it while
239 // the animation is playing out.
240 box
.setAttribute('vanishing', 'slide');
244 var slideCount
= this.childNodes
.length
+ 1 - Ribbon
.ITEMS_COUNT
;
245 var margin
= itemWidth
* slideCount
;
246 var startBox
= this.renderThumbnail_(startIndex
);
247 if (startIndex
== firstIndex
) {
248 // Sliding to the right.
249 startBox
.style
.marginLeft
= -margin
+ 'px';
251 this.insertBefore(startBox
, this.firstChild
);
253 this.appendChild(startBox
);
254 setTimeout(function() {
255 startBox
.style
.marginLeft
= '0';
258 // Sliding to the left. Start item will become invisible and should be
259 // removed afterwards.
260 startBox
.setAttribute('vanishing', 'slide');
261 startBox
.style
.marginLeft
= '0';
263 this.insertBefore(startBox
, this.firstChild
);
265 this.appendChild(startBox
);
266 setTimeout(function() {
267 startBox
.style
.marginLeft
= -margin
+ 'px';
271 ImageUtil
.setClass(this, 'fade-left',
272 firstIndex
> 0 && selectedIndex
!= firstIndex
);
274 ImageUtil
.setClass(this, 'fade-right',
275 lastIndex
< length
- 1 && selectedIndex
!= lastIndex
);
277 this.firstVisibleIndex_
= firstIndex
;
278 this.lastVisibleIndex_
= lastIndex
;
280 this.scheduleRemove_();
283 var oldSelected
= this.querySelector('[selected]');
285 oldSelected
.removeAttribute('selected');
288 this.renderCache_
[this.dataModel_
.item(selectedIndex
).getEntry().toURL()];
290 newSelected
.setAttribute('selected', true);
294 * Schedule the removal of thumbnails marked as vanishing.
297 Ribbon
.prototype.scheduleRemove_ = function() {
298 if (this.removeTimeout_
)
299 clearTimeout(this.removeTimeout_
);
301 this.removeTimeout_
= setTimeout(function() {
302 this.removeTimeout_
= null;
303 this.removeVanishing_();
308 * Remove all thumbnails marked as vanishing.
311 Ribbon
.prototype.removeVanishing_ = function() {
312 if (this.removeTimeout_
) {
313 clearTimeout(this.removeTimeout_
);
314 this.removeTimeout_
= 0;
316 var vanishingNodes
= this.querySelectorAll('[vanishing]');
317 for (var i
= 0; i
!= vanishingNodes
.length
; i
++) {
318 vanishingNodes
[i
].removeAttribute('vanishing');
319 this.removeChild(vanishingNodes
[i
]);
324 * Create a DOM element for a thumbnail.
326 * @param {number} index Item index.
327 * @return {!Element} Newly created element.
330 Ribbon
.prototype.renderThumbnail_ = function(index
) {
331 var item
= this.dataModel_
.item(index
);
332 var url
= item
.getEntry().toURL();
334 var cached
= this.renderCache_
[url
];
336 var img
= cached
.querySelector('img');
338 img
.classList
.add('cached');
342 var thumbnail
= this.ownerDocument
.createElement('div');
343 thumbnail
.className
= 'ribbon-image';
344 thumbnail
.addEventListener('click', function() {
345 var index
= this.dataModel_
.indexOf(item
);
346 this.selectionModel_
.unselectAll();
347 this.selectionModel_
.setIndexSelected(index
, true);
350 util
.createChild(thumbnail
, 'image-wrapper');
352 this.setThumbnailImage_(thumbnail
, item
);
354 // TODO: Implement LRU eviction.
355 // Never evict the thumbnails that are currently in the DOM because we rely
356 // on this cache to find them by URL.
357 this.renderCache_
[url
] = thumbnail
;
362 * Set the thumbnail image.
364 * @param {!Element} thumbnail Thumbnail element.
365 * @param {!Gallery.Item} item Gallery item.
368 Ribbon
.prototype.setThumbnailImage_ = function(thumbnail
, item
) {
369 var loader
= new ThumbnailLoader(
371 ThumbnailLoader
.LoaderType
.IMAGE
,
374 thumbnail
.querySelector('.image-wrapper'),
375 ThumbnailLoader
.FillMode
.FILL
/* fill */,
376 ThumbnailLoader
.OptimizationMode
.NEVER_DISCARD
);
380 * Content change handler.
382 * @param {!Event} event Event.
385 Ribbon
.prototype.onContentChange_ = function(event
) {
386 var url
= event
.item
.getEntry().toURL();
387 if (event
.oldEntry
.toURL() !== url
)
388 this.remapCache_(event
.oldEntry
.toURL(), url
);
390 var thumbnail
= this.renderCache_
[url
];
391 if (thumbnail
&& event
.item
)
392 this.setThumbnailImage_(thumbnail
, event
.item
);
396 * Update the thumbnail element cache.
398 * @param {string} oldUrl Old url.
399 * @param {string} newUrl New url.
402 Ribbon
.prototype.remapCache_ = function(oldUrl
, newUrl
) {
403 if (oldUrl
!= newUrl
&& (oldUrl
in this.renderCache_
)) {
404 this.renderCache_
[newUrl
] = this.renderCache_
[oldUrl
];
405 delete this.renderCache_
[oldUrl
];