Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / ribbon.js
blob6d6e3a066023e025a1cca4b4a5f5fc24082bec1b
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.
5 /**
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}
13 * @constructor
14 * @suppress {checkStructDictInheritance}
15 * @struct
17 function Ribbon(document, dataModel, selectionModel) {
18 var self = assertInstanceof(document.createElement('div'), HTMLElement);
19 Ribbon.decorate(self, dataModel, selectionModel);
20 return self;
23 /**
24 * Inherit from HTMLDivElement.
26 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
28 /**
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_ = {};
44 /** @type {number} */
45 self.firstVisibleIndex_ = 0;
47 /** @type {number} */
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';
65 /**
66 * Max number of thumbnails in the ribbon.
67 * @type {number}
68 * @const
70 Ribbon.ITEMS_COUNT = 5;
72 /**
73 * Force redraw the ribbon.
75 Ribbon.prototype.redraw = function() {
76 this.onSelection_();
79 /**
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
88 /**
89 * Enable the ribbon.
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_);
101 this.reset();
102 this.redraw();
106 * Disable ribbon.
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.
120 * @private
122 Ribbon.prototype.onSplice_ = function(event) {
123 if (event.removed.length > 1) {
124 console.error('Cannot remove multiple items.');
125 return;
128 if (event.removed.length > 0 && event.added.length > 0) {
129 console.error('Replacing is not implemented.');
130 return;
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]);
136 if (index === -1)
137 continue;
138 var element = this.renderThumbnail_(index);
139 var nextItem = this.dataModel_.item(index + 1);
140 var nextElement =
141 nextItem && this.renderCache_[nextItem.getEntry().toURL()];
142 this.insertBefore(element, nextElement);
144 return;
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');
150 return;
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');
159 } else {
160 // Push a new item at the right end.
161 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
163 } else {
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');
172 } else {
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';
179 }, 0);
184 removed.removeAttribute('selected');
185 removed.setAttribute('vanishing', 'smooth');
186 this.scheduleRemove_();
190 * Selection change handler.
191 * @private
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_);
229 ++index) {
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_)
233 continue;
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';
250 if (this.firstChild)
251 this.insertBefore(startBox, this.firstChild);
252 else
253 this.appendChild(startBox);
254 setTimeout(function() {
255 startBox.style.marginLeft = '0';
256 }, 0);
257 } else {
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';
262 if (this.firstChild)
263 this.insertBefore(startBox, this.firstChild);
264 else
265 this.appendChild(startBox);
266 setTimeout(function() {
267 startBox.style.marginLeft = -margin + 'px';
268 }, 0);
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]');
284 if (oldSelected)
285 oldSelected.removeAttribute('selected');
287 var newSelected =
288 this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
289 if (newSelected)
290 newSelected.setAttribute('selected', true);
294 * Schedule the removal of thumbnails marked as vanishing.
295 * @private
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_();
304 }.bind(this), 200);
308 * Remove all thumbnails marked as vanishing.
309 * @private
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.
328 * @private
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];
335 if (cached) {
336 var img = cached.querySelector('img');
337 if (img)
338 img.classList.add('cached');
339 return 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);
348 }.bind(this));
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;
358 return thumbnail;
362 * Set the thumbnail image.
364 * @param {!Element} thumbnail Thumbnail element.
365 * @param {!Gallery.Item} item Gallery item.
366 * @private
368 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
369 var loader = new ThumbnailLoader(
370 item.getEntry(),
371 ThumbnailLoader.LoaderType.IMAGE,
372 item.getMetadata());
373 loader.load(
374 thumbnail.querySelector('.image-wrapper'),
375 ThumbnailLoader.FillMode.FILL /* fill */,
376 ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
380 * Content change handler.
382 * @param {!Event} event Event.
383 * @private
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.
400 * @private
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];