Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / examples / ux / DataView / Animated.js
1 /**
2  * @class Ext.ux.DataViewTransition
3  * @extends Object
4  * @author Ed Spencer (http://sencha.com)
5  * Transition plugin for DataViews
6  */
7 Ext.define('Ext.ux.DataView.Animated', {
8
9     /**
10      * @property defaults
11      * @type Object
12      * Default configuration options for all DataViewTransition instances
13      */
14     defaults: {
15         duration  : 750,
16         idProperty: 'id'
17     },
18     
19     /**
20      * Creates the plugin instance, applies defaults
21      * @constructor
22      * @param {Object} config Optional config object
23      */
24     constructor: function(config) {
25         Ext.apply(this, config || {}, this.defaults);
26     },
27
28     /**
29      * Initializes the transition plugin. Overrides the dataview's default refresh function
30      * @param {Ext.view.View} dataview The dataview
31      */
32     init: function(dataview) {
33         /**
34          * @property dataview
35          * @type Ext.view.View
36          * Reference to the DataView this instance is bound to
37          */
38         this.dataview = dataview;
39         
40         var idProperty = this.idProperty,
41             store = dataview.store;
42         
43         dataview.blockRefresh = true;
44         dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() {
45             this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) {
46                 element.id = element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId);
47             }, this);
48         }, dataview);
49         
50         /**
51          * @property dataviewID
52          * @type String
53          * The string ID of the DataView component. This is used internally when animating child objects
54          */
55         this.dataviewID = dataview.id;
56         
57         /**
58          * @property cachedStoreData
59          * @type Object
60          * A cache of existing store data, keyed by id. This is used to determine
61          * whether any items were added or removed from the store on data change
62          */
63         this.cachedStoreData = {};
64         
65         //catch the store data with the snapshot immediately
66         this.cacheStoreData(store.data || store.snapshot);
67
68         dataview.on('resize', function() {
69             var store = dataview.store;
70             if (store.getCount() > 0) {
71                 // reDraw.call(this, store);
72             }
73         }, this);
74         
75         dataview.store.on('datachanged', reDraw, this);
76         
77         function reDraw(store) {
78             var parentEl = dataview.getTargetEl(),
79                 calcItem = store.getAt(0),
80                 added    = this.getAdded(store),
81                 removed  = this.getRemoved(store),
82                 previous = this.getRemaining(store),
83                 existing = Ext.apply({}, previous, added);
84             
85             //hide old items
86             Ext.each(removed, function(item) {
87                 var id = this.dataviewID + '-' + item.internalId;
88                 Ext.fly(id).animate({
89                     remove  : false,
90                     duration: duration,
91                     opacity : 0,
92                     useDisplay: true,
93                     callback: function() {
94                         Ext.fly(id).setDisplayed(false);
95                     }
96                 });
97             }, this);
98             
99             //store is empty
100             if (calcItem == undefined) {
101                 this.cacheStoreData(store);
102                 return;
103             }
104             
105             this.cacheStoreData(store);
106             
107             var el = Ext.get(this.dataviewID + "-" + calcItem.internalId);
108             
109             //if there is nothing rendered, force a refresh and return. This happens when loading asynchronously (was not
110             //covered correctly in previous versions, which only accepted local data)
111             if (!el) {
112                 dataview.refresh();
113                 return true;
114             }
115             
116             //calculate the number of rows and columns we have
117             var itemCount   = store.getCount(),
118                 itemWidth   = el.getMargin('lr') + el.getWidth(),
119                 itemHeight  = el.getMargin('bt') + el.getHeight(),
120                 dvWidth     = parentEl.getWidth(),
121                 columns     = Math.floor(dvWidth / itemWidth),
122                 rows        = Math.ceil(itemCount / columns),
123                 currentRows = Math.ceil(this.getExistingCount() / columns);
124             
125             //stores the current top and left values for each element (discovered below)
126             var oldPositions = {},
127                 newPositions = {},
128                 elCache      = {};
129             
130             //find current positions of each element and save a reference in the elCache
131             Ext.iterate(previous, function(id, item) {
132                 var id = item.internalId,
133                     el = elCache[id] = Ext.get(this.dataviewID + '-' + id);
134                 
135                 oldPositions[id] = {
136                     top : el.getTop()  - parentEl.getTop()  - el.getMargin('t') - parentEl.getPadding('t'),
137                     left: el.getLeft() - parentEl.getLeft() - el.getMargin('l') - parentEl.getPadding('l')
138                 };
139             }, this);
140             
141             //make sure the correct styles are applied to the parent element
142             parentEl.applyStyles({
143                 display : 'block',
144                 position: 'relative'
145             });
146             
147             //set absolute positioning on all DataView items. We need to set position, left and 
148             //top at the same time to avoid any flickering
149             Ext.iterate(previous, function(id, item) {
150                 var oldPos = oldPositions[id],
151                     el     = elCache[id];
152
153                 if (el.getStyle('position') != 'absolute') {
154                     elCache[id].applyStyles({
155                         position: 'absolute',
156                         left    : oldPos.left + "px",
157                         top     : oldPos.top + "px"
158                     });
159                 }
160             });
161             
162             //get new positions
163             var index = 0;
164             Ext.iterate(store.data.items, function(item) {
165                 var id = item.internalId,
166                     el = elCache[id];
167                 
168                 var column = index % columns,
169                     row    = Math.floor(index / columns),
170                     top    = row    * itemHeight,
171                     left   = column * itemWidth;
172                 
173                 newPositions[id] = {
174                     top : top,
175                     left: left
176                 };
177                 
178                 index ++;
179             }, this);
180             
181             //do the movements
182             var startTime  = new Date(),
183                 duration   = this.duration,
184                 dataviewID = this.dataviewID;
185             
186             var doAnimate = function() {
187                 var elapsed  = new Date() - startTime,
188                     fraction = elapsed / duration,
189                     id;
190                 
191                 if (fraction >= 1) {
192                     for (id in newPositions) {
193                         Ext.fly(dataviewID + '-' + id).applyStyles({
194                             top : newPositions[id].top + "px",
195                             left: newPositions[id].left + "px"
196                         });
197                     }
198                     
199                     Ext.TaskManager.stop(task);
200                 } else {
201                     //move each item
202                     for (id in newPositions) {
203                         if (!previous[id]) {
204                             continue;
205                         }
206                         
207                         var oldPos  = oldPositions[id],
208                             newPos  = newPositions[id],
209                             oldTop  = oldPos.top,
210                             newTop  = newPos.top,
211                             oldLeft = oldPos.left,
212                             newLeft = newPos.left,
213                             diffTop = fraction * Math.abs(oldTop  - newTop),
214                             diffLeft= fraction * Math.abs(oldLeft - newLeft),
215                             midTop  = oldTop  > newTop  ? oldTop  - diffTop  : oldTop  + diffTop,
216                             midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft;
217                         
218                         Ext.fly(dataviewID + '-' + id).applyStyles({
219                             top : midTop + "px",
220                             left: midLeft + "px"
221                         }).setDisplayed(true);
222                     }
223                 }
224             };
225             
226             var task = {
227                 run     : doAnimate,
228                 interval: 20,
229                 scope   : this
230             };
231             
232             Ext.TaskManager.start(task);
233             
234             //show new items
235             Ext.iterate(added, function(id, item) {
236                 Ext.fly(this.dataviewID + '-' + item.internalId).applyStyles({
237                     top    : newPositions[item.internalId].top + "px",
238                     left   : newPositions[item.internalId].left + "px"
239                 }).setDisplayed(true);
240                 
241                 Ext.fly(this.dataviewID + '-' + item.internalId).animate({
242                     remove  : false,
243                     duration: duration,
244                     opacity : 1
245                 });
246             }, this);
247             
248             this.cacheStoreData(store);
249         }
250     },
251     
252     /**
253      * Caches the records from a store locally for comparison later
254      * @param {Ext.data.Store} store The store to cache data from
255      */
256     cacheStoreData: function(store) {
257         this.cachedStoreData = {};
258         
259         store.each(function(record) {
260              this.cachedStoreData[record.internalId] = record;
261         }, this);
262     },
263     
264     /**
265      * Returns all records that were already in the DataView
266      * @return {Object} All existing records
267      */
268     getExisting: function() {
269         return this.cachedStoreData;
270     },
271     
272     /**
273      * Returns the total number of items that are currently visible in the DataView
274      * @return {Number} The number of existing items
275      */
276     getExistingCount: function() {
277         var count = 0,
278             items = this.getExisting();
279         
280         for (var k in items) {
281             count++;
282         }
283         
284         return count;
285     },
286     
287     /**
288      * Returns all records in the given store that were not already present
289      * @param {Ext.data.Store} store The updated store instance
290      * @return {Object} Object of records not already present in the dataview in format {id: record}
291      */
292     getAdded: function(store) {
293         var added = {};
294         
295         store.each(function(record) {
296             if (this.cachedStoreData[record.internalId] == undefined) {
297                 added[record.internalId] = record;
298             }
299         }, this);
300         
301         return added;
302     },
303     
304     /**
305      * Returns all records that are present in the DataView but not the new store
306      * @param {Ext.data.Store} store The updated store instance
307      * @return {Array} Array of records that used to be present
308      */
309     getRemoved: function(store) {
310         var removed = [],
311             id;
312         
313         for (id in this.cachedStoreData) {
314             if (store.findBy(function(record) {return record.internalId == id;}) == -1) {
315                 removed.push(this.cachedStoreData[id]);
316             }
317         }
318         
319         return removed;
320     },
321     
322     /**
323      * Returns all records that are already present and are still present in the new store
324      * @param {Ext.data.Store} store The updated store instance
325      * @return {Object} Object of records that are still present from last time in format {id: record}
326      */
327     getRemaining: function(store) {
328         var remaining = {};
329
330         store.each(function(record) {
331             if (this.cachedStoreData[record.internalId] != undefined) {
332                 remaining[record.internalId] = record;
333             }
334         }, this);
335         
336         return remaining;
337     }
338 });