Added style queue to prevent multiple reflows during layout. Added randomize option to sorting.

Other general improvements.
pull/56/head
Glen Cheney 11 years ago
parent efeb3c7dc4
commit 970f4cd6f9

@ -12,19 +12,29 @@
* Inspired by Isotope http://isotope.metafizzy.co/
* Use it for whatever you want!
* @author Glen Cheney (http://glencheney.com)
* @version 1.6.1
* @date 2/4/13
* @version 1.6.2
* @date 03/06/13
*/
(function($, Modernizr, undefined) {
'use strict';
// You can return `undefined` from the `by` function to revert to DOM order
// This plugin does NOT return a jQuery object. It returns a plain array because
// jQuery sorts everything in DOM order.
$.fn.sorted = function(options) {
var opts = $.extend({}, $.fn.sorted.defaults, options),
arr = this.get(),
revert = false;
if ( !arr.length ) {
return [];
}
if ( opts.randomize ) {
return $.fn.sorted.randomize( arr );
}
// Sort the elements by the opts.by function.
// If we don't have opts.by, default to DOM order
if (opts.by !== $.noop && opts.by !== null && opts.by !== undefined) {
@ -64,9 +74,32 @@
$.fn.sorted.defaults = {
reverse: false,
by: null
by: null,
randomize: false
};
// http://stackoverflow.com/a/962890/373422
$.fn.sorted.randomize = function( array ) {
var top = array.length,
tmp, current;
if ( !top ) {
return array;
}
while ( --top ) {
current = Math.floor( Math.random() * (top + 1) );
tmp = array[ current ];
array[ current ] = array[ top ];
array[ top ] = tmp;
}
return array;
};
var Shuffle = function( $container, options ) {
var self = this;
@ -74,6 +107,7 @@
self.$container = $container.addClass('shuffle');
self.$window = $(window);
self.unique = 'shuffle_' + $.now();
self.fire('loading');
self._init();
@ -88,8 +122,16 @@
var self = this,
transEndEventNames,
resizeFunc = $.proxy( self._onResize, self ),
throttledResize = self.throttle ? self.throttle( self.throttleTime, resizeFunc ) : resizeFunc;
afterResizeFunc = $.proxy( self._afterResize, self ),
beforeResizeFunc;
if ( self.hideLayoutWithFade ) {
beforeResizeFunc = $.proxy( self._beforeResize, self );
self._debouncedBeforeResize = self.throttle ? self.throttle( self.throttleTime, true, beforeResizeFunc ) : beforeResizeFunc;
}
// Get debounced versions of our resize methods
self._debouncedResize = self.throttle ? self.throttle( self.throttleTime, afterResizeFunc ) : afterResizeFunc;
self.$items = self._getItems().addClass('shuffle-item');
self.transitionName = self.prefixed('transition'),
@ -128,9 +170,9 @@
self._initItems( !self.showInitialTransition );
// http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
self.windowHeight = self.$window.height();
self.windowWidth = self.$window.width();
self.$window.on('resize.shuffle', throttledResize);
// self.windowHeight = self.$window.height();
// self.windowWidth = self.$window.width();
self.$window.on('resize.shuffle', resizeFunc);
self._setColumns();
self._resetCols();
@ -142,36 +184,6 @@
}
},
/**
* The magic. This is what makes the plugin 'shuffle'
*/
shuffle : function( category ) {
var self = this;
if ( !self.enabled ) {
return;
}
if (!category) {
category = 'all';
}
self.filter( category );
// Save the last filter in case elements are appended.
self.lastFilter = category;
// How many filtered elements?
self.visibleItems = self.$items.filter('.filtered').length;
self._resetCols();
// Shrink each concealed item
self.shrink();
// Update transforms on .filtered elements so they will animate to their new positions
self._reLayout();
},
filter : function( category, $collection ) {
var self = this,
isPartialSet = $collection !== undefined,
@ -227,14 +239,16 @@
return $filtered;
},
_initItems : function( withoutTransition ) {
_initItems : function( withoutTransition, $items ) {
var self = this;
self.$items.each(function() {
$items = $items || self.$items;
$items.each(function() {
$(this).css(self.itemCss);
// Set CSS transition for transforms and opacity
if ( self.supported && !withoutTransition ) {
if ( self.supported && !withoutTransition && self.useTransition ) {
self._setTransition(this);
}
});
@ -245,6 +259,7 @@
element.style[self.transitionName] = self.transform + ' ' + self.speed + 'ms ' + self.easing + ', opacity ' + self.speed + 'ms ' + self.easing;
},
_setSequentialDelay : function( $collection ) {
var self = this;
@ -252,16 +267,73 @@
return;
}
$collection.each(function(i) {
this.style[self.transitionName + 'Delay'] = ((i + 1) * self.sequentialFadeDelay) + 'ms';
// $collection can be an array of dom elements or jquery object
$.each( $collection, function(i) {
// This works because the transition-property: transform, opacity;
this.style[self.transitionName + 'Delay'] = '0ms,' + ((i + 1) * self.sequentialFadeDelay) + 'ms';
// Set the delay back to zero after one transition
$(this).one($.support.transition.end, function() {
$(this).one(self.transitionEndName, function() {
this.style[self.transitionName + 'Delay'] = '0ms';
});
});
},
_resetDelay : function( $collection ) {
var self = this;
if ( !self.supported ) {
return;
}
$.each( $collection, function() {
$(this).off( self.transitionEndName );
this.style[ self.transitionName + 'Delay' ] = '0ms';
});
},
_orderItemsByDelay : function( $items ) {
var self = this,
$ordered = $(),
$randomized = $(),
ordered, randomized, merged,
by = function( $el ) {
return $el.data('delayOrder');
};
if ( !self.$ordered ) {
$items.each(function() {
var delayOrder = $(this).data('delayOrder');
if ( delayOrder ) {
$ordered = $ordered.add( this );
} else {
$randomized = $randomized.add( this );
}
});
ordered = $ordered.sorted({ by: by });
} else {
$ordered = self.$ordered;
ordered = self.ordered;
$randomized = $items.not( $ordered );
}
// Sort randomly
randomized = $randomized.sorted({ randomize: true });
// Merge with the ordered array
merged = ordered.concat( randomized );
// Save values
self.$ordered = $ordered;
self.ordered = ordered;
self.itemsOrderedByDelay = merged;
},
_getItems : function() {
return this.$container.children( this.itemSelector );
},
@ -297,6 +369,8 @@
} else {
self.needsUpdate = false;
}
self.containerWidth = containerWidth;
},
/**
@ -322,9 +396,9 @@
* @param {array} items - array of items that will be shown/layed out in order in their array.
* Because jQuery collection are always ordered in DOM order, we can't pass a jq collection
* @param {function} complete callback function
* @param {boolean} onlyPosition set this to true to only trigger positioning of the items
* @param {boolean} isOnlyPosition set this to true to only trigger positioning of the items
*/
_layout: function( items, fn, onlyPosition ) {
_layout: function( items, fn, isOnlyPosition, isHide ) {
var self = this;
fn = fn || self.filterEnd;
@ -338,7 +412,7 @@
if ( colSpan === 1 ) {
// if brick spans only one column, just like singleMode
self._placeItem( $this, self.colYs, fn );
self._placeItem( $this, self.colYs, fn, isOnlyPosition, isHide );
} else {
// brick spans more than one column
// how many different places could this brick fit horizontally
@ -355,10 +429,14 @@
groupY[i] = Math.max.apply( Math, groupColY );
}
self._placeItem( $this, groupY, fn, onlyPosition );
self._placeItem( $this, groupY, fn, isOnlyPosition, isHide );
}
});
// `_layout` always happens after `shrink`, so it's safe to process the style
// queue here with styles from the shrink method
self._processStyleQueue();
// Adjust the height of the container
self.setContainerSize();
},
@ -372,21 +450,21 @@
}
},
_reLayout : function( callback ) {
_reLayout : function( callback, isOnlyPosition ) {
var self = this;
callback = callback || self.filterEnd;
self._resetCols();
// If we've already sorted the elements, keep them sorted
if ( self.keepSorted && self.lastSort ) {
self.sort( self.lastSort, true );
self.sort( self.lastSort, true, isOnlyPosition );
} else {
self._layout( self.$items.filter('.filtered').get(), self.filterEnd );
self._layout( self.$items.filter('.filtered').get(), self.filterEnd, isOnlyPosition );
}
},
// worker method that places brick in the columnSet with the the minY
_placeItem : function( $item, setY, callback, onlyPosition ) {
_placeItem : function( $item, setY, callback, isOnlyPosition, isHide ) {
// get the minimum Y value from the columns
var self = this,
minimumY = Math.min.apply( Math, setY ),
@ -418,29 +496,26 @@
self.colYs[ shortCol + i ] = setHeight;
}
if ( onlyPosition ) {
self._skipTransition($item[0], function() {
self.transition({
from: 'layout',
$this: $item,
x: x,
y: y,
// scale : 1,
opacity: 0
});
});
var transitionObj = {
from: 'layout',
$this: $item,
x: x,
y: y,
scale: 1
};
if ( !isOnlyPosition ) {
transitionObj.opacity = 1;
transitionObj.callback = callback;
} else {
self.transition({
from: 'layout',
$this: $item,
x: x,
y: y,
scale : 1,
opacity: 1,
callback: callback
});
transitionObj.skipTransition = true;
}
if ( isHide ) {
transitionObj.opacity = 0;
}
self.styleQueue.push( transitionObj );
},
/**
@ -448,7 +523,8 @@
*/
shrink : function() {
var self = this,
$concealed = self.$items.filter('.concealed');
$concealed = self.$items.filter('.concealed'),
transitionObj = {};
// Abort if no items
if ($concealed.length === 0) {
@ -467,7 +543,7 @@
if (!x) { x = 0; }
if (!y) { y = 0; }
self.transition({
transitionObj = {
from: 'shrink',
$this: $this,
x: x,
@ -475,44 +551,61 @@
scale : 0.001,
opacity: 0,
callback: self.shrinkEnd
});
};
self.styleQueue.push( transitionObj );
});
},
_onResize : function() {
var self = this;
var self = this,
containerWidth = self.$container.width();
if ( !self.enabled ) {
// If shuffle is disabled, destroyed, or containerWidth hasn't changed, don't do anything
if ( !self.enabled || self.destroyed || containerWidth === self.containerWidth ) {
return;
}
var height = self.$window.height(),
width = self.$window.width();
// This should execute the first time _onResize is called
if ( self.hideLayoutWithFade ) {
self._debouncedBeforeResize();
}
// This should execute the last time _onResize is called
self._debouncedResize();
},
if (width !== self.windowWidth || height !== self.windowHeight) {
_afterResize : function() {
var self = this;
// If we're hiding the layout with a fade,
if ( self.hideLayoutWithFade && self.supported ) {
// recaculate column and gutter values
self._setColumns();
// Layout the items with only a position
self._reLayout( false, true );
// Change the transition-delay value accordingly
self._setSequentialDelay( self.itemsOrderedByDelay );
self.fire('done');
self.$items.css('opacity', 1);
} else {
self.resized();
self.windowHeight = height;
self.windowWidth = width;
}
},
/**
* Gets the .filtered elements, sorts them, and passes them to layout
*
* @param {object} opts the options object for the sorted plugin
* @param {bool} [fromFilter] was called from Shuffle.filter method.
*/
sort: function(opts, fromFilter) {
var self = this,
items = self.$items.filter('.filtered').sorted(opts);
self._resetCols();
self._layout(items, function() {
if (fromFilter) {
self.filterEnd();
}
self.sortEnd();
});
self.lastSort = opts;
_beforeResize : function() {
var self = this;
if ( !self.supported ) {
return;
}
self.$items.css('opacity', 0);
self.fire('loading');
self._resetDelay( self.$items );
self._orderItemsByDelay( self.$items );
},
/**
@ -571,7 +664,7 @@
opts.callback = opts.callback || $.noop;
// Use CSS Transforms if we have them
if (self.supported) {
if ( self.supported ) {
// Make scale one if it's not preset
if ( opts.scale === undefined ) {
@ -584,38 +677,55 @@
transform = 'translate(' + opts.x + 'px, ' + opts.y + 'px) scale(' + opts.scale + ', ' + opts.scale + ')';
}
// Update css to trigger CSS Animation
opts.$this.css('opacity' , opts.opacity);
if ( opts.opacity !== undefined && self.useTransition ) {
// Update css to trigger CSS Animation
opts.$this.css('opacity' , opts.opacity);
}
if ( opts.x !== undefined ) {
self.setPrefixedCss(opts.$this, 'transform', transform);
}
opts.$this.one(self.transitionEndName, complete);
if ( self.useTransition ) {
opts.$this.one(self.transitionEndName, complete);
} else {
complete();
}
} else {
// Use jQuery to animate left/top
opts.$this.stop().animate({
var cssObj = {
left: opts.x,
top: opts.y,
opacity: opts.opacity
}, self.speed, 'swing', complete);
};
if ( self.useTransition ) {
// Use jQuery to animate left/top
opts.$this.stop(true, true).animate( cssObj, self.speed, 'swing', complete);
} else {
opts.$this.css( cssObj );
complete();
}
}
},
/**
* Relayout everything
*/
resized: function( isOnlyLayout ) {
if ( this.enabled ) {
_processStyleQueue : function() {
var self = this,
queue = self.styleQueue;
if ( !isOnlyLayout ) {
// Get updated colCount
this._setColumns();
$.each( queue, function(i, transitionObj) {
if ( transitionObj.skipTransition ) {
self._skipTransition( transitionObj.$this[0], function() {
self.transition( transitionObj );
});
} else {
self.transition( transitionObj );
}
});
// Layout items
this._reLayout();
}
// Remove everything in the style queue
self.styleQueue.length = 0;
},
shrinkEnd: function() {
@ -645,36 +755,35 @@
element.style[ property ] = value;
}
reflow = element.offsetWidth; // Force reflow
// Force reflow
reflow = element.offsetWidth;
// Put the duration back
element.style[ durationName ] = duration;
},
appended : function( $newItems, animateIn, isSequential ) {
// True if undefined
animateIn = animateIn === false ? false : true;
isSequential = isSequential === false ? false : true;
this._addItems( $newItems, animateIn, isSequential );
},
_addItems : function( $newItems, animateIn, isSequential ) {
var self = this,
$passed;
$passed,
passed;
if ( !self.supported ) {
animateIn = false;
}
$newItems.addClass('shuffle-item');
self._initItems( undefined, $newItems );
self.$items = self._getItems();
self._initItems();
$newItems.not($passed).css('opacity', 0);
$passed = self.filter( undefined, $newItems );
passed = $passed.get();
// How many filtered elements?
self.visibleItems = self.$items.filter('.filtered').length;
if ( animateIn ) {
self._layout( $passed, null, true );
self._layout( passed, null, true, true );
if ( isSequential ) {
self._setSequentialDelay( $passed );
@ -682,7 +791,7 @@
self._revealAppended( $passed );
} else {
self._layout( $passed );
self._layout( passed );
}
},
@ -700,7 +809,86 @@
}, self.revealAppendedDelay);
},
// Use this instead of `update()` if you don't need the columns and gutters updated
/**
* Public Methods
*/
/**
* The magic. This is what makes the plugin 'shuffle'
* @param {String|Function} category category to filter by. Can be a function
*/
shuffle : function( category ) {
var self = this;
if ( !self.enabled ) {
return;
}
if (!category) {
category = 'all';
}
self.filter( category );
// Save the last filter in case elements are appended.
self.lastFilter = category;
// How many filtered elements?
self.visibleItems = self.$items.filter('.filtered').length;
self._resetCols();
// Shrink each concealed item
self.shrink();
// Update transforms on .filtered elements so they will animate to their new positions
self._reLayout();
},
/**
* Gets the .filtered elements, sorts them, and passes them to layout
*
* @param {object} opts the options object for the sorted plugin
* @param {Boolean} [fromFilter] was called from Shuffle.filter method.
*/
sort: function( opts, fromFilter, isOnlyPosition ) {
var self = this,
items = self.$items.filter('.filtered').sorted(opts);
if ( !fromFilter ) {
self._resetCols();
}
self._layout(items, function() {
if (fromFilter) {
self.filterEnd();
}
self.sortEnd();
}, isOnlyPosition);
self.lastSort = opts;
},
/**
* Relayout everything
*/
resized: function( isOnlyLayout ) {
if ( this.enabled ) {
if ( !isOnlyLayout ) {
// Get updated colCount
this._setColumns();
}
// Layout items
this._reLayout();
}
},
/**
* Use this instead of `update()` if you don't need the columns and gutters updated
* Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
* could be off.
*/
layout : function() {
this.update( true );
},
@ -709,6 +897,20 @@
this.resized( isOnlyLayout );
},
/**
* New items have been appended to shuffle. Fade them in sequentially
* @param {jQuery} $newItems jQuery collection of new items
* @param {Boolean} [animateIn] If false, the new items won't animate in
* @param {Boolean} [isSequential] If false, new items won't sequentially fade in
*/
appended : function( $newItems, animateIn, isSequential ) {
// True if undefined
animateIn = animateIn === false ? false : true;
isSequential = isSequential === false ? false : true;
this._addItems( $newItems, animateIn, isSequential );
},
disable : function() {
this.enabled = false;
},
@ -724,8 +926,9 @@
var self = this;
self.$container.removeAttr('style').removeData('shuffle');
$(window).off('.shuffle');
self.$window.off('.shuffle');
self.$items.removeAttr('style').removeClass('concealed filtered shuffle-item');
self.destroyed = true;
}
};
@ -773,24 +976,27 @@
// Overrideable options
$.fn.shuffle.options = {
group : 'all', // Filter group
speed : 600, // Transition/animation speed (milliseconds)
speed : 400, // Transition/animation speed (milliseconds)
easing : 'ease-out', // css easing function to use
itemSelector: '', // e.g. '.gallery-item'
gutterWidth : 0, // a static number or function that tells the plugin how wide the gutters between columns are (in pixels)
columnWidth : 0,// a static number or function that returns a number which tells the plugin how wide the columns are (in pixels)
showInitialTransition : true, // If set to false, the shuffle-items will only have a transition applied to them after the first layout
delimeter : null, // if your group is not json, and is comma delimeted, you could set delimeter to ','
buffer: 0,
buffer: 0, // useful for percentage based heights when they might not always be exactly the same (in pixels)
throttle: $.debounce || null,
throttleTime: 300,
keepSorted : true
keepSorted : true, // Keep sorted when shuffling/layout
hideLayoutWithFade: false,
sequentialFadeDelay: 150,
useTransition: true // You don't want transitions on shuffle items? Fine, but you're weird
};
// Not overrideable
$.fn.shuffle.settings = {
sequentialFadeDelay: 250,
revealAppendedDelay: 300,
enabled: true,
styleQueue: [],
supported: Modernizr.csstransforms && Modernizr.csstransitions, // supports transitions and transforms
prefixed: Modernizr.prefixed,
threeD: Modernizr.csstransforms3d // supports 3d transforms

Loading…
Cancel
Save