/** * @author Ed Spencer (http://sencha.com) * Transition plugin for DataViews */ Ext.define('Ext.ux.DataView.Animated', { /** * @property defaults * @type Object * Default configuration options for all DataViewTransition instances */ defaults: { duration : 750, idProperty: 'id' }, /** * Creates the plugin instance, applies defaults * @constructor * @param {Object} config Optional config object */ constructor: function(config) { Ext.apply(this, config || {}, this.defaults); }, /** * Initializes the transition plugin. Overrides the dataview's default refresh function * @param {Ext.view.View} dataview The dataview */ init: function(dataview) { /** * @property dataview * @type Ext.view.View * Reference to the DataView this instance is bound to */ this.dataview = dataview; var idProperty = this.idProperty, store = dataview.store; dataview.blockRefresh = true; dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() { this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) { element.id = element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId); }, this); }, dataview); /** * @property dataviewID * @type String * The string ID of the DataView component. This is used internally when animating child objects */ this.dataviewID = dataview.id; /** * @property cachedStoreData * @type Object * A cache of existing store data, keyed by id. This is used to determine * whether any items were added or removed from the store on data change */ this.cachedStoreData = {}; //catch the store data with the snapshot immediately this.cacheStoreData(store.data || store.snapshot); dataview.on('resize', function() { var store = dataview.store; if (store.getCount() > 0) { // reDraw.call(this, store); } }, this); dataview.store.on('datachanged', reDraw, this); function reDraw(store) { var parentEl = dataview.getTargetEl(), calcItem = store.getAt(0), added = this.getAdded(store), removed = this.getRemoved(store), previous = this.getRemaining(store), existing = Ext.apply({}, previous, added); //hide old items Ext.each(removed, function(item) { var id = this.dataviewID + '-' + item.internalId; Ext.fly(id).animate({ remove : false, duration: duration, opacity : 0, useDisplay: true, callback: function() { Ext.fly(id).setDisplayed(false); } }); }, this); //store is empty if (calcItem == undefined) { this.cacheStoreData(store); return; } this.cacheStoreData(store); var el = Ext.get(this.dataviewID + "-" + calcItem.internalId); //if there is nothing rendered, force a refresh and return. This happens when loading asynchronously (was not //covered correctly in previous versions, which only accepted local data) if (!el) { dataview.refresh(); return true; } //calculate the number of rows and columns we have var itemCount = store.getCount(), itemWidth = el.getMargin('lr') + el.getWidth(), itemHeight = el.getMargin('bt') + el.getHeight(), dvWidth = parentEl.getWidth(), columns = Math.floor(dvWidth / itemWidth), rows = Math.ceil(itemCount / columns), currentRows = Math.ceil(this.getExistingCount() / columns); //stores the current top and left values for each element (discovered below) var oldPositions = {}, newPositions = {}, elCache = {}; //find current positions of each element and save a reference in the elCache Ext.iterate(previous, function(id, item) { var id = item.internalId, el = elCache[id] = Ext.get(this.dataviewID + '-' + id); oldPositions[id] = { top : el.getTop() - parentEl.getTop() - el.getMargin('t') - parentEl.getPadding('t'), left: el.getLeft() - parentEl.getLeft() - el.getMargin('l') - parentEl.getPadding('l') }; }, this); //make sure the correct styles are applied to the parent element parentEl.applyStyles({ display : 'block', position: 'relative' }); //set absolute positioning on all DataView items. We need to set position, left and //top at the same time to avoid any flickering Ext.iterate(previous, function(id, item) { var oldPos = oldPositions[id], el = elCache[id]; if (el.getStyle('position') != 'absolute') { elCache[id].applyStyles({ position: 'absolute', left : oldPos.left + "px", top : oldPos.top + "px" }); } }); //get new positions var index = 0; Ext.iterate(store.data.items, function(item) { var id = item.internalId, el = elCache[id]; var column = index % columns, row = Math.floor(index / columns), top = row * itemHeight, left = column * itemWidth; newPositions[id] = { top : top, left: left }; index ++; }, this); //do the movements var startTime = new Date(), duration = this.duration, dataviewID = this.dataviewID; var doAnimate = function() { var elapsed = new Date() - startTime, fraction = elapsed / duration, id; if (fraction >= 1) { for (id in newPositions) { Ext.fly(dataviewID + '-' + id).applyStyles({ top : newPositions[id].top + "px", left: newPositions[id].left + "px" }); } Ext.TaskManager.stop(task); } else { //move each item for (id in newPositions) { if (!previous[id]) { continue; } var oldPos = oldPositions[id], newPos = newPositions[id], oldTop = oldPos.top, newTop = newPos.top, oldLeft = oldPos.left, newLeft = newPos.left, diffTop = fraction * Math.abs(oldTop - newTop), diffLeft= fraction * Math.abs(oldLeft - newLeft), midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop, midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft; Ext.fly(dataviewID + '-' + id).applyStyles({ top : midTop + "px", left: midLeft + "px" }).setDisplayed(true); } } }; var task = { run : doAnimate, interval: 20, scope : this }; Ext.TaskManager.start(task); //show new items Ext.iterate(added, function(id, item) { Ext.fly(this.dataviewID + '-' + item.internalId).applyStyles({ top : newPositions[item.internalId].top + "px", left : newPositions[item.internalId].left + "px" }).setDisplayed(true); Ext.fly(this.dataviewID + '-' + item.internalId).animate({ remove : false, duration: duration, opacity : 1 }); }, this); this.cacheStoreData(store); } }, /** * Caches the records from a store locally for comparison later * @param {Ext.data.Store} store The store to cache data from */ cacheStoreData: function(store) { this.cachedStoreData = {}; store.each(function(record) { this.cachedStoreData[record.internalId] = record; }, this); }, /** * Returns all records that were already in the DataView * @return {Object} All existing records */ getExisting: function() { return this.cachedStoreData; }, /** * Returns the total number of items that are currently visible in the DataView * @return {Number} The number of existing items */ getExistingCount: function() { var count = 0, items = this.getExisting(); for (var k in items) { count++; } return count; }, /** * Returns all records in the given store that were not already present * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records not already present in the dataview in format {id: record} */ getAdded: function(store) { var added = {}; store.each(function(record) { if (this.cachedStoreData[record.internalId] == undefined) { added[record.internalId] = record; } }, this); return added; }, /** * Returns all records that are present in the DataView but not the new store * @param {Ext.data.Store} store The updated store instance * @return {Array} Array of records that used to be present */ getRemoved: function(store) { var removed = [], id; for (id in this.cachedStoreData) { if (store.findBy(function(record) {return record.internalId == id;}) == -1) { removed.push(this.cachedStoreData[id]); } } return removed; }, /** * Returns all records that are already present and are still present in the new store * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records that are still present from last time in format {id: record} */ getRemaining: function(store) { var remaining = {}; store.each(function(record) { if (this.cachedStoreData[record.internalId] != undefined) { remaining[record.internalId] = record; } }, this); return remaining; } });