/** * Transition plugin for DataViews */Ext.define('Ext.ux.DataView.Animated', { alias: 'plugin.ux-animated-dataview', /** * @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) { var me = this, store = dataview.store, items = dataview.all, task = { interval: 20 }, duration = me.duration; /** * @property dataview * @type Ext.view.View * Reference to the DataView this instance is bound to */ me.dataview = dataview; dataview.blockRefresh = true; dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() { this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) { 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 */ me.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 */ me.cachedStoreData = {}; //catch the store data with the snapshot immediately me.cacheStoreData(store.data || store.snapshot); dataview.on('resize', function() { var store = dataview.store; if (store.getCount() > 0) { // reDraw.call(this, store); } }, this); // Buffer listenher so that rapid calls, for example a filter followed by a sort // Only produce one redraw. dataview.store.on({ datachanged: reDraw, scope: this, buffer: 50 }); function reDraw() { var parentEl = dataview.getTargetEl(), parentElY = parentEl.getY(), parentElPaddingTop = parentEl.getPadding('t'), added = me.getAdded(store), removed = me.getRemoved(store), remaining = me.getRemaining(store), itemArray, i, id, itemFly = new Ext.dom.Fly(), rtl = me.dataview.getInherited().rtl, oldPos, newPos, styleSide = rtl ? 'right' : 'left', newStyle = {}; // Not yet rendered if (!parentEl) { return; } // Collect nodes that will be removed in the forthcoming refresh so // that we can put them back in order to fade them out Ext.iterate(removed, function(recId, item) { id = me.dataviewID + '-' + recId; // Stop any animations for removed items and ensure th. Ext.fx.Manager.stopAnimation(id); item.dom = Ext.getDom(id); if (!item.dom) { delete removed[recId]; } }); me.cacheStoreData(store); // stores the current top and left values for each element (discovered below) var oldPositions = {}, newPositions = {}; // Find current positions of elements which are to remain after the refresh. Ext.iterate(remaining, function(id, item) { if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) { oldPos = oldPositions[id] = { top : itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop }; oldPos[styleSide] = me.getItemX(itemFly); } else { delete remaining[id]; } }); // The view MUST refresh, creating items in the natural flow, and collecting the items // so that its item collection is consistent. dataview.refresh(); // Replace removed nodes so that they can be faded out, THEN removed Ext.iterate(removed, function(id, item) { parentEl.dom.appendChild(item.dom); itemFly.attach(item.dom).animate({ duration: duration, opacity : 0, callback: function(anim) { var el = Ext.get(anim.target.id); if (el) { el.destroy(); } } }); delete item.dom; }); // We have taken care of any removals. // If the store is empty, we are done. if (!store.getCount()) { return; } // Collect the correct new positions after the refresh itemArray = items.slice(); // Reverse order so that moving to absolute position does not affect the position of // the next one we're looking at. for (i = itemArray.length - 1; i >= 0; i--) { id = store.getAt(i).internalId; itemFly.attach(itemArray[i]); newPositions[id] = { dom: itemFly.dom, top : itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop }; newPositions[id][styleSide] = me.getItemX(itemFly); // We're going to absolutely position each item. // If it is a "remaining" one from last refesh, shunt it back to // its old position from where it will be animated. newPos = oldPositions[id] || newPositions[id]; // set absolute positioning on all DataView items. We need to set position, left and // top at the same time to avoid any flickering newStyle.position = 'absolute'; newStyle.top = newPos.top + "px"; newStyle[styleSide] = newPos.left + "px"; itemFly.applyStyles(newStyle); } // This is the function which moves remaining items to their new position var doAnimate = function() { var elapsed = new Date() - task.taskStartTime, fraction = elapsed / duration; if (fraction >= 1) { // At end, return all items to natural flow. newStyle.position = newStyle.top = newStyle[styleSide] = ''; for (id in newPositions) { itemFly.attach(newPositions[id].dom).applyStyles(newStyle); } Ext.TaskManager.stop(task); } else { // In frame, move each "remaining" item according to time elapsed for (id in remaining) { 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"; itemFly.attach(newPos.dom).applyStyles(newStyle); } } }; // Fade in new items Ext.iterate(added, function(id, item) { if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) { itemFly.setOpacity(0); itemFly.animate({ duration: duration, opacity : 1 }); } }); // Stop any previous animations Ext.TaskManager.stop(task); task.run = doAnimate; Ext.TaskManager.start(task); me.cacheStoreData(store); } }, getItemX: function(el) { var rtl = this.dataview.getInherited().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) { var cachedStoreData = this.cachedStoreData = {}; store.each(function(record) { cachedStoreData[record.internalId] = record; }); }, /** * 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 cachedStoreData = this.cachedStoreData, added = {}; store.each(function(record) { if (cachedStoreData[record.internalId] == null) { added[record.internalId] = record; } }); 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 cachedStoreData = this.cachedStoreData, removed = {}, id; for (id in cachedStoreData) { if (store.findBy(function(record) {return record.internalId == id;}) == -1) { removed[id] = 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 cachedStoreData = this.cachedStoreData, remaining = {}; store.each(function(record) { if (cachedStoreData[record.internalId] != null) { remaining[record.internalId] = record; } }); return remaining; }});