/** * @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 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); // Not yet rendered if (!parentEl) { return; } //make sure the correct styles are applied to the parent element if (Ext.isIEQuirks) { parentEl.applyStyles({ zoom: 1, display : 'block', position: 'relative' }); } //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 itemWidth = el.getMargin('lr') + el.getWidth(), itemHeight = el.getMargin('bt') + el.getHeight(), dvWidth = parentEl.dom.clientWidth, columns = Math.floor(dvWidth / itemWidth), rtl = this.dataview.getHierarchyState().rtl, styleSide = rtl ? 'right' : 'left', newStyle; //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.getY() - parentEl.getY() - el.getMargin('t') - parentEl.getPadding('t') }; oldPositions[id][styleSide] = this.getItemX(el); }, 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') { newStyle = { position: 'absolute', top : oldPos.top + "px" }; newStyle[styleSide] = oldPos[styleSide] + "px"; elCache[id].applyStyles(newStyle); } }); //get new positions var index = 0; Ext.iterate(store.data.items, function(item) { var id = item.internalId, column = index % columns, row = Math.floor(index / columns), top = row * itemHeight, left = column * itemWidth; newPositions[id] = { top : top }; newPositions[id][styleSide] = 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) { newStyle = { top : newPositions[id].top + "px" }; newStyle[styleSide] = newPositions[id][styleSide] + "px"; Ext.fly(dataviewID + '-' + id).applyStyles(newStyle); } 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[styleSide], newLeft = newPos[styleSide], 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; newStyle = { top : midTop + "px" }; newStyle[styleSide] = midLeft + "px"; Ext.fly(dataviewID + '-' + id).applyStyles(newStyle).setDisplayed(true); } } }; var task = { run : doAnimate, interval: 20, scope : this }; Ext.TaskManager.start(task); //show new items Ext.iterate(added, function(id, item) { newStyle = { top : newPositions[item.internalId].top + "px" }; newStyle[styleSide] = newPositions[item.internalId][styleSide] + "px"; Ext.fly(this.dataviewID + '-' + item.internalId).applyStyles(newStyle).setDisplayed(true); Ext.fly(this.dataviewID + '-' + item.internalId).animate({ remove : false, duration: duration, opacity : 1 }); }, this); this.cacheStoreData(store); } }, getItemX: function(el) { var rtl = this.dataview.getHierarchyState().rtl, parentEl = el.up(''); if (rtl) { return parentEl.getViewRegion().right - el.getRegion().right + el.getMargin('r'); } else { return el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l'); } }, /** * 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; } });