/* 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 usescaling: true, // disable it if you're not using scaling effect or want to improve performance enhancement: function(c) {}, // visual enhacement (eg. font replacement) function for cloned elements selector: '> *', dx: 0, dy: 0 }; $.extend(options, customoptions); if ($.browser.msie || (typeof($.fn.scale) == 'undefined')) { // got ie and want scaling effect? kiss my ass. 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 var $collection = $(collection).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 destheight; var adjustheightoncallback = 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 // replace the collection and quit if ie6 if ($.browser.msie && $.browser.version.substr(0,1)<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 () { if (!postcallbackperformed) { postcallbackperformed = 1; // 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 $todelete = $sourceparent.find('> *'); $sourceparent.prepend($dest.find('> *')); $todelete.remove(); if (adjustheightoncallback) { $sourceparent.css('height', destheight); } options.enhancement($sourceparent); // perform custom visual enhancements on a newly replaced collection if (typeof callbackfunction == 'function') { callbackfunction.call(this); } } }; // 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()); // 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'; 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'; }); // 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); // insert 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; } } // 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), animation: {top: destelement.offset().top - correctionoffset.top, left: destelement.offset().left - correctionoffset.left, opacity: 1.0 } }); } else { animationqueue.push({ element: $(this), 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), animation: {opacity: '0.0'}}); } else { animationqueue.push({element: $(this), animation: {opacity: '0.0', 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) { // 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 d = destelement.clone(); var rawdestelement = d.get(0); rawdestelement.style.position = 'absolute'; rawdestelement.style.margin = '0'; 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); animationqueue.push({element: $(d), animation: animationoptions}); } }); $dest.remove(); 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); } }); }; })(jquery);