]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * Transition plugin for DataViews\r | |
3 | */\r | |
4 | Ext.define('Ext.ux.DataView.Animated', {\r | |
5 | \r | |
6 | /**\r | |
7 | * @property defaults\r | |
8 | * @type Object\r | |
9 | * Default configuration options for all DataViewTransition instances\r | |
10 | */\r | |
11 | defaults: {\r | |
12 | duration : 750,\r | |
13 | idProperty: 'id'\r | |
14 | },\r | |
15 | \r | |
16 | /**\r | |
17 | * Creates the plugin instance, applies defaults\r | |
18 | * @constructor\r | |
19 | * @param {Object} config Optional config object\r | |
20 | */\r | |
21 | constructor: function(config) {\r | |
22 | Ext.apply(this, config || {}, this.defaults);\r | |
23 | },\r | |
24 | \r | |
25 | /**\r | |
26 | * Initializes the transition plugin. Overrides the dataview's default refresh function\r | |
27 | * @param {Ext.view.View} dataview The dataview\r | |
28 | */\r | |
29 | init: function(dataview) {\r | |
30 | var me = this,\r | |
31 | store = dataview.store,\r | |
32 | items = dataview.all,\r | |
33 | task = {\r | |
34 | interval: 20\r | |
35 | },\r | |
36 | duration = me.duration;\r | |
37 | \r | |
38 | /**\r | |
39 | * @property dataview\r | |
40 | * @type Ext.view.View\r | |
41 | * Reference to the DataView this instance is bound to\r | |
42 | */\r | |
43 | me.dataview = dataview;\r | |
44 | \r | |
45 | dataview.blockRefresh = true;\r | |
46 | dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() {\r | |
47 | this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) {\r | |
48 | element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId);\r | |
49 | }, this);\r | |
50 | }, dataview);\r | |
51 | \r | |
52 | /**\r | |
53 | * @property dataviewID\r | |
54 | * @type String\r | |
55 | * The string ID of the DataView component. This is used internally when animating child objects\r | |
56 | */\r | |
57 | me.dataviewID = dataview.id;\r | |
58 | \r | |
59 | /**\r | |
60 | * @property cachedStoreData\r | |
61 | * @type Object\r | |
62 | * A cache of existing store data, keyed by id. This is used to determine\r | |
63 | * whether any items were added or removed from the store on data change\r | |
64 | */\r | |
65 | me.cachedStoreData = {};\r | |
66 | \r | |
67 | //catch the store data with the snapshot immediately\r | |
68 | me.cacheStoreData(store.data || store.snapshot);\r | |
69 | \r | |
70 | dataview.on('resize', function() {\r | |
71 | var store = dataview.store;\r | |
72 | if (store.getCount() > 0) {\r | |
73 | // reDraw.call(this, store);\r | |
74 | }\r | |
75 | }, this);\r | |
76 | \r | |
77 | // Buffer listenher so that rapid calls, for example a filter followed by a sort\r | |
78 | // Only produce one redraw.\r | |
79 | dataview.store.on({\r | |
80 | datachanged: reDraw,\r | |
81 | scope: this,\r | |
82 | buffer: 50\r | |
83 | });\r | |
84 | \r | |
85 | function reDraw() {\r | |
86 | var parentEl = dataview.getTargetEl(),\r | |
87 | parentElY = parentEl.getY(),\r | |
88 | parentElPaddingTop = parentEl.getPadding('t'),\r | |
89 | added = me.getAdded(store),\r | |
90 | removed = me.getRemoved(store),\r | |
91 | remaining = me.getRemaining(store),\r | |
92 | itemArray,\r | |
93 | i, id,\r | |
94 | itemFly = new Ext.dom.Fly(),\r | |
95 | rtl = me.dataview.getInherited().rtl,\r | |
96 | oldPos, newPos,\r | |
97 | styleSide = rtl ? 'right' : 'left',\r | |
98 | newStyle = {};\r | |
99 | \r | |
100 | // Not yet rendered\r | |
101 | if (!parentEl) {\r | |
102 | return;\r | |
103 | }\r | |
104 | \r | |
105 | // Collect nodes that will be removed in the forthcoming refresh so\r | |
106 | // that we can put them back in order to fade them out\r | |
107 | Ext.iterate(removed, function(recId, item) {\r | |
108 | id = me.dataviewID + '-' + recId;\r | |
109 | \r | |
110 | // Stop any animations for removed items and ensure th.\r | |
111 | Ext.fx.Manager.stopAnimation(id);\r | |
112 | \r | |
113 | item.dom = Ext.getDom(id);\r | |
114 | if (!item.dom) {\r | |
115 | delete removed[recId];\r | |
116 | }\r | |
117 | });\r | |
118 | \r | |
119 | me.cacheStoreData(store);\r | |
120 | \r | |
121 | // stores the current top and left values for each element (discovered below)\r | |
122 | var oldPositions = {},\r | |
123 | newPositions = {};\r | |
124 | \r | |
125 | // Find current positions of elements which are to remain after the refresh.\r | |
126 | Ext.iterate(remaining, function(id, item) {\r | |
127 | if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) {\r | |
128 | oldPos = oldPositions[id] = {\r | |
129 | top : itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop\r | |
130 | };\r | |
131 | oldPos[styleSide] = me.getItemX(itemFly);\r | |
132 | } else {\r | |
133 | delete remaining[id];\r | |
134 | }\r | |
135 | });\r | |
136 | \r | |
137 | // The view MUST refresh, creating items in the natural flow, and collecting the items\r | |
138 | // so that its item collection is consistent.\r | |
139 | dataview.refresh();\r | |
140 | \r | |
141 | // Replace removed nodes so that they can be faded out, THEN removed\r | |
142 | Ext.iterate(removed, function(id, item) {\r | |
143 | parentEl.dom.appendChild(item.dom);\r | |
144 | itemFly.attach(item.dom).animate({\r | |
145 | duration: duration,\r | |
146 | opacity : 0,\r | |
147 | callback: function(anim) {\r | |
148 | var el = Ext.get(anim.target.id);\r | |
149 | if (el) {\r | |
150 | el.destroy();\r | |
151 | }\r | |
152 | }\r | |
153 | });\r | |
154 | delete item.dom;\r | |
155 | });\r | |
156 | \r | |
157 | // We have taken care of any removals.\r | |
158 | // If the store is empty, we are done.\r | |
159 | if (!store.getCount()) {\r | |
160 | return;\r | |
161 | }\r | |
162 | \r | |
163 | // Collect the correct new positions after the refresh\r | |
164 | itemArray = items.slice();\r | |
165 | \r | |
166 | // Reverse order so that moving to absolute position does not affect the position of\r | |
167 | // the next one we're looking at.\r | |
168 | for (i = itemArray.length - 1; i >= 0; i--) {\r | |
169 | id = store.getAt(i).internalId;\r | |
170 | itemFly.attach(itemArray[i]);\r | |
171 | \r | |
172 | newPositions[id] = {\r | |
173 | dom: itemFly.dom,\r | |
174 | top : itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop\r | |
175 | };\r | |
176 | newPositions[id][styleSide] = me.getItemX(itemFly);\r | |
177 | \r | |
178 | // We're going to absolutely position each item.\r | |
179 | // If it is a "remaining" one from last refesh, shunt it back to\r | |
180 | // its old position from where it will be animated.\r | |
181 | newPos = oldPositions[id] || newPositions[id];\r | |
182 | \r | |
183 | // set absolute positioning on all DataView items. We need to set position, left and \r | |
184 | // top at the same time to avoid any flickering\r | |
185 | newStyle.position = 'absolute';\r | |
186 | newStyle.top = newPos.top + "px";\r | |
187 | newStyle[styleSide] = newPos.left + "px";\r | |
188 | itemFly.applyStyles(newStyle);\r | |
189 | }\r | |
190 | \r | |
191 | // This is the function which moves remaining items to their new position\r | |
192 | var doAnimate = function() {\r | |
193 | var elapsed = new Date() - task.taskStartTime,\r | |
194 | fraction = elapsed / duration;\r | |
195 | \r | |
196 | if (fraction >= 1) {\r | |
197 | // At end, return all items to natural flow.\r | |
198 | newStyle.position = newStyle.top = newStyle[styleSide] = '';\r | |
199 | for (id in newPositions) {\r | |
200 | itemFly.attach(newPositions[id].dom).applyStyles(newStyle);\r | |
201 | }\r | |
202 | Ext.TaskManager.stop(task);\r | |
203 | } else {\r | |
204 | // In frame, move each "remaining" item according to time elapsed\r | |
205 | for (id in remaining) {\r | |
206 | var oldPos = oldPositions[id],\r | |
207 | newPos = newPositions[id],\r | |
208 | oldTop = oldPos.top,\r | |
209 | newTop = newPos.top,\r | |
210 | oldLeft = oldPos[styleSide],\r | |
211 | newLeft = newPos[styleSide],\r | |
212 | diffTop = fraction * Math.abs(oldTop - newTop),\r | |
213 | diffLeft= fraction * Math.abs(oldLeft - newLeft),\r | |
214 | midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop,\r | |
215 | midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft;\r | |
216 | \r | |
217 | newStyle.top = midTop + "px";\r | |
218 | newStyle[styleSide] = midLeft + "px";\r | |
219 | itemFly.attach(newPos.dom).applyStyles(newStyle);\r | |
220 | }\r | |
221 | }\r | |
222 | };\r | |
223 | \r | |
224 | // Fade in new items\r | |
225 | Ext.iterate(added, function(id, item) {\r | |
226 | if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) {\r | |
227 | itemFly.setOpacity(0);\r | |
228 | itemFly.animate({\r | |
229 | duration: duration,\r | |
230 | opacity : 1\r | |
231 | });\r | |
232 | }\r | |
233 | });\r | |
234 | \r | |
235 | // Stop any previous animations\r | |
236 | Ext.TaskManager.stop(task);\r | |
237 | task.run = doAnimate;\r | |
238 | Ext.TaskManager.start(task);\r | |
239 | \r | |
240 | me.cacheStoreData(store);\r | |
241 | }\r | |
242 | },\r | |
243 | \r | |
244 | getItemX: function(el) {\r | |
245 | var rtl = this.dataview.getInherited().rtl,\r | |
246 | parentEl = el.up('');\r | |
247 | \r | |
248 | if (rtl) {\r | |
249 | return parentEl.getViewRegion().right - el.getRegion().right + el.getMargin('r');\r | |
250 | } else {\r | |
251 | return el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l');\r | |
252 | }\r | |
253 | },\r | |
254 | \r | |
255 | /**\r | |
256 | * Caches the records from a store locally for comparison later\r | |
257 | * @param {Ext.data.Store} store The store to cache data from\r | |
258 | */\r | |
259 | cacheStoreData: function(store) {\r | |
260 | var cachedStoreData = this.cachedStoreData = {};\r | |
261 | \r | |
262 | store.each(function(record) {\r | |
263 | cachedStoreData[record.internalId] = record;\r | |
264 | });\r | |
265 | },\r | |
266 | \r | |
267 | /**\r | |
268 | * Returns all records that were already in the DataView\r | |
269 | * @return {Object} All existing records\r | |
270 | */\r | |
271 | getExisting: function() {\r | |
272 | return this.cachedStoreData;\r | |
273 | },\r | |
274 | \r | |
275 | /**\r | |
276 | * Returns the total number of items that are currently visible in the DataView\r | |
277 | * @return {Number} The number of existing items\r | |
278 | */\r | |
279 | getExistingCount: function() {\r | |
280 | var count = 0,\r | |
281 | items = this.getExisting();\r | |
282 | \r | |
283 | for (var k in items) {\r | |
284 | count++;\r | |
285 | }\r | |
286 | \r | |
287 | return count;\r | |
288 | },\r | |
289 | \r | |
290 | /**\r | |
291 | * Returns all records in the given store that were not already present\r | |
292 | * @param {Ext.data.Store} store The updated store instance\r | |
293 | * @return {Object} Object of records not already present in the dataview in format {id: record}\r | |
294 | */\r | |
295 | getAdded: function(store) {\r | |
296 | var cachedStoreData = this.cachedStoreData,\r | |
297 | added = {};\r | |
298 | \r | |
299 | store.each(function(record) {\r | |
300 | if (cachedStoreData[record.internalId] == null) {\r | |
301 | added[record.internalId] = record;\r | |
302 | }\r | |
303 | });\r | |
304 | \r | |
305 | return added;\r | |
306 | },\r | |
307 | \r | |
308 | /**\r | |
309 | * Returns all records that are present in the DataView but not the new store\r | |
310 | * @param {Ext.data.Store} store The updated store instance\r | |
311 | * @return {Array} Array of records that used to be present\r | |
312 | */\r | |
313 | getRemoved: function(store) {\r | |
314 | var cachedStoreData = this.cachedStoreData,\r | |
315 | removed = {},\r | |
316 | id;\r | |
317 | \r | |
318 | for (id in cachedStoreData) {\r | |
319 | if (store.findBy(function(record) {return record.internalId == id;}) == -1) {\r | |
320 | removed[id] = cachedStoreData[id];\r | |
321 | }\r | |
322 | }\r | |
323 | \r | |
324 | return removed;\r | |
325 | },\r | |
326 | \r | |
327 | /**\r | |
328 | * Returns all records that are already present and are still present in the new store\r | |
329 | * @param {Ext.data.Store} store The updated store instance\r | |
330 | * @return {Object} Object of records that are still present from last time in format {id: record}\r | |
331 | */\r | |
332 | getRemaining: function(store) {\r | |
333 | var cachedStoreData = this.cachedStoreData,\r | |
334 | remaining = {};\r | |
335 | \r | |
336 | store.each(function(record) {\r | |
337 | if (cachedStoreData[record.internalId] != null) {\r | |
338 | remaining[record.internalId] = record;\r | |
339 | }\r | |
340 | });\r | |
341 | \r | |
342 | return remaining;\r | |
343 | }\r | |
344 | });\r |