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