Index: chrome/browser/resources/file_manager/js/photo/ribbon.js |
diff --git a/chrome/browser/resources/file_manager/js/photo/ribbon.js b/chrome/browser/resources/file_manager/js/photo/ribbon.js |
index 5e5b88b8d6a199fff1ecd5c0f4726aa59d7c02c9..8322ed4b37f8ee9af255923e451a5a2467cb1c81 100644 |
--- a/chrome/browser/resources/file_manager/js/photo/ribbon.js |
+++ b/chrome/browser/resources/file_manager/js/photo/ribbon.js |
@@ -7,13 +7,14 @@ |
* |
* @param {Document} document Document. |
* @param {MetadataCache} metadataCache MetadataCache instance. |
- * @param {function(number)} selectFunc Selection function. |
+ * @param {cr.ui.ArrayDataModel} dataModel Data model. |
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model. |
* @return {Element} Ribbon element. |
* @constructor |
*/ |
-function Ribbon(document, metadataCache, selectFunc) { |
+function Ribbon(document, metadataCache, dataModel, selectionModel) { |
var self = document.createElement('div'); |
- Ribbon.decorate(self, metadataCache, selectFunc); |
+ Ribbon.decorate(self, metadataCache, dataModel, selectionModel); |
return self; |
} |
@@ -27,18 +28,17 @@ Ribbon.prototype.__proto__ = HTMLDivElement.prototype; |
* |
* @param {Ribbon} self Self pointer. |
* @param {MetadataCache} metadataCache MetadataCache instance. |
- * @param {function(number)} selectFunc Selection function. |
+ * @param {cr.ui.ArrayDataModel} dataModel Data model. |
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model. |
*/ |
-Ribbon.decorate = function(self, metadataCache, selectFunc) { |
+Ribbon.decorate = function(self, metadataCache, dataModel, selectionModel) { |
self.__proto__ = Ribbon.prototype; |
self.metadataCache_ = metadataCache; |
- self.selectFunc_ = selectFunc; |
+ self.dataModel_ = dataModel; |
+ self.selectionModel_ = selectionModel; |
self.className = 'ribbon'; |
- self.firstVisibleIndex_ = 0; |
- self.lastVisibleIndex_ = -1; // Zero thumbnails |
- |
self.renderCache_ = {}; |
}; |
@@ -49,20 +49,104 @@ Ribbon.decorate = function(self, metadataCache, selectFunc) { |
Ribbon.ITEMS_COUNT = 5; |
/** |
- * Update the ribbon. |
- * |
- * @param {Array.<Gallery.Item>} items Array of items. |
- * @param {number} selectedIndex Selected index. |
+ * Enable the ribbon. |
+ */ |
+Ribbon.prototype.enable = function() { |
+ this.firstVisibleIndex_ = 0; |
+ this.lastVisibleIndex_ = -1; // Zero thumbnails |
+ |
+ this.onSpliceBound_ = this.onSplice_.bind(this); |
+ this.dataModel_.addEventListener('splice', this.onSpliceBound_); |
+ |
+ this.onSelectionBound_ = this.onSelection_.bind(this); |
+ this.selectionModel_.addEventListener('change', this.onSelectionBound_); |
+ |
+ this.onSelection_(); |
+}; |
+ |
+/** |
+ * Disable ribbon. |
+ */ |
+Ribbon.prototype.disable = function() { |
+ this.dataModel_.removeEventListener('splice', this.onSpliceBound_); |
+ this.selectionModel_.removeEventListener('change', this.onSelectionBound_); |
+ |
+ this.removeVanishing_(); |
+ this.textContent = ''; |
+}; |
+ |
+/** |
+ * Data model splice handler. |
+ * @param {Event} event Event. |
+ * @private |
*/ |
-Ribbon.prototype.update = function(items, selectedIndex) { |
- // Never show a single thumbnail. |
- if (items.length == 1) |
+Ribbon.prototype.onSplice_ = function(event) { |
+ if (event.removed.length == 0) |
+ return; |
+ |
+ if (event.removed.length > 1) { |
+ console.error('Cannot remove multiple items'); |
return; |
+ } |
+ |
+ var removed = this.renderCache_[event.removed[0].getUrl()]; |
+ if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) { |
+ console.error('Can only remove the selected item'); |
+ return; |
+ } |
+ |
+ var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])'); |
+ if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end. |
+ var lastNode = persistentNodes[persistentNodes.length - 1]; |
+ if (lastNode.nextSibling) { |
+ // Pull back a vanishing node from the right. |
+ lastNode.nextSibling.removeAttribute('vanishing'); |
+ } else { |
+ // Push a new item at the right end. |
+ this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_)); |
+ } |
+ } else { |
+ // No items to the right, move the window to the left. |
+ this.lastVisibleIndex_--; |
+ if (this.firstVisibleIndex_) { |
+ this.firstVisibleIndex_--; |
+ var firstNode = persistentNodes[0]; |
+ if (firstNode.previousSibling) { |
+ // Pull back a vanishing node from the left. |
+ firstNode.previousSibling.removeAttribute('vanishing'); |
+ } else { |
+ // Push a new item at the left end. |
+ var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_); |
+ newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px'; |
+ this.insertBefore(newThumbnail, this.firstChild); |
+ setTimeout(function() { |
+ newThumbnail.style.marginLeft = '0'; |
+ }, 0); |
+ } |
+ } |
+ } |
+ |
+ removed.removeAttribute('selected'); |
+ removed.setAttribute('vanishing', 'smooth'); |
+ this.scheduleRemove_(); |
+}; |
+ |
+/** |
+ * Selection change handler. |
+ * @private |
+ */ |
+Ribbon.prototype.onSelection_ = function() { |
+ var indexes = this.selectionModel_.selectedIndexes; |
+ if (indexes.length == 0) |
+ return; // Ignore temporary empty selection. |
+ var selectedIndex = indexes[0]; |
+ |
+ var length = this.dataModel_.length; |
// TODO(dgozman): use margin instead of 2 here. |
var itemWidth = this.clientHeight - 2; |
var fullItems = Ribbon.ITEMS_COUNT; |
- fullItems = Math.min(fullItems, items.length); |
+ fullItems = Math.min(fullItems, length); |
var right = Math.floor((fullItems - 1) / 2); |
var fullWidth = fullItems * itemWidth; |
@@ -70,7 +154,7 @@ Ribbon.prototype.update = function(items, selectedIndex) { |
var lastIndex = selectedIndex + right; |
lastIndex = Math.max(lastIndex, fullItems - 1); |
- lastIndex = Math.min(lastIndex, items.length - 1); |
+ lastIndex = Math.min(lastIndex, length - 1); |
var firstIndex = lastIndex - fullItems + 1; |
if (this.firstVisibleIndex_ != firstIndex || |
@@ -81,23 +165,31 @@ Ribbon.prototype.update = function(items, selectedIndex) { |
this.lastVisibleIndex_ = lastIndex; |
} |
+ this.removeVanishing_(); |
+ |
this.textContent = ''; |
var startIndex = Math.min(firstIndex, this.firstVisibleIndex_); |
- var toRemove = []; |
// All the items except the first one treated equally. |
for (var index = startIndex + 1; |
index <= Math.max(lastIndex, this.lastVisibleIndex_); |
++index) { |
- var box = this.renderThumbnail_(index, items); |
+ // Only add items that are in either old or the new viewport. |
+ if (this.lastVisibleIndex_ < index && index < firstIndex || |
+ lastIndex < index && index < this.firstVisibleIndex_) |
+ continue; |
+ var box = this.renderThumbnail_(index); |
box.style.marginLeft = '0'; |
this.appendChild(box); |
if (index < firstIndex || index > lastIndex) { |
- toRemove.push(box); |
+ // If the node is not in the new viewport we only need it while |
+ // the animation is playing out. |
+ box.setAttribute('vanishing', 'slide'); |
} |
} |
- var margin = itemWidth * Math.abs(firstIndex - this.firstVisibleIndex_); |
- var startBox = this.renderThumbnail_(startIndex, items); |
+ var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT; |
+ var margin = itemWidth * slideCount; |
+ var startBox = this.renderThumbnail_(startIndex); |
if (startIndex == firstIndex) { |
// Sliding to the right. |
startBox.style.marginLeft = -margin + 'px'; |
@@ -111,7 +203,7 @@ Ribbon.prototype.update = function(items, selectedIndex) { |
} else { |
// Sliding to the left. Start item will become invisible and should be |
// removed afterwards. |
- toRemove.push(startBox); |
+ startBox.setAttribute('vanishing', 'slide'); |
startBox.style.marginLeft = '0'; |
if (this.firstChild) |
this.insertBefore(startBox, this.firstChild); |
@@ -126,41 +218,62 @@ Ribbon.prototype.update = function(items, selectedIndex) { |
firstIndex > 0 && selectedIndex != firstIndex); |
ImageUtil.setClass(this, 'fade-right', |
- lastIndex < items.length - 1 && selectedIndex != lastIndex); |
+ lastIndex < length - 1 && selectedIndex != lastIndex); |
this.firstVisibleIndex_ = firstIndex; |
this.lastVisibleIndex_ = lastIndex; |
- if (this.removeTimeout_) |
- clearTimeout(this.removeTimeout_); |
- |
- this.removeTimeout_ = setTimeout(function() { |
- this.removeTimeout_ = null; |
- for (var i = 0; i < toRemove.length; i++) { |
- var box = toRemove[i]; |
- if (box.parentNode == this) |
- this.removeChild(box); |
- } |
- }.bind(this), 200); |
+ this.scheduleRemove_(); |
} |
var oldSelected = this.querySelector('[selected]'); |
if (oldSelected) oldSelected.removeAttribute('selected'); |
- var newSelected = this.getThumbnail_(selectedIndex); |
+ var newSelected = |
+ this.renderCache_[this.dataModel_.item(selectedIndex).getUrl()]; |
if (newSelected) newSelected.setAttribute('selected', true); |
}; |
/** |
+ * Schedule the removal of thumbnails marked as vanishing. |
+ * @private |
+ */ |
+Ribbon.prototype.scheduleRemove_ = function() { |
+ if (this.removeTimeout_) |
+ clearTimeout(this.removeTimeout_); |
+ |
+ this.removeTimeout_ = setTimeout(function() { |
+ this.removeTimeout_ = null; |
+ this.removeVanishing_(); |
+ }.bind(this), 200); |
+}; |
+ |
+/** |
+ * Remove all thumbnails marked as vanishing. |
+ * @private |
+ */ |
+Ribbon.prototype.removeVanishing_ = function() { |
+ if (this.removeTimeout_) { |
+ clearTimeout(this.removeTimeout_); |
+ this.removeTimeout_ = 0; |
+ } |
+ var vanishingNodes = this.querySelectorAll('[vanishing]'); |
+ for (var i = 0; i != vanishingNodes.length; i++) { |
+ vanishingNodes[i].removeAttribute('vanishing'); |
+ this.removeChild(vanishingNodes[i]); |
+ } |
+}; |
+ |
+/** |
* Create a DOM element for a thumbnail. |
* |
* @param {number} index Item index. |
- * @param {Array.<Gallery.Item>} items Items array. |
* @return {Element} Newly created element. |
* @private |
*/ |
-Ribbon.prototype.renderThumbnail_ = function(index, items) { |
- var url = items[index].getUrl(); |
+Ribbon.prototype.renderThumbnail_ = function(index) { |
+ var item = this.dataModel_.item(index); |
+ var url = item.getUrl(); |
var cached = this.renderCache_[url]; |
if (cached) |
@@ -168,14 +281,20 @@ Ribbon.prototype.renderThumbnail_ = function(index, items) { |
var thumbnail = this.ownerDocument.createElement('div'); |
thumbnail.className = 'ribbon-image'; |
- thumbnail.addEventListener('click', this.selectFunc_.bind(null, index)); |
- thumbnail.setAttribute('index', index); |
+ thumbnail.addEventListener('click', function() { |
+ var index = this.dataModel_.slice().indexOf(item); |
+ this.selectionModel_.unselectAll(); |
+ this.selectionModel_.setIndexSelected(index, true); |
+ }.bind(this)); |
util.createChild(thumbnail, 'image-wrapper'); |
this.metadataCache_.get(url, Gallery.METADATA_TYPE, |
this.setThumbnailImage_.bind(this, thumbnail, url)); |
+ // TODO: Implement LRU eviction. |
+ // Never evict the thumbnails that are currently in the DOM because we rely |
+ // on this cache to find them by URL. |
this.renderCache_[url] = thumbnail; |
return thumbnail; |
}; |
@@ -196,26 +315,16 @@ Ribbon.prototype.setThumbnailImage_ = function(thumbnail, url, metadata) { |
/** |
* Update the thumbnail image. |
* |
- * @param {number} index Item index. |
* @param {string} url Image url. |
* @param {Object} metadata Metadata. |
*/ |
-Ribbon.prototype.updateThumbnail = function(index, url, metadata) { |
- var thumbnail = this.getThumbnail_(index) || this.renderCache_[url]; |
+Ribbon.prototype.updateThumbnail = function(url, metadata) { |
+ var thumbnail = this.renderCache_[url]; |
if (thumbnail) |
this.setThumbnailImage_(thumbnail, url, metadata); |
}; |
/** |
- * @param {number} index Thumbnail index. |
- * @return {Element} Thumbnail element or null if not rendered. |
- * @private |
- */ |
-Ribbon.prototype.getThumbnail_ = function(index) { |
- return this.querySelector('[index="' + index + '"]'); |
-}; |
- |
-/** |
* Update the thumbnail element cache. |
* |
* @param {string} oldUrl Old url. |