Chromium Code Reviews| 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..aeda8bceb2aeaab49b168b5fe08f51b7bd755b4c 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])'); |
|
dgozman
2012/08/27 15:07:43
ribbon-image is a class, right? Forgot the dot.
Vladislav Kaznacheev
2012/08/30 09:58:45
Done.
|
| + 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. |