0
|
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); |