|
1 /* |
|
2 |
|
3 Quicksand 1.2.2 |
|
4 |
|
5 Reorder and filter items with a nice shuffling animation. |
|
6 |
|
7 Copyright (c) 2010 Jacek Galanciak (razorjack.net) and agilope.com |
|
8 Big thanks for Piotr Petrus (riddle.pl) for deep code review and wonderful docs & demos. |
|
9 |
|
10 Dual licensed under the MIT and GPL version 2 licenses. |
|
11 http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt |
|
12 http://github.com/jquery/jquery/blob/master/GPL-LICENSE.txt |
|
13 |
|
14 Project site: http://razorjack.net/quicksand |
|
15 Github site: http://github.com/razorjack/quicksand |
|
16 |
|
17 */ |
|
18 |
|
19 (function ($) { |
|
20 $.fn.quicksand = function (collection, customOptions) { |
|
21 var options = { |
|
22 duration: 750, |
|
23 easing: 'swing', |
|
24 attribute: 'data-id', // attribute to recognize same items within source and dest |
|
25 adjustHeight: 'auto', // 'dynamic' animates height during shuffling (slow), 'auto' adjusts it before or after the animation, false leaves height constant |
|
26 useScaling: true, // disable it if you're not using scaling effect or want to improve performance |
|
27 enhancement: function(c) {}, // Visual enhacement (eg. font replacement) function for cloned elements |
|
28 selector: '> *', |
|
29 dx: 0, |
|
30 dy: 0 |
|
31 }; |
|
32 $.extend(options, customOptions); |
|
33 |
|
34 if ($.browser.msie || (typeof($.fn.scale) == 'undefined')) { |
|
35 // Got IE and want scaling effect? Kiss my ass. |
|
36 options.useScaling = false; |
|
37 } |
|
38 |
|
39 var callbackFunction; |
|
40 if (typeof(arguments[1]) == 'function') { |
|
41 var callbackFunction = arguments[1]; |
|
42 } else if (typeof(arguments[2] == 'function')) { |
|
43 var callbackFunction = arguments[2]; |
|
44 } |
|
45 |
|
46 |
|
47 return this.each(function (i) { |
|
48 var val; |
|
49 var animationQueue = []; // used to store all the animation params before starting the animation; solves initial animation slowdowns |
|
50 var $collection = $(collection).clone(); // destination (target) collection |
|
51 var $sourceParent = $(this); // source, the visible container of source collection |
|
52 var sourceHeight = $(this).css('height'); // used to keep height and document flow during the animation |
|
53 |
|
54 var destHeight; |
|
55 var adjustHeightOnCallback = false; |
|
56 |
|
57 var offset = $($sourceParent).offset(); // offset of visible container, used in animation calculations |
|
58 var offsets = []; // coordinates of every source collection item |
|
59 |
|
60 var $source = $(this).find(options.selector); // source collection items |
|
61 |
|
62 // Replace the collection and quit if IE6 |
|
63 if ($.browser.msie && $.browser.version.substr(0,1)<7) { |
|
64 $sourceParent.html('').append($collection); |
|
65 return; |
|
66 } |
|
67 |
|
68 // Gets called when any animation is finished |
|
69 var postCallbackPerformed = 0; // prevents the function from being called more than one time |
|
70 var postCallback = function () { |
|
71 |
|
72 if (!postCallbackPerformed) { |
|
73 postCallbackPerformed = 1; |
|
74 |
|
75 // hack: |
|
76 // used to be: $sourceParent.html($dest.html()); // put target HTML into visible source container |
|
77 // but new webkit builds cause flickering when replacing the collections |
|
78 $toDelete = $sourceParent.find('> *'); |
|
79 $sourceParent.prepend($dest.find('> *')); |
|
80 $toDelete.remove(); |
|
81 |
|
82 if (adjustHeightOnCallback) { |
|
83 $sourceParent.css('height', destHeight); |
|
84 } |
|
85 options.enhancement($sourceParent); // Perform custom visual enhancements on a newly replaced collection |
|
86 if (typeof callbackFunction == 'function') { |
|
87 callbackFunction.call(this); |
|
88 } |
|
89 } |
|
90 }; |
|
91 |
|
92 // Position: relative situations |
|
93 var $correctionParent = $sourceParent.offsetParent(); |
|
94 var correctionOffset = $correctionParent.offset(); |
|
95 if ($correctionParent.css('position') == 'relative') { |
|
96 if ($correctionParent.get(0).nodeName.toLowerCase() == 'body') { |
|
97 |
|
98 } else { |
|
99 correctionOffset.top += (parseFloat($correctionParent.css('border-top-width')) || 0); |
|
100 correctionOffset.left +=( parseFloat($correctionParent.css('border-left-width')) || 0); |
|
101 } |
|
102 } else { |
|
103 correctionOffset.top -= (parseFloat($correctionParent.css('border-top-width')) || 0); |
|
104 correctionOffset.left -= (parseFloat($correctionParent.css('border-left-width')) || 0); |
|
105 correctionOffset.top -= (parseFloat($correctionParent.css('margin-top')) || 0); |
|
106 correctionOffset.left -= (parseFloat($correctionParent.css('margin-left')) || 0); |
|
107 } |
|
108 |
|
109 // perform custom corrections from options (use when Quicksand fails to detect proper correction) |
|
110 if (isNaN(correctionOffset.left)) { |
|
111 correctionOffset.left = 0; |
|
112 } |
|
113 if (isNaN(correctionOffset.top)) { |
|
114 correctionOffset.top = 0; |
|
115 } |
|
116 |
|
117 correctionOffset.left -= options.dx; |
|
118 correctionOffset.top -= options.dy; |
|
119 |
|
120 // keeps nodes after source container, holding their position |
|
121 $sourceParent.css('height', $(this).height()); |
|
122 |
|
123 // get positions of source collections |
|
124 $source.each(function (i) { |
|
125 offsets[i] = $(this).offset(); |
|
126 }); |
|
127 |
|
128 // stops previous animations on source container |
|
129 $(this).stop(); |
|
130 var dx = 0; var dy = 0; |
|
131 $source.each(function (i) { |
|
132 $(this).stop(); // stop animation of collection items |
|
133 var rawObj = $(this).get(0); |
|
134 if (rawObj.style.position == 'absolute') { |
|
135 dx = -options.dx; |
|
136 dy = -options.dy; |
|
137 } else { |
|
138 dx = options.dx; |
|
139 dy = options.dy; |
|
140 } |
|
141 |
|
142 rawObj.style.position = 'absolute'; |
|
143 rawObj.style.margin = '0'; |
|
144 |
|
145 rawObj.style.top = (offsets[i].top - parseFloat(rawObj.style.marginTop) - correctionOffset.top + dy) + 'px'; |
|
146 rawObj.style.left = (offsets[i].left - parseFloat(rawObj.style.marginLeft) - correctionOffset.left + dx) + 'px'; |
|
147 }); |
|
148 |
|
149 // create temporary container with destination collection |
|
150 var $dest = $($sourceParent).clone(); |
|
151 var rawDest = $dest.get(0); |
|
152 rawDest.innerHTML = ''; |
|
153 rawDest.setAttribute('id', ''); |
|
154 rawDest.style.height = 'auto'; |
|
155 rawDest.style.width = $sourceParent.width() + 'px'; |
|
156 $dest.append($collection); |
|
157 // insert node into HTML |
|
158 // Note that the node is under visible source container in the exactly same position |
|
159 // The browser render all the items without showing them (opacity: 0.0) |
|
160 // No offset calculations are needed, the browser just extracts position from underlayered destination items |
|
161 // and sets animation to destination positions. |
|
162 $dest.insertBefore($sourceParent); |
|
163 $dest.css('opacity', 0.0); |
|
164 rawDest.style.zIndex = -1; |
|
165 |
|
166 rawDest.style.margin = '0'; |
|
167 rawDest.style.position = 'absolute'; |
|
168 rawDest.style.top = offset.top - correctionOffset.top + 'px'; |
|
169 rawDest.style.left = offset.left - correctionOffset.left + 'px'; |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 if (options.adjustHeight === 'dynamic') { |
|
176 // If destination container has different height than source container |
|
177 // the height can be animated, adjusting it to destination height |
|
178 $sourceParent.animate({height: $dest.height()}, options.duration, options.easing); |
|
179 } else if (options.adjustHeight === 'auto') { |
|
180 destHeight = $dest.height(); |
|
181 if (parseFloat(sourceHeight) < parseFloat(destHeight)) { |
|
182 // Adjust the height now so that the items don't move out of the container |
|
183 $sourceParent.css('height', destHeight); |
|
184 } else { |
|
185 // Adjust later, on callback |
|
186 adjustHeightOnCallback = true; |
|
187 } |
|
188 } |
|
189 |
|
190 // Now it's time to do shuffling animation |
|
191 // First of all, we need to identify same elements within source and destination collections |
|
192 $source.each(function (i) { |
|
193 var destElement = []; |
|
194 if (typeof(options.attribute) == 'function') { |
|
195 |
|
196 val = options.attribute($(this)); |
|
197 $collection.each(function() { |
|
198 if (options.attribute(this) == val) { |
|
199 destElement = $(this); |
|
200 return false; |
|
201 } |
|
202 }); |
|
203 } else { |
|
204 destElement = $collection.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']'); |
|
205 } |
|
206 if (destElement.length) { |
|
207 // The item is both in source and destination collections |
|
208 // It it's under different position, let's move it |
|
209 if (!options.useScaling) { |
|
210 animationQueue.push( |
|
211 { |
|
212 element: $(this), |
|
213 animation: |
|
214 {top: destElement.offset().top - correctionOffset.top, |
|
215 left: destElement.offset().left - correctionOffset.left, |
|
216 opacity: 1.0 |
|
217 } |
|
218 }); |
|
219 |
|
220 } else { |
|
221 animationQueue.push({ |
|
222 element: $(this), |
|
223 animation: {top: destElement.offset().top - correctionOffset.top, |
|
224 left: destElement.offset().left - correctionOffset.left, |
|
225 opacity: 1.0, |
|
226 scale: '1.0' |
|
227 } |
|
228 }); |
|
229 |
|
230 } |
|
231 } else { |
|
232 // The item from source collection is not present in destination collections |
|
233 // Let's remove it |
|
234 if (!options.useScaling) { |
|
235 animationQueue.push({element: $(this), |
|
236 animation: {opacity: '0.0'}}); |
|
237 } else { |
|
238 animationQueue.push({element: $(this), animation: {opacity: '0.0', |
|
239 scale: '0.0'}}); |
|
240 } |
|
241 } |
|
242 }); |
|
243 |
|
244 $collection.each(function (i) { |
|
245 // Grab all items from target collection not present in visible source collection |
|
246 |
|
247 var sourceElement = []; |
|
248 var destElement = []; |
|
249 if (typeof(options.attribute) == 'function') { |
|
250 val = options.attribute($(this)); |
|
251 $source.each(function() { |
|
252 if (options.attribute(this) == val) { |
|
253 sourceElement = $(this); |
|
254 return false; |
|
255 } |
|
256 }); |
|
257 |
|
258 $collection.each(function() { |
|
259 if (options.attribute(this) == val) { |
|
260 destElement = $(this); |
|
261 return false; |
|
262 } |
|
263 }); |
|
264 } else { |
|
265 sourceElement = $source.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']'); |
|
266 destElement = $collection.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']'); |
|
267 } |
|
268 |
|
269 var animationOptions; |
|
270 if (sourceElement.length === 0) { |
|
271 // No such element in source collection... |
|
272 if (!options.useScaling) { |
|
273 animationOptions = { |
|
274 opacity: '1.0' |
|
275 }; |
|
276 } else { |
|
277 animationOptions = { |
|
278 opacity: '1.0', |
|
279 scale: '1.0' |
|
280 }; |
|
281 } |
|
282 // Let's create it |
|
283 d = destElement.clone(); |
|
284 var rawDestElement = d.get(0); |
|
285 rawDestElement.style.position = 'absolute'; |
|
286 rawDestElement.style.margin = '0'; |
|
287 rawDestElement.style.top = destElement.offset().top - correctionOffset.top + 'px'; |
|
288 rawDestElement.style.left = destElement.offset().left - correctionOffset.left + 'px'; |
|
289 d.css('opacity', 0.0); // IE |
|
290 if (options.useScaling) { |
|
291 d.css('transform', 'scale(0.0)'); |
|
292 } |
|
293 d.appendTo($sourceParent); |
|
294 |
|
295 animationQueue.push({element: $(d), |
|
296 animation: animationOptions}); |
|
297 } |
|
298 }); |
|
299 |
|
300 $dest.remove(); |
|
301 options.enhancement($sourceParent); // Perform custom visual enhancements during the animation |
|
302 for (i = 0; i < animationQueue.length; i++) { |
|
303 animationQueue[i].element.animate(animationQueue[i].animation, options.duration, options.easing, postCallback); |
|
304 } |
|
305 }); |
|
306 }; |
|
307 })(jQuery); |