/**
* 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 = {},
oldPositions, newPositions, doAnimate;
// 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)
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
doAnimate = function() {
var elapsed = new Date() - task.taskStartTime,
fraction = elapsed / duration,
oldPos, newPos, oldTop, newTop, oldLeft, newLeft,
diffTop, diffLeft, midTop, midLeft;
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) {
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(),
k; // eslint-disable-line no-unused-vars
for (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) {
// eslint-disable-next-line brace-style, semi
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;
}
});