/** * @class Ext.ux.DataViewTransition * Transition plugin for DataViews */Ext.ux.DataViewTransition = Ext.extend(Object, { /** * @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; 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, dataview.store.getAt(index).get(idProperty)); }, 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 = {}; //var store = dataview.store; //catch the store data with the snapshot immediately this.cacheStoreData(dataview.store.snapshot); dataview.store.on('datachanged', function(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) { Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({ remove : false, duration: duration, opacity : 0, useDisplay: true }); }, this); //store is empty if (calcItem == undefined) { this.cacheStoreData(store); return; } var el = Ext.get(this.dataviewID + "-" + calcItem.get(this.idProperty)); //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); //make sure the correct styles are applied to the parent element parentEl.applyStyles({ display : 'block', position: 'relative' }); //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.get(this.idProperty), el = elCache[id] = Ext.get(this.dataviewID + '-' + id); oldPositions[id] = { top : el.getY() - parentEl.getY() - el.getMargin('t') - parentEl.getPadding('t'), left: el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l') }; }, this); //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", //we set the width here to make ListViews work correctly. This is not needed for DataViews width : el.getWidth(!Ext.isIE || Ext.isStrict), height : el.getHeight(!Ext.isIE || Ext.isStrict) }); } }); //get new positions var index = 0; Ext.iterate(store.data.items, function(item) { var id = item.get(idProperty), 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; if (fraction >= 1) { for (var 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 (var 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" }); } } }; var task = { run : doAnimate, interval: 20, scope : this }; Ext.TaskManager.start(task); //<debug> var count = 0; for (var k in added) { count++; } if (Ext.global.console && Ext.global.console.log) { Ext.global.console.log('added:', count); } //</debug> //show new items Ext.iterate(added, function(id, item) { Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).applyStyles({ top : newPositions[item.get(this.idProperty)].top + "px", left : newPositions[item.get(this.idProperty)].left + "px" }); Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({ remove : false, duration: duration, opacity : 1 }); }, this); this.cacheStoreData(store); }, this); }, /** * 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.get(this.idProperty)] = 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.get(this.idProperty)] == undefined) { added[record.get(this.idProperty)] = 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 = []; for (var id in this.cachedStoreData) { if (store.findExact(this.idProperty, Number(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.get(this.idProperty)] != undefined) { remaining[record.get(this.idProperty)] = record; } }, this); return remaining; }});