OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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. | |
4 | |
5 'use strict'; | |
6 | |
7 /** | |
8 * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode. | |
9 * | |
10 * @param {Document} document Document. | |
11 * @param {MetadataCache} metadataCache MetadataCache instance. | |
12 * @param {cr.ui.ArrayDataModel} dataModel Data model. | |
13 * @param {cr.ui.ListSelectionModel} selectionModel Selection model. | |
14 * @return {Element} Ribbon element. | |
15 * @constructor | |
16 */ | |
17 function Ribbon(document, metadataCache, dataModel, selectionModel) { | |
18 var self = document.createElement('div'); | |
19 Ribbon.decorate(self, metadataCache, dataModel, selectionModel); | |
20 return self; | |
21 } | |
22 | |
23 /** | |
24 * Inherit from HTMLDivElement. | |
25 */ | |
26 Ribbon.prototype.__proto__ = HTMLDivElement.prototype; | |
27 | |
28 /** | |
29 * Decorate a Ribbon instance. | |
30 * | |
31 * @param {Ribbon} self Self pointer. | |
32 * @param {MetadataCache} metadataCache MetadataCache instance. | |
33 * @param {cr.ui.ArrayDataModel} dataModel Data model. | |
34 * @param {cr.ui.ListSelectionModel} selectionModel Selection model. | |
35 */ | |
36 Ribbon.decorate = function(self, metadataCache, dataModel, selectionModel) { | |
37 self.__proto__ = Ribbon.prototype; | |
38 self.metadataCache_ = metadataCache; | |
39 self.dataModel_ = dataModel; | |
40 self.selectionModel_ = selectionModel; | |
41 | |
42 self.className = 'ribbon'; | |
43 }; | |
44 | |
45 /** | |
46 * Max number of thumbnails in the ribbon. | |
47 * @type {number} | |
48 */ | |
49 Ribbon.ITEMS_COUNT = 5; | |
50 | |
51 /** | |
52 * Force redraw the ribbon. | |
53 */ | |
54 Ribbon.prototype.redraw = function() { | |
55 this.onSelection_(); | |
56 }; | |
57 | |
58 /** | |
59 * Clear all cached data to force full redraw on the next selection change. | |
60 */ | |
61 Ribbon.prototype.reset = function() { | |
62 this.renderCache_ = {}; | |
63 this.firstVisibleIndex_ = 0; | |
64 this.lastVisibleIndex_ = -1; // Zero thumbnails | |
65 }; | |
66 | |
67 /** | |
68 * Enable the ribbon. | |
69 */ | |
70 Ribbon.prototype.enable = function() { | |
71 this.onContentBound_ = this.onContentChange_.bind(this); | |
72 this.dataModel_.addEventListener('content', this.onContentBound_); | |
73 | |
74 this.onSpliceBound_ = this.onSplice_.bind(this); | |
75 this.dataModel_.addEventListener('splice', this.onSpliceBound_); | |
76 | |
77 this.onSelectionBound_ = this.onSelection_.bind(this); | |
78 this.selectionModel_.addEventListener('change', this.onSelectionBound_); | |
79 | |
80 this.reset(); | |
81 this.redraw(); | |
82 }; | |
83 | |
84 /** | |
85 * Disable ribbon. | |
86 */ | |
87 Ribbon.prototype.disable = function() { | |
88 this.dataModel_.removeEventListener('content', this.onContentBound_); | |
89 this.dataModel_.removeEventListener('splice', this.onSpliceBound_); | |
90 this.selectionModel_.removeEventListener('change', this.onSelectionBound_); | |
91 | |
92 this.removeVanishing_(); | |
93 this.textContent = ''; | |
94 }; | |
95 | |
96 /** | |
97 * Data model splice handler. | |
98 * @param {Event} event Event. | |
99 * @private | |
100 */ | |
101 Ribbon.prototype.onSplice_ = function(event) { | |
102 if (event.removed.length == 0) | |
103 return; | |
104 | |
105 if (event.removed.length > 1) { | |
106 console.error('Cannot remove multiple items'); | |
107 return; | |
108 } | |
109 | |
110 var removed = this.renderCache_[event.removed[0].getUrl()]; | |
111 if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) { | |
112 console.error('Can only remove the selected item'); | |
113 return; | |
114 } | |
115 | |
116 var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])'); | |
117 if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end. | |
118 var lastNode = persistentNodes[persistentNodes.length - 1]; | |
119 if (lastNode.nextSibling) { | |
120 // Pull back a vanishing node from the right. | |
121 lastNode.nextSibling.removeAttribute('vanishing'); | |
122 } else { | |
123 // Push a new item at the right end. | |
124 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_)); | |
125 } | |
126 } else { | |
127 // No items to the right, move the window to the left. | |
128 this.lastVisibleIndex_--; | |
129 if (this.firstVisibleIndex_) { | |
130 this.firstVisibleIndex_--; | |
131 var firstNode = persistentNodes[0]; | |
132 if (firstNode.previousSibling) { | |
133 // Pull back a vanishing node from the left. | |
134 firstNode.previousSibling.removeAttribute('vanishing'); | |
135 } else { | |
136 // Push a new item at the left end. | |
137 var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_); | |
138 newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px'; | |
139 this.insertBefore(newThumbnail, this.firstChild); | |
140 setTimeout(function() { | |
141 newThumbnail.style.marginLeft = '0'; | |
142 }, 0); | |
143 } | |
144 } | |
145 } | |
146 | |
147 removed.removeAttribute('selected'); | |
148 removed.setAttribute('vanishing', 'smooth'); | |
149 this.scheduleRemove_(); | |
150 }; | |
151 | |
152 /** | |
153 * Selection change handler. | |
154 * @private | |
155 */ | |
156 Ribbon.prototype.onSelection_ = function() { | |
157 var indexes = this.selectionModel_.selectedIndexes; | |
158 if (indexes.length == 0) | |
159 return; // Ignore temporary empty selection. | |
160 var selectedIndex = indexes[0]; | |
161 | |
162 var length = this.dataModel_.length; | |
163 | |
164 // TODO(dgozman): use margin instead of 2 here. | |
165 var itemWidth = this.clientHeight - 2; | |
166 var fullItems = Ribbon.ITEMS_COUNT; | |
167 fullItems = Math.min(fullItems, length); | |
168 var right = Math.floor((fullItems - 1) / 2); | |
169 | |
170 var fullWidth = fullItems * itemWidth; | |
171 this.style.width = fullWidth + 'px'; | |
172 | |
173 var lastIndex = selectedIndex + right; | |
174 lastIndex = Math.max(lastIndex, fullItems - 1); | |
175 lastIndex = Math.min(lastIndex, length - 1); | |
176 var firstIndex = lastIndex - fullItems + 1; | |
177 | |
178 if (this.firstVisibleIndex_ != firstIndex || | |
179 this.lastVisibleIndex_ != lastIndex) { | |
180 | |
181 if (this.lastVisibleIndex_ == -1) { | |
182 this.firstVisibleIndex_ = firstIndex; | |
183 this.lastVisibleIndex_ = lastIndex; | |
184 } | |
185 | |
186 this.removeVanishing_(); | |
187 | |
188 this.textContent = ''; | |
189 var startIndex = Math.min(firstIndex, this.firstVisibleIndex_); | |
190 // All the items except the first one treated equally. | |
191 for (var index = startIndex + 1; | |
192 index <= Math.max(lastIndex, this.lastVisibleIndex_); | |
193 ++index) { | |
194 // Only add items that are in either old or the new viewport. | |
195 if (this.lastVisibleIndex_ < index && index < firstIndex || | |
196 lastIndex < index && index < this.firstVisibleIndex_) | |
197 continue; | |
198 var box = this.renderThumbnail_(index); | |
199 box.style.marginLeft = '0'; | |
200 this.appendChild(box); | |
201 if (index < firstIndex || index > lastIndex) { | |
202 // If the node is not in the new viewport we only need it while | |
203 // the animation is playing out. | |
204 box.setAttribute('vanishing', 'slide'); | |
205 } | |
206 } | |
207 | |
208 var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT; | |
209 var margin = itemWidth * slideCount; | |
210 var startBox = this.renderThumbnail_(startIndex); | |
211 if (startIndex == firstIndex) { | |
212 // Sliding to the right. | |
213 startBox.style.marginLeft = -margin + 'px'; | |
214 if (this.firstChild) | |
215 this.insertBefore(startBox, this.firstChild); | |
216 else | |
217 this.appendChild(startBox); | |
218 setTimeout(function() { | |
219 startBox.style.marginLeft = '0'; | |
220 }, 0); | |
221 } else { | |
222 // Sliding to the left. Start item will become invisible and should be | |
223 // removed afterwards. | |
224 startBox.setAttribute('vanishing', 'slide'); | |
225 startBox.style.marginLeft = '0'; | |
226 if (this.firstChild) | |
227 this.insertBefore(startBox, this.firstChild); | |
228 else | |
229 this.appendChild(startBox); | |
230 setTimeout(function() { | |
231 startBox.style.marginLeft = -margin + 'px'; | |
232 }, 0); | |
233 } | |
234 | |
235 ImageUtil.setClass(this, 'fade-left', | |
236 firstIndex > 0 && selectedIndex != firstIndex); | |
237 | |
238 ImageUtil.setClass(this, 'fade-right', | |
239 lastIndex < length - 1 && selectedIndex != lastIndex); | |
240 | |
241 this.firstVisibleIndex_ = firstIndex; | |
242 this.lastVisibleIndex_ = lastIndex; | |
243 | |
244 this.scheduleRemove_(); | |
245 } | |
246 | |
247 var oldSelected = this.querySelector('[selected]'); | |
248 if (oldSelected) oldSelected.removeAttribute('selected'); | |
249 | |
250 var newSelected = | |
251 this.renderCache_[this.dataModel_.item(selectedIndex).getUrl()]; | |
252 if (newSelected) newSelected.setAttribute('selected', true); | |
253 }; | |
254 | |
255 /** | |
256 * Schedule the removal of thumbnails marked as vanishing. | |
257 * @private | |
258 */ | |
259 Ribbon.prototype.scheduleRemove_ = function() { | |
260 if (this.removeTimeout_) | |
261 clearTimeout(this.removeTimeout_); | |
262 | |
263 this.removeTimeout_ = setTimeout(function() { | |
264 this.removeTimeout_ = null; | |
265 this.removeVanishing_(); | |
266 }.bind(this), 200); | |
267 }; | |
268 | |
269 /** | |
270 * Remove all thumbnails marked as vanishing. | |
271 * @private | |
272 */ | |
273 Ribbon.prototype.removeVanishing_ = function() { | |
274 if (this.removeTimeout_) { | |
275 clearTimeout(this.removeTimeout_); | |
276 this.removeTimeout_ = 0; | |
277 } | |
278 var vanishingNodes = this.querySelectorAll('[vanishing]'); | |
279 for (var i = 0; i != vanishingNodes.length; i++) { | |
280 vanishingNodes[i].removeAttribute('vanishing'); | |
281 this.removeChild(vanishingNodes[i]); | |
282 } | |
283 }; | |
284 | |
285 /** | |
286 * Create a DOM element for a thumbnail. | |
287 * | |
288 * @param {number} index Item index. | |
289 * @return {Element} Newly created element. | |
290 * @private | |
291 */ | |
292 Ribbon.prototype.renderThumbnail_ = function(index) { | |
293 var item = this.dataModel_.item(index); | |
294 var url = item.getUrl(); | |
295 | |
296 var cached = this.renderCache_[url]; | |
297 if (cached) { | |
298 var img = cached.querySelector('img'); | |
299 if (img) | |
300 img.classList.add('cached'); | |
301 return cached; | |
302 } | |
303 | |
304 var thumbnail = this.ownerDocument.createElement('div'); | |
305 thumbnail.className = 'ribbon-image'; | |
306 thumbnail.addEventListener('click', function() { | |
307 var index = this.dataModel_.indexOf(item); | |
308 this.selectionModel_.unselectAll(); | |
309 this.selectionModel_.setIndexSelected(index, true); | |
310 }.bind(this)); | |
311 | |
312 util.createChild(thumbnail, 'image-wrapper'); | |
313 | |
314 this.metadataCache_.get(url, Gallery.METADATA_TYPE, | |
315 this.setThumbnailImage_.bind(this, thumbnail, url)); | |
316 | |
317 // TODO: Implement LRU eviction. | |
318 // Never evict the thumbnails that are currently in the DOM because we rely | |
319 // on this cache to find them by URL. | |
320 this.renderCache_[url] = thumbnail; | |
321 return thumbnail; | |
322 }; | |
323 | |
324 /** | |
325 * Set the thumbnail image. | |
326 * | |
327 * @param {Element} thumbnail Thumbnail element. | |
328 * @param {string} url Image url. | |
329 * @param {Object} metadata Metadata. | |
330 * @private | |
331 */ | |
332 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, url, metadata) { | |
333 new ThumbnailLoader(url, ThumbnailLoader.LoaderType.IMAGE, metadata).load( | |
334 thumbnail.querySelector('.image-wrapper'), | |
335 ThumbnailLoader.FillMode.FILL /* fill */, | |
336 ThumbnailLoader.OptimizationMode.NEVER_DISCARD); | |
337 }; | |
338 | |
339 /** | |
340 * Content change handler. | |
341 * | |
342 * @param {Event} event Event. | |
343 * @private | |
344 */ | |
345 Ribbon.prototype.onContentChange_ = function(event) { | |
346 var url = event.item.getUrl(); | |
347 this.remapCache_(event.oldUrl, url); | |
348 | |
349 var thumbnail = this.renderCache_[url]; | |
350 if (thumbnail && event.metadata) | |
351 this.setThumbnailImage_(thumbnail, url, event.metadata); | |
352 }; | |
353 | |
354 /** | |
355 * Update the thumbnail element cache. | |
356 * | |
357 * @param {string} oldUrl Old url. | |
358 * @param {string} newUrl New url. | |
359 * @private | |
360 */ | |
361 Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) { | |
362 if (oldUrl != newUrl && (oldUrl in this.renderCache_)) { | |
363 this.renderCache_[newUrl] = this.renderCache_[oldUrl]; | |
364 delete this.renderCache_[oldUrl]; | |
365 } | |
366 }; | |
OLD | NEW |