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