forked from razorjack/quicksand
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jquery.quicksand.js
438 lines (392 loc) · 16.7 KB
/
jquery.quicksand.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
/*
Quicksand 1.2.2
Reorder and filter items with a nice shuffling animation.
Copyright (c) 2010 Jacek Galanciak (razorjack.net) and agilope.com
Big thanks for Piotr Petrus (riddle.pl) for deep code review and wonderful docs & demos.
Dual licensed under the MIT and GPL version 2 licenses.
http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt
http://github.com/jquery/jquery/blob/master/GPL-LICENSE.txt
Project site: http://razorjack.net/quicksand
Github site: http://github.com/razorjack/quicksand
*/
(function($) {
$.fn.quicksand = function(collection, customOptions) {
var options = {
duration : 750,
easing : 'swing',
attribute : 'data-id', // attribute to recognize same items within source and dest
adjustHeight : 'auto', // 'dynamic' animates height during shuffling (slow), 'auto' adjusts it
// before or after the animation, false leaves height constant
adjustWidth : 'auto', // 'dynamic' animates width during shuffling (slow),
// 'auto' adjusts it before or after the animation, false leaves width constant
useScaling : false, // enable it if you're using scaling effect
enhancement : function(c) {}, // Visual enhacement (eg. font replacement) function for cloned elements
selector : '> *',
atomic : false,
dx : 0,
dy : 0,
maxWidth : 0,
retainExisting : true // disable if you want the collection of items to be replaced completely by incoming items.
};
$.extend(options, customOptions);
// Got IE and want scaling effect? Kiss my ass.
if ($.browser.msie || (typeof ($.fn.scale) == 'undefined')) {
options.useScaling = false;
}
var callbackFunction;
if (typeof (arguments[1]) == 'function') {
var callbackFunction = arguments[1];
} else if (typeof (arguments[2] == 'function')) {
var callbackFunction = arguments[2];
}
return this.each(function(i) {
var val;
var animationQueue = []; // used to store all the animation params before starting the animation;
// solves initial animation slowdowns
if (typeof(options.attribute) == 'function') {
var $collection = $(collection);
} else {
var $collection = $(collection).filter('[' + options.attribute + ']').clone(); // destination (target) collection
}
var $sourceParent = $(this); // source, the visible container of source collection
var sourceHeight = $(this).css('height'); // used to keep height and document flow during the animation
var sourceWidth = $(this).css('width'); // used to keep width and document flow during the animation
var destHeight, destWidth;
var adjustHeightOnCallback = false;
var adjustWidthOnCallback = false;
var offset = $($sourceParent).offset(); // offset of visible container, used in animation calculations
var offsets = []; // coordinates of every source collection item
var $source = $(this).find(options.selector); // source collection items
var width = $($source).innerWidth(); // need for the responsive design
// Replace the collection and quit if IE6
if ($.browser.msie && parseInt($.browser.version) < 7) {
$sourceParent.html('').append($collection);
return;
}
// Gets called when any animation is finished
var postCallbackPerformed = 0; // prevents the function from being called more than one time
var postCallback = function() {
$(this).css('margin', '').css('position', '').css('top', '').css('left', '').css('opacity', '');
if (!postCallbackPerformed) {
postCallbackPerformed = 1;
if (!options.atomic) {
// hack: used to be: $sourceParent.html($dest.html());
// put target HTML into visible source container
// but new webkit builds cause flickering when replacing the collections
var $toDelete = $sourceParent.find(options.selector);
if (!options.retainExisting) {
$sourceParent.prepend($dest.find(options.selector));
$toDelete.remove();
} else {
// Avoid replacing elements because we may have already altered items in significant
// ways and it would be bad to have to do it again. (i.e. lazy load images)
// But $dest holds the correct ordering. So we must re-sequence items in $sourceParent to match.
var $keepElements = $([]);
$dest.find(options.selector).each(function(i) {
var $matchedElement = $([]);
if (typeof (options.attribute) == 'function') {
var val = options.attribute($(this));
$toDelete.each(function() {
if (options.attribute(this) == val) {
$matchedElement = $(this);
return false;
}
});
} else {
$matchedElement = $toDelete.filter(
'[' + options.attribute + '="'+
$(this).attr(options.attribute) + '"]');
}
if ($matchedElement.length > 0) {
// There is a matching element in the $toDelete list and in $dest
// list, so make sure it is in the right location within $sourceParent
// and put it in the list of elements we need to not delete.
$keepElements = $keepElements.add($matchedElement);
if (i === 0) {
$sourceParent.prepend($matchedElement);
} else {
$matchedElement.insertAfter($sourceParent.find(options.selector).get(i - 1));
}
}
});
// Remove whatever is remaining from the DOM
$toDelete.not($keepElements).remove();
}
if (adjustHeightOnCallback) {
$sourceParent.css('height', destHeight);
}
if (adjustWidthOnCallback) {
$sourceParent.css('width', sourceWidth);
}
}
options.enhancement($sourceParent); // Perform custom visual enhancements on a newly replaced collection
if (typeof callbackFunction == 'function') {
callbackFunction.call(this);
}
}
if (false === options.adjustHeight) {
$sourceParent.css('height', 'auto');
}
if (false === options.adjustWidth) {
$sourceParent.css('width', 'auto');
}
};
// Position: relative situations
var $correctionParent = $sourceParent.offsetParent();
var correctionOffset = $correctionParent.offset();
if ($correctionParent.css('position') == 'relative') {
if ($correctionParent.get(0).nodeName.toLowerCase() == 'body') {
} else {
correctionOffset.top += (parseFloat($correctionParent.css('border-top-width')) || 0);
correctionOffset.left += (parseFloat($correctionParent.css('border-left-width')) || 0);
}
} else {
correctionOffset.top -= (parseFloat($correctionParent.css('border-top-width')) || 0);
correctionOffset.left -= (parseFloat($correctionParent.css('border-left-width')) || 0);
correctionOffset.top -= (parseFloat($correctionParent.css('margin-top')) || 0);
correctionOffset.left -= (parseFloat($correctionParent.css('margin-left')) || 0);
}
// perform custom corrections from options (use when Quicksand fails to detect proper correction)
if (isNaN(correctionOffset.left)) {
correctionOffset.left = 0;
}
if (isNaN(correctionOffset.top)) {
correctionOffset.top = 0;
}
correctionOffset.left -= options.dx;
correctionOffset.top -= options.dy;
// keeps nodes after source container, holding their position
$sourceParent.css('height', $(this).height());
$sourceParent.css('width', $(this).width());
// get positions of source collections
$source.each(function(i) {
offsets[i] = $(this).offset();
});
// stops previous animations on source container
$(this).stop();
var dx = 0;
var dy = 0;
$source.each(function(i) {
$(this).stop(); // stop animation of collection items
var rawObj = $(this).get(0);
if (rawObj.style.position == 'absolute') {
dx = -options.dx;
dy = -options.dy;
} else {
dx = options.dx;
dy = options.dy;
}
rawObj.style.position = 'absolute';
rawObj.style.margin = '0';
if (!options.adjustWidth) {
rawObj.style.width = (width + 'px'); // sets the width to the current element
// with even if it has been changed
// by a responsive design
}
rawObj.style.top = (offsets[i].top- parseFloat(rawObj.style.marginTop)
- correctionOffset.top + dy) + 'px';
rawObj.style.left = (offsets[i].left- parseFloat(rawObj.style.marginLeft)
- correctionOffset.left + dx) + 'px';
if (options.maxWidth > 0 && offsets[i].left > options.maxWidth)
rawObj.style.display = 'none';
});
// create temporary container with destination collection
var $dest = $($sourceParent).clone();
var rawDest = $dest.get(0);
rawDest.innerHTML = '';
rawDest.setAttribute('id', '');
rawDest.style.height = 'auto';
rawDest.style.width = $sourceParent.width() + 'px';
$dest.append($collection);
// Inserts node into HTML. Note that the node is under visible source container in the exactly same position
// The browser render all the items without showing them (opacity: 0.0) No offset calculations are needed,
// the browser just extracts position from underlayered destination items and sets animation to destination positions.
$dest.insertBefore($sourceParent);
$dest.css('opacity', 0.0);
rawDest.style.zIndex = -1;
rawDest.style.margin = '0';
rawDest.style.position = 'absolute';
rawDest.style.top = offset.top - correctionOffset.top + 'px';
rawDest.style.left = offset.left - correctionOffset.left + 'px';
if (options.adjustHeight === 'dynamic') {
// If destination container has different height than source container the height can be animated,
// adjusting it to destination height
$sourceParent.animate({ height : $dest.height() }, options.duration, options.easing);
} else if (options.adjustHeight === 'auto') {
destHeight = $dest.height();
if (parseFloat(sourceHeight) < parseFloat(destHeight)) {
// Adjust the height now so that the items don't move out of the container
$sourceParent.css('height', destHeight);
} else {
// Adjust later, on callback
adjustHeightOnCallback = true;
}
}
if (options.adjustWidth === 'dynamic') {
// If destination container has different width than source container the width can be animated,
// adjusting it to destination width
$sourceParent.animate({ width : $dest.width() }, options.duration, options.easing);
} else if (options.adjustWidth === 'auto') {
destWidth = $dest.width();
if (parseFloat(sourceWidth) < parseFloat(destWidth)) {
// Adjust the height now so that the items don't move out of the container
$sourceParent.css('width', destWidth);
} else {
// Adjust later, on callback
adjustWidthOnCallback = true;
}
}
// Now it's time to do shuffling animation. First of all, we need to identify same elements within
// source and destination collections
$source.each(function(i) {
var destElement = [];
if (typeof (options.attribute) == 'function') {
val = options.attribute($(this));
$collection.each(function() {
if (options.attribute(this) == val) {
destElement = $(this);
return false;
}
});
} else {
destElement = $collection.filter('[' + options.attribute + '="' + $(this).attr(options.attribute) + '"]');
}
if (destElement.length) {
// The item is both in source and destination collections. It it's under different position, let's move it
if (!options.useScaling) {
animationQueue.push({element : $(this), dest : destElement,
style : {
top : $(this).offset().top,
left : $(this).offset().left,
opacity : ""
},
animation : {
top : destElement.offset().top - correctionOffset.top,
left : destElement.offset().left - correctionOffset.left,
opacity : 1.0
}
});
} else {
animationQueue.push({element : $(this), dest : destElement,
style : {
top : $(this).offset().top,
left : $(this).offset().left,
opacity : ""
},
animation : {
top : destElement.offset().top - correctionOffset.top,
left : destElement.offset().left - correctionOffset.left,
opacity : 1.0,
scale : '1.0'
}
});
}
} else {
// The item from source collection is not present in destination collections. Let's remove it
if (!options.useScaling) {
animationQueue.push({element : $(this),
style : {
top : $(this).offset().top,
left : $(this).offset().left,
opacity : ""
},
animation : {
opacity : '0.0'
}
});
} else {
animationQueue.push({element : $(this),
animation : {
opacity : '0.0',
style : {
top : $(this).offset().top,
left : $(this).offset().left,
opacity : ""
},
scale : '0.0'
}
});
}
}
});
$collection.each(function(i) {
// Grab all items from target collection not present in visible source collection
var sourceElement = [];
var destElement = [];
if (typeof (options.attribute) == 'function') {
val = options.attribute($(this));
$source.each(function() {
if (options.attribute(this) == val) {
sourceElement = $(this);
return false;
}
});
$collection.each(function() {
if (options.attribute(this) == val) {
destElement = $(this);
return false;
}
});
} else {
sourceElement = $source.filter('[' + options.attribute + '="' + $(this).attr(options.attribute) + '"]');
destElement = $collection.filter('[' + options.attribute + '="' + $(this).attr(options.attribute) + '"]');
}
var animationOptions;
if (sourceElement.length === 0
&& destElement.length > 0) {
// No such element in source collection...
if (!options.useScaling) {
animationOptions = {opacity : '1.0'};
} else {
animationOptions = {opacity : '1.0', scale : '1.0'};
}
// Let's create it
var d = destElement.clone();
var rawDestElement = d.get(0);
rawDestElement.style.position = 'absolute';
rawDestElement.style.margin = '0';
if (!options.adjustWidth) {
// sets the width to the current element with even if it has been changed by a responsive design
rawDestElement.style.width = width + 'px';
}
rawDestElement.style.top = destElement.offset().top - correctionOffset.top + 'px';
rawDestElement.style.left = destElement.offset().left - correctionOffset.left + 'px';
d.css('opacity', 0.0); // IE
if (options.useScaling) {
d.css('transform', 'scale(0.0)');
}
d.appendTo($sourceParent);
if (options.maxWidth == 0 || destElement.offset().left < options.maxWidth) {
animationQueue.push({element : $(d), dest : destElement,animation : animationOptions});
}
}
});
$dest.remove();
if (!options.atomic) {
options.enhancement($sourceParent); // Perform custom visual enhancements during the animation
for (i = 0; i < animationQueue.length; i++) {
animationQueue[i].element.animate(animationQueue[i].animation, options.duration, options.easing,
postCallback);
}
} else {
$toDelete = $sourceParent.find(options.selector);
$sourceParent.prepend($dest.find(options.selector));
for (i = 0; i < animationQueue.length; i++) {
if (animationQueue[i].dest && animationQueue[i].style) {
var destElement = animationQueue[i].dest;
var offset = destElement.offset();
destElement.css({position : 'relative',
top : (animationQueue[i].style.top - offset.top),
left : (animationQueue[i].style.left - offset.left)
});
destElement.animate({top : "0", left : "0"}, options.duration, options.easing, postCallback);
} else {
animationQueue[i].element.animate(animationQueue[i].animation, options.duration, options.easing,
postCallback);
}
}
$toDelete.remove();
}
});
};
})(jQuery);