Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / ui / file_manager / gallery / js / ribbon.js
blob4fc44b6c58d63bc97a6596e764356785f2218345
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 'use strict';
7 /**
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.
14 * @constructor
16 function Ribbon(document, dataModel, selectionModel) {
17 var self = document.createElement('div');
18 Ribbon.decorate(self, dataModel, selectionModel);
19 return self;
22 /**
23 * Inherit from HTMLDivElement.
25 Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
27 /**
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';
42 /**
43 * Max number of thumbnails in the ribbon.
44 * @type {number}
46 Ribbon.ITEMS_COUNT = 5;
48 /**
49 * Force redraw the ribbon.
51 Ribbon.prototype.redraw = function() {
52 this.onSelection_();
55 /**
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
64 /**
65 * Enable the ribbon.
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_);
77 this.reset();
78 this.redraw();
81 /**
82 * Disable ribbon.
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 = '';
93 /**
94 * Data model splice handler.
95 * @param {Event} event Event.
96 * @private
98 Ribbon.prototype.onSplice_ = function(event) {
99 if (event.removed.length > 1) {
100 console.error('Cannot remove multiple items.');
101 return;
104 if (event.removed.length > 0 && event.added.length > 0) {
105 console.error('Replacing is not implemented.');
106 return;
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]);
112 if (index === -1)
113 continue;
114 var element = this.renderThumbnail_(index);
115 var nextItem = this.dataModel_.item(index + 1);
116 var nextElement =
117 nextItem && this.renderCache_[nextItem.getEntry().toURL()];
118 this.insertBefore(element, nextElement);
120 return;
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');
126 return;
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');
135 } else {
136 // Push a new item at the right end.
137 this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
139 } else {
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');
148 } else {
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';
155 }, 0);
160 removed.removeAttribute('selected');
161 removed.setAttribute('vanishing', 'smooth');
162 this.scheduleRemove_();
166 * Selection change handler.
167 * @private
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_);
205 ++index) {
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_)
209 continue;
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';
226 if (this.firstChild)
227 this.insertBefore(startBox, this.firstChild);
228 else
229 this.appendChild(startBox);
230 setTimeout(function() {
231 startBox.style.marginLeft = '0';
232 }, 0);
233 } else {
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';
238 if (this.firstChild)
239 this.insertBefore(startBox, this.firstChild);
240 else
241 this.appendChild(startBox);
242 setTimeout(function() {
243 startBox.style.marginLeft = -margin + 'px';
244 }, 0);
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]');
260 if (oldSelected)
261 oldSelected.removeAttribute('selected');
263 var newSelected =
264 this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
265 if (newSelected)
266 newSelected.setAttribute('selected', true);
270 * Schedule the removal of thumbnails marked as vanishing.
271 * @private
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_();
280 }.bind(this), 200);
284 * Remove all thumbnails marked as vanishing.
285 * @private
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.
304 * @private
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];
311 if (cached) {
312 var img = cached.querySelector('img');
313 if (img)
314 img.classList.add('cached');
315 return 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);
324 }.bind(this));
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;
334 return thumbnail;
338 * Set the thumbnail image.
340 * @param {Element} thumbnail Thumbnail element.
341 * @param {Gallery.Item} item Gallery item.
342 * @private
344 Ribbon.prototype.setThumbnailImage_ = function(thumbnail, item) {
345 var loader = new ThumbnailLoader(
346 item.getEntry(),
347 ThumbnailLoader.LoaderType.IMAGE,
348 item.getMetadata());
349 loader.load(
350 thumbnail.querySelector('.image-wrapper'),
351 ThumbnailLoader.FillMode.FILL /* fill */,
352 ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
356 * Content change handler.
358 * @param {Event} event Event.
359 * @private
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.
376 * @private
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];