Fix callback events when using jQuery.animate.

Replaced setTimeout/proxy with a defer method which can take a context.
Changed jQuery to use v1 instead of v2 in testing so that old IE can
use the tests.
Add a few specs where `supported = false` so jQuery.animate will be
tested.
Add isTransitioning flag to determine when shuffle is already doing
something.
pull/56/head
Glen Cheney 10 years ago
parent a40cc67a90
commit 7ad8133147

@ -44,11 +44,11 @@ module.exports = function(grunt) {
},
src: {
files: 'src/*.js',
tasks: ['concat']
tasks: ['concat', 'test']
},
test: {
files: 'test/specs.js',
tasks: ['jasmine:main']
tasks: ['test']
}
},
@ -117,7 +117,7 @@ module.exports = function(grunt) {
specs: 'test/specs.js',
vendor: [
'dist/modernizr.custom.min.js',
'http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js',
'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js',
'bower_components/jasmine-jquery/lib/jasmine-jquery.js'
],
outfile: 'test/_SpecRunner.html',

@ -108,6 +108,11 @@ function each(obj, iterator, context) {
}
}
function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
// Used for unique instance variables
var id = 0;
var $window = $( window );
@ -133,10 +138,10 @@ var Shuffle = function( element, options ) {
// Dispatch the done event asynchronously so that people can bind to it after
// Shuffle has been initialized.
setTimeout( $.proxy(function() {
defer(function() {
this.initialized = true;
this._fire( Shuffle.EventType.DONE );
}, this), 16 );
}, this, 16);
};
@ -317,10 +322,10 @@ Shuffle.prototype._init = function() {
// The shuffle items haven't had transitions set on them yet
// so the user doesn't see the first layout. Set them now that the first layout is done.
if ( this.supported ) {
setTimeout($.proxy(function() {
defer(function() {
this._setTransitions();
this.element.style[ TRANSITION ] = 'height ' + this.speed + 'ms ' + this.easing;
}, this), 0);
}, this);
}
};
@ -474,7 +479,7 @@ Shuffle.prototype._initItems = function( $items ) {
Shuffle.ClassName.SHUFFLE_ITEM,
Shuffle.ClassName.FILTERED
].join(' '));
$items.css( this.itemCss ).data('position', {x: 0, y: 0});
$items.css( this.itemCss ).data('position', {x: 0, y: 0}).data('scale', DEFAULT_SCALE);
};
/**
@ -655,8 +660,6 @@ Shuffle.prototype._fire = function( name, args ) {
* @param {boolean} isOnlyPosition If true this will position the items with zero opacity.
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
var added = 0;
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
@ -665,6 +668,7 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
@ -691,7 +695,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
}
};
added++;
this.styleQueue.push( transitionObj );
}, this);
@ -699,12 +702,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// queue here with styles from the shrink method
this._processStyleQueue();
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
if ( items.length > 0 && added === 0 ) {
this._layoutEnd();
}
// Adjust the height of the container
this._setContainerSize();
};
@ -813,6 +810,8 @@ Shuffle.prototype._shrink = function( $collection ) {
return;
}
$item.data( 'scale', CONCEALED_SCALE );
var transitionObj = {
$item: $item,
x: itemData.position.x,
@ -834,7 +833,7 @@ Shuffle.prototype._shrink = function( $collection ) {
*/
Shuffle.prototype._onResize = function() {
// If shuffle is disabled, destroyed, don't do anything
if ( !this.enabled || this.destroyed ) {
if ( !this.enabled || this.destroyed || this.isTransitioning ) {
return;
}
@ -887,16 +886,13 @@ Shuffle.prototype._getStylesForTransition = function( opts ) {
* @private
*/
Shuffle.prototype._transition = function( opts ) {
opts.$item.data('scale', opts.scale);
var styles = this._getStylesForTransition( opts );
this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
};
Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
callfront();
// Transition end handler removes its listener.
function handleTransitionEnd( evt ) {
// Make sure this event handler has not bubbled up from a child.
if ( evt.target === evt.currentTarget ) {
@ -905,29 +901,34 @@ Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, call
}
}
callfront();
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( !this.initialized ) {
$item.css( styles );
callback();
return;
}
// Use CSS Transforms if we have them
if ( this.supported ) {
$item.css( styles );
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( this.initialized ) {
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
} else {
callback();
}
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
// Use jQuery to animate left/top
} else {
$item.stop( true ).animate( styles, this.speed, 'swing', callback );
var anim = $item.stop( true ).animate( styles, this.speed, 'swing', callback );
this._animations.push( anim.promise() );
}
};
Shuffle.prototype._processStyleQueue = function() {
Shuffle.prototype._processStyleQueue = function( noLayout ) {
var $transitions = $();
// Iterate over the queue and keep track of ones that use transitions.
each(this.styleQueue, function( transitionObj ) {
if ( transitionObj.skipTransition ) {
this._styleImmediately( transitionObj );
@ -937,13 +938,24 @@ Shuffle.prototype._processStyleQueue = function() {
}
}, this);
if ( $transitions.length > 0 ) {
if ( this.initialized && this.supported ) {
// TODO: Transitioning flag.
this._whenCollectionDone( $transitions, TRANSITIONEND, this._layoutEnd );
if ( $transitions.length > 0 && this.initialized ) {
// Set flag that shuffle is currently in motion.
this.isTransitioning = true;
if ( this.supported ) {
this._whenCollectionDone( $transitions, TRANSITIONEND, this._movementFinished );
// The _transition function appends a promise to the animations array.
// When they're all complete, do things.
} else {
this._layoutEnd();
this._whenAnimationsDone( this._movementFinished );
}
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
} else if ( !noLayout ) {
defer( this._layoutEnd, this );
}
// Remove everything in the style queue
@ -956,8 +968,13 @@ Shuffle.prototype._styleImmediately = function( opts ) {
}, this);
};
Shuffle.prototype._movementFinished = function() {
this.isTransitioning = false;
this._layoutEnd();
};
Shuffle.prototype._layoutEnd = function() {
setTimeout($.proxy( this._fire, this, Shuffle.EventType.LAYOUT ), 0);
this._fire( Shuffle.EventType.LAYOUT );
};
Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
@ -975,7 +992,9 @@ Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
each(this.styleQueue, function( transitionObj ) {
transitionObj.skipTransition = true;
});
this._processStyleQueue();
// Apply shrink positions, but do not cause a layout event.
this._processStyleQueue( true );
if ( addToEnd ) {
this._addItemsToEnd( $newItems, isSequential );
@ -1008,7 +1027,7 @@ Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
* @private
*/
Shuffle.prototype._revealAppended = function( newFilteredItems ) {
setTimeout($.proxy(function() {
defer(function() {
each(newFilteredItems, function( el ) {
this._transition({
$item: $(el),
@ -1019,8 +1038,9 @@ Shuffle.prototype._revealAppended = function( newFilteredItems ) {
this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
$(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
this._movementFinished();
});
}, this), this.revealAppendedDelay);
}, this, this.revealAppendedDelay);
};
@ -1039,7 +1059,6 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
function handleEventName( evt ) {
if ( evt.target === evt.currentTarget ) {
$( evt.target ).off( eventName, handleEventName );
done++;
// Execute callback if all items have emitted the correct event.
@ -1054,6 +1073,19 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
};
/**
* Execute a callback after jQuery `animate` for a collection has finished.
* @param {Function} callback Callback to execute when they're done.
* @private
*/
Shuffle.prototype._whenAnimationsDone = function( callback ) {
$.when.apply( null, this._animations ).always( $.proxy( function() {
this._animations.length = 0;
callback.call( this );
}, this ));
};
/**
* Public Methods
*/
@ -1064,7 +1096,7 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
* @param {Object} [sortObj] A sort object which can sort the filtered set
*/
Shuffle.prototype.shuffle = function( category, sortObj ) {
if ( !this.enabled ) {
if ( !this.enabled || this.isTransitioning ) {
return;
}
@ -1089,14 +1121,16 @@ Shuffle.prototype.shuffle = function( category, sortObj ) {
* @param {Object} opts the options object for the sorted plugin
*/
Shuffle.prototype.sort = function( opts ) {
this._resetCols();
if ( this.enabled && !this.isTransitioning ) {
this._resetCols();
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
this._layout( items );
this._layout( items );
this.lastSort = opts;
this.lastSort = opts;
}
};
/**
@ -1105,7 +1139,7 @@ Shuffle.prototype.sort = function( opts ) {
* recalculated.
*/
Shuffle.prototype.update = function( isOnlyLayout ) {
if ( this.enabled ) {
if ( this.enabled && !this.isTransitioning ) {
if ( !isOnlyLayout ) {
// Get updated colCount
@ -1167,25 +1201,27 @@ Shuffle.prototype.remove = function( $collection ) {
return;
}
this._whenCollectionDone( $collection, TRANSITIONEND, function() {
function handleRemoved() {
// Remove the collection in the callback
$collection.remove();
// Update the items, layout, count and fire off `removed` event
// Update things now that elements have been removed.
this.$items = this._getItems();
this.layout();
this._updateItemCount();
this._fire( Shuffle.EventType.REMOVED, [ $collection, this ] );
// Let it get garbage collected
$collection = null;
});
}
// Hide collection first.
this._toggleFilterClasses( $(), $collection );
this._shrink( $collection );
// Process changes
this._processStyleQueue();
this.sort();
this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
};
/**
@ -1251,7 +1287,8 @@ Shuffle.settings = {
itemCss : { // default CSS for each item
position: 'absolute',
top: 0,
left: 0
left: 0,
visibility: 'visible'
},
offset: { top: 0, left: 0 },
revealAppendedDelay: 300,
@ -1260,6 +1297,7 @@ Shuffle.settings = {
enabled: true,
destroyed: false,
initialized: false,
_animations: [],
styleQueue: []
};

File diff suppressed because one or more lines are too long

@ -114,6 +114,11 @@ function each(obj, iterator, context) {
}
}
function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
// Used for unique instance variables
var id = 0;
var $window = $( window );
@ -139,10 +144,10 @@ var Shuffle = function( element, options ) {
// Dispatch the done event asynchronously so that people can bind to it after
// Shuffle has been initialized.
setTimeout( $.proxy(function() {
defer(function() {
this.initialized = true;
this._fire( Shuffle.EventType.DONE );
}, this), 16 );
}, this, 16);
};
@ -323,10 +328,10 @@ Shuffle.prototype._init = function() {
// The shuffle items haven't had transitions set on them yet
// so the user doesn't see the first layout. Set them now that the first layout is done.
if ( this.supported ) {
setTimeout($.proxy(function() {
defer(function() {
this._setTransitions();
this.element.style[ TRANSITION ] = 'height ' + this.speed + 'ms ' + this.easing;
}, this), 0);
}, this);
}
};
@ -480,7 +485,7 @@ Shuffle.prototype._initItems = function( $items ) {
Shuffle.ClassName.SHUFFLE_ITEM,
Shuffle.ClassName.FILTERED
].join(' '));
$items.css( this.itemCss ).data('position', {x: 0, y: 0});
$items.css( this.itemCss ).data('position', {x: 0, y: 0}).data('scale', DEFAULT_SCALE);
};
/**
@ -661,8 +666,6 @@ Shuffle.prototype._fire = function( name, args ) {
* @param {boolean} isOnlyPosition If true this will position the items with zero opacity.
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
var added = 0;
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
@ -671,6 +674,7 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
@ -697,7 +701,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
}
};
added++;
this.styleQueue.push( transitionObj );
}, this);
@ -705,12 +708,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// queue here with styles from the shrink method
this._processStyleQueue();
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
if ( items.length > 0 && added === 0 ) {
this._layoutEnd();
}
// Adjust the height of the container
this._setContainerSize();
};
@ -819,6 +816,8 @@ Shuffle.prototype._shrink = function( $collection ) {
return;
}
$item.data( 'scale', CONCEALED_SCALE );
var transitionObj = {
$item: $item,
x: itemData.position.x,
@ -840,7 +839,7 @@ Shuffle.prototype._shrink = function( $collection ) {
*/
Shuffle.prototype._onResize = function() {
// If shuffle is disabled, destroyed, don't do anything
if ( !this.enabled || this.destroyed ) {
if ( !this.enabled || this.destroyed || this.isTransitioning ) {
return;
}
@ -893,16 +892,13 @@ Shuffle.prototype._getStylesForTransition = function( opts ) {
* @private
*/
Shuffle.prototype._transition = function( opts ) {
opts.$item.data('scale', opts.scale);
var styles = this._getStylesForTransition( opts );
this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
};
Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
callfront();
// Transition end handler removes its listener.
function handleTransitionEnd( evt ) {
// Make sure this event handler has not bubbled up from a child.
if ( evt.target === evt.currentTarget ) {
@ -911,29 +907,34 @@ Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, call
}
}
callfront();
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( !this.initialized ) {
$item.css( styles );
callback();
return;
}
// Use CSS Transforms if we have them
if ( this.supported ) {
$item.css( styles );
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( this.initialized ) {
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
} else {
callback();
}
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
// Use jQuery to animate left/top
} else {
$item.stop( true ).animate( styles, this.speed, 'swing', callback );
var anim = $item.stop( true ).animate( styles, this.speed, 'swing', callback );
this._animations.push( anim.promise() );
}
};
Shuffle.prototype._processStyleQueue = function() {
Shuffle.prototype._processStyleQueue = function( noLayout ) {
var $transitions = $();
// Iterate over the queue and keep track of ones that use transitions.
each(this.styleQueue, function( transitionObj ) {
if ( transitionObj.skipTransition ) {
this._styleImmediately( transitionObj );
@ -943,13 +944,24 @@ Shuffle.prototype._processStyleQueue = function() {
}
}, this);
if ( $transitions.length > 0 ) {
if ( this.initialized && this.supported ) {
// TODO: Transitioning flag.
this._whenCollectionDone( $transitions, TRANSITIONEND, this._layoutEnd );
if ( $transitions.length > 0 && this.initialized ) {
// Set flag that shuffle is currently in motion.
this.isTransitioning = true;
if ( this.supported ) {
this._whenCollectionDone( $transitions, TRANSITIONEND, this._movementFinished );
// The _transition function appends a promise to the animations array.
// When they're all complete, do things.
} else {
this._layoutEnd();
this._whenAnimationsDone( this._movementFinished );
}
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
} else if ( !noLayout ) {
defer( this._layoutEnd, this );
}
// Remove everything in the style queue
@ -962,8 +974,13 @@ Shuffle.prototype._styleImmediately = function( opts ) {
}, this);
};
Shuffle.prototype._movementFinished = function() {
this.isTransitioning = false;
this._layoutEnd();
};
Shuffle.prototype._layoutEnd = function() {
setTimeout($.proxy( this._fire, this, Shuffle.EventType.LAYOUT ), 0);
this._fire( Shuffle.EventType.LAYOUT );
};
Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
@ -981,7 +998,9 @@ Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
each(this.styleQueue, function( transitionObj ) {
transitionObj.skipTransition = true;
});
this._processStyleQueue();
// Apply shrink positions, but do not cause a layout event.
this._processStyleQueue( true );
if ( addToEnd ) {
this._addItemsToEnd( $newItems, isSequential );
@ -1014,7 +1033,7 @@ Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
* @private
*/
Shuffle.prototype._revealAppended = function( newFilteredItems ) {
setTimeout($.proxy(function() {
defer(function() {
each(newFilteredItems, function( el ) {
this._transition({
$item: $(el),
@ -1025,8 +1044,9 @@ Shuffle.prototype._revealAppended = function( newFilteredItems ) {
this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
$(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
this._movementFinished();
});
}, this), this.revealAppendedDelay);
}, this, this.revealAppendedDelay);
};
@ -1045,7 +1065,6 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
function handleEventName( evt ) {
if ( evt.target === evt.currentTarget ) {
$( evt.target ).off( eventName, handleEventName );
done++;
// Execute callback if all items have emitted the correct event.
@ -1060,6 +1079,19 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
};
/**
* Execute a callback after jQuery `animate` for a collection has finished.
* @param {Function} callback Callback to execute when they're done.
* @private
*/
Shuffle.prototype._whenAnimationsDone = function( callback ) {
$.when.apply( null, this._animations ).always( $.proxy( function() {
this._animations.length = 0;
callback.call( this );
}, this ));
};
/**
* Public Methods
*/
@ -1070,7 +1102,7 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
* @param {Object} [sortObj] A sort object which can sort the filtered set
*/
Shuffle.prototype.shuffle = function( category, sortObj ) {
if ( !this.enabled ) {
if ( !this.enabled || this.isTransitioning ) {
return;
}
@ -1095,14 +1127,16 @@ Shuffle.prototype.shuffle = function( category, sortObj ) {
* @param {Object} opts the options object for the sorted plugin
*/
Shuffle.prototype.sort = function( opts ) {
this._resetCols();
if ( this.enabled && !this.isTransitioning ) {
this._resetCols();
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
this._layout( items );
this._layout( items );
this.lastSort = opts;
this.lastSort = opts;
}
};
/**
@ -1111,7 +1145,7 @@ Shuffle.prototype.sort = function( opts ) {
* recalculated.
*/
Shuffle.prototype.update = function( isOnlyLayout ) {
if ( this.enabled ) {
if ( this.enabled && !this.isTransitioning ) {
if ( !isOnlyLayout ) {
// Get updated colCount
@ -1173,25 +1207,27 @@ Shuffle.prototype.remove = function( $collection ) {
return;
}
this._whenCollectionDone( $collection, TRANSITIONEND, function() {
function handleRemoved() {
// Remove the collection in the callback
$collection.remove();
// Update the items, layout, count and fire off `removed` event
// Update things now that elements have been removed.
this.$items = this._getItems();
this.layout();
this._updateItemCount();
this._fire( Shuffle.EventType.REMOVED, [ $collection, this ] );
// Let it get garbage collected
$collection = null;
});
}
// Hide collection first.
this._toggleFilterClasses( $(), $collection );
this._shrink( $collection );
// Process changes
this._processStyleQueue();
this.sort();
this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
};
/**
@ -1257,7 +1293,8 @@ Shuffle.settings = {
itemCss : { // default CSS for each item
position: 'absolute',
top: 0,
left: 0
left: 0,
visibility: 'visible'
},
offset: { top: 0, left: 0 },
revealAppendedDelay: 300,
@ -1266,6 +1303,7 @@ Shuffle.settings = {
enabled: true,
destroyed: false,
initialized: false,
_animations: [],
styleQueue: []
};

File diff suppressed because one or more lines are too long

@ -91,6 +91,11 @@ function each(obj, iterator, context) {
}
}
function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
// Used for unique instance variables
var id = 0;
var $window = $( window );
@ -116,10 +121,10 @@ var Shuffle = function( element, options ) {
// Dispatch the done event asynchronously so that people can bind to it after
// Shuffle has been initialized.
setTimeout( $.proxy(function() {
defer(function() {
this.initialized = true;
this._fire( Shuffle.EventType.DONE );
}, this), 16 );
}, this, 16);
};
@ -300,10 +305,10 @@ Shuffle.prototype._init = function() {
// The shuffle items haven't had transitions set on them yet
// so the user doesn't see the first layout. Set them now that the first layout is done.
if ( this.supported ) {
setTimeout($.proxy(function() {
defer(function() {
this._setTransitions();
this.element.style[ TRANSITION ] = 'height ' + this.speed + 'ms ' + this.easing;
}, this), 0);
}, this);
}
};
@ -457,7 +462,7 @@ Shuffle.prototype._initItems = function( $items ) {
Shuffle.ClassName.SHUFFLE_ITEM,
Shuffle.ClassName.FILTERED
].join(' '));
$items.css( this.itemCss ).data('position', {x: 0, y: 0});
$items.css( this.itemCss ).data('position', {x: 0, y: 0}).data('scale', DEFAULT_SCALE);
};
/**
@ -638,8 +643,6 @@ Shuffle.prototype._fire = function( name, args ) {
* @param {boolean} isOnlyPosition If true this will position the items with zero opacity.
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
var added = 0;
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
@ -648,6 +651,7 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
@ -674,7 +678,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
}
};
added++;
this.styleQueue.push( transitionObj );
}, this);
@ -682,12 +685,6 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
// queue here with styles from the shrink method
this._processStyleQueue();
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
if ( items.length > 0 && added === 0 ) {
this._layoutEnd();
}
// Adjust the height of the container
this._setContainerSize();
};
@ -796,6 +793,8 @@ Shuffle.prototype._shrink = function( $collection ) {
return;
}
$item.data( 'scale', CONCEALED_SCALE );
var transitionObj = {
$item: $item,
x: itemData.position.x,
@ -817,7 +816,7 @@ Shuffle.prototype._shrink = function( $collection ) {
*/
Shuffle.prototype._onResize = function() {
// If shuffle is disabled, destroyed, don't do anything
if ( !this.enabled || this.destroyed ) {
if ( !this.enabled || this.destroyed || this.isTransitioning ) {
return;
}
@ -870,16 +869,13 @@ Shuffle.prototype._getStylesForTransition = function( opts ) {
* @private
*/
Shuffle.prototype._transition = function( opts ) {
opts.$item.data('scale', opts.scale);
var styles = this._getStylesForTransition( opts );
this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
};
Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
callfront();
// Transition end handler removes its listener.
function handleTransitionEnd( evt ) {
// Make sure this event handler has not bubbled up from a child.
if ( evt.target === evt.currentTarget ) {
@ -888,29 +884,34 @@ Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, call
}
}
callfront();
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( !this.initialized ) {
$item.css( styles );
callback();
return;
}
// Use CSS Transforms if we have them
if ( this.supported ) {
$item.css( styles );
// Transitions are not set until shuffle has loaded to avoid the initial transition.
if ( this.initialized ) {
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
} else {
callback();
}
// Namespaced because the reveal appended function also wants to know
// about the transition end event.
$item.on( TRANSITIONEND, handleTransitionEnd );
// Use jQuery to animate left/top
} else {
$item.stop( true ).animate( styles, this.speed, 'swing', callback );
var anim = $item.stop( true ).animate( styles, this.speed, 'swing', callback );
this._animations.push( anim.promise() );
}
};
Shuffle.prototype._processStyleQueue = function() {
Shuffle.prototype._processStyleQueue = function( noLayout ) {
var $transitions = $();
// Iterate over the queue and keep track of ones that use transitions.
each(this.styleQueue, function( transitionObj ) {
if ( transitionObj.skipTransition ) {
this._styleImmediately( transitionObj );
@ -920,13 +921,24 @@ Shuffle.prototype._processStyleQueue = function() {
}
}, this);
if ( $transitions.length > 0 ) {
if ( this.initialized && this.supported ) {
// TODO: Transitioning flag.
this._whenCollectionDone( $transitions, TRANSITIONEND, this._layoutEnd );
if ( $transitions.length > 0 && this.initialized ) {
// Set flag that shuffle is currently in motion.
this.isTransitioning = true;
if ( this.supported ) {
this._whenCollectionDone( $transitions, TRANSITIONEND, this._movementFinished );
// The _transition function appends a promise to the animations array.
// When they're all complete, do things.
} else {
this._layoutEnd();
this._whenAnimationsDone( this._movementFinished );
}
// A call to layout happened, but none of the newly filtered items will
// change position. Asynchronously fire the callback here.
} else if ( !noLayout ) {
defer( this._layoutEnd, this );
}
// Remove everything in the style queue
@ -939,8 +951,13 @@ Shuffle.prototype._styleImmediately = function( opts ) {
}, this);
};
Shuffle.prototype._movementFinished = function() {
this.isTransitioning = false;
this._layoutEnd();
};
Shuffle.prototype._layoutEnd = function() {
setTimeout($.proxy( this._fire, this, Shuffle.EventType.LAYOUT ), 0);
this._fire( Shuffle.EventType.LAYOUT );
};
Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
@ -958,7 +975,9 @@ Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
each(this.styleQueue, function( transitionObj ) {
transitionObj.skipTransition = true;
});
this._processStyleQueue();
// Apply shrink positions, but do not cause a layout event.
this._processStyleQueue( true );
if ( addToEnd ) {
this._addItemsToEnd( $newItems, isSequential );
@ -991,7 +1010,7 @@ Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
* @private
*/
Shuffle.prototype._revealAppended = function( newFilteredItems ) {
setTimeout($.proxy(function() {
defer(function() {
each(newFilteredItems, function( el ) {
this._transition({
$item: $(el),
@ -1002,8 +1021,9 @@ Shuffle.prototype._revealAppended = function( newFilteredItems ) {
this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
$(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
this._movementFinished();
});
}, this), this.revealAppendedDelay);
}, this, this.revealAppendedDelay);
};
@ -1022,7 +1042,6 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
function handleEventName( evt ) {
if ( evt.target === evt.currentTarget ) {
$( evt.target ).off( eventName, handleEventName );
done++;
// Execute callback if all items have emitted the correct event.
@ -1037,6 +1056,19 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
};
/**
* Execute a callback after jQuery `animate` for a collection has finished.
* @param {Function} callback Callback to execute when they're done.
* @private
*/
Shuffle.prototype._whenAnimationsDone = function( callback ) {
$.when.apply( null, this._animations ).always( $.proxy( function() {
this._animations.length = 0;
callback.call( this );
}, this ));
};
/**
* Public Methods
*/
@ -1047,7 +1079,7 @@ Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callba
* @param {Object} [sortObj] A sort object which can sort the filtered set
*/
Shuffle.prototype.shuffle = function( category, sortObj ) {
if ( !this.enabled ) {
if ( !this.enabled || this.isTransitioning ) {
return;
}
@ -1072,14 +1104,16 @@ Shuffle.prototype.shuffle = function( category, sortObj ) {
* @param {Object} opts the options object for the sorted plugin
*/
Shuffle.prototype.sort = function( opts ) {
this._resetCols();
if ( this.enabled && !this.isTransitioning ) {
this._resetCols();
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
var sortOptions = opts || this.lastSort;
var items = this._getFilteredItems().sorted( sortOptions );
this._layout( items );
this._layout( items );
this.lastSort = opts;
this.lastSort = opts;
}
};
/**
@ -1088,7 +1122,7 @@ Shuffle.prototype.sort = function( opts ) {
* recalculated.
*/
Shuffle.prototype.update = function( isOnlyLayout ) {
if ( this.enabled ) {
if ( this.enabled && !this.isTransitioning ) {
if ( !isOnlyLayout ) {
// Get updated colCount
@ -1150,25 +1184,27 @@ Shuffle.prototype.remove = function( $collection ) {
return;
}
this._whenCollectionDone( $collection, TRANSITIONEND, function() {
function handleRemoved() {
// Remove the collection in the callback
$collection.remove();
// Update the items, layout, count and fire off `removed` event
// Update things now that elements have been removed.
this.$items = this._getItems();
this.layout();
this._updateItemCount();
this._fire( Shuffle.EventType.REMOVED, [ $collection, this ] );
// Let it get garbage collected
$collection = null;
});
}
// Hide collection first.
this._toggleFilterClasses( $(), $collection );
this._shrink( $collection );
// Process changes
this._processStyleQueue();
this.sort();
this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
};
/**
@ -1234,7 +1270,8 @@ Shuffle.settings = {
itemCss : { // default CSS for each item
position: 'absolute',
top: 0,
left: 0
left: 0,
visibility: 'visible'
},
offset: { top: 0, left: 0 },
revealAppendedDelay: 300,
@ -1243,6 +1280,7 @@ Shuffle.settings = {
enabled: true,
destroyed: false,
initialized: false,
_animations: [],
styleQueue: []
};

@ -21,7 +21,7 @@
<script src="../dist/modernizr.custom.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="../bower_components/jasmine-jquery/lib/jasmine-jquery.js"></script>

@ -6,12 +6,15 @@ describe('Shuffle.js', function() {
describe('regular markup', function() {
var $shuffle;
beforeEach(function() {
loadFixtures('regular.html');
$shuffle = $('#regular-shuffle');
});
afterEach(function(done) {
var shuffle = $('#regular-shuffle').data('shuffle');
var shuffle = $shuffle.data('shuffle');
$shuffle = null;
if (!shuffle) {
return done();
@ -27,6 +30,7 @@ describe('Shuffle.js', function() {
} else {
shuffle.$el.one('done.shuffle', finish);
}
});
it('should get default options', function() {
@ -46,7 +50,6 @@ describe('Shuffle.js', function() {
expect(shuffle.throttleTime).toBe(300);
expect(shuffle.sequentialFadeDelay).toBe(150);
expect(shuffle.useSizer).toBe(false);
expect(shuffle.supported).toBe(Modernizr.csstransforms && Modernizr.csstransitions);
expect(shuffle.unique).toBe('shuffle_0');
});
@ -68,7 +71,6 @@ describe('Shuffle.js', function() {
it('should be 3 columns with gutters', function() {
var $shuffle = $('#regular-shuffle');
$shuffle.css({
width: '1000px'
});
@ -90,7 +92,6 @@ describe('Shuffle.js', function() {
it('can have a function for columns and gutters', function() {
var $shuffle = $('#regular-shuffle');
$shuffle.css({
width: '1000px'
});
@ -118,7 +119,6 @@ describe('Shuffle.js', function() {
});
it('can filter by the data attribute', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -156,7 +156,6 @@ describe('Shuffle.js', function() {
});
it('can shuffle by function', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -174,6 +173,34 @@ describe('Shuffle.js', function() {
var $filteredItems = $('#item1, #item2, #item3, #item4, #item5');
expect($concealedItems).toHaveClass('concealed');
expect($filteredItems).toHaveClass('filtered');
expect(shuffle.isTransitioning).toBe(false);
done();
}
// First shuffle is async.
$shuffle.one('done.shuffle', first);
});
it('can shuffle by function without transitions', function(done) {
var shuffle = $shuffle.shuffle({
speed: 100,
supported: false
}).data('shuffle');
function first() {
shuffle.shuffle(function($el) {
return parseInt($el.attr('id').substring(4), 10) <= 5;
});
$shuffle.one('layout.shuffle', second);
}
function second() {
expect(shuffle.visibleItems).toBe(5);
var $concealedItems = $('#item6, #item7, #item8, #item9, #item10');
var $filteredItems = $('#item1, #item2, #item3, #item4, #item5');
expect($concealedItems).toHaveClass('concealed');
expect($filteredItems).toHaveClass('filtered');
expect(shuffle.isTransitioning).toBe(false);
done();
}
@ -182,7 +209,6 @@ describe('Shuffle.js', function() {
});
it('can initialize filtered and the category parameter is optional', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100,
group: 'design'
@ -200,7 +226,6 @@ describe('Shuffle.js', function() {
});
it('can initialize sorted', function() {
var $shuffle = $('#regular-shuffle');
var sortObj = {
by: function($el) {
@ -216,14 +241,11 @@ describe('Shuffle.js', function() {
expect(shuffle.lastSort).toEqual(sortObj);
});
it('can add items', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100,
group: 'black'
}).data('shuffle');
describe('inserting elements', function() {
function first() {
var $collection;
beforeEach(function() {
var $eleven = $('<div>', {
'class': 'item',
'data-age': 36,
@ -239,23 +261,64 @@ describe('Shuffle.js', function() {
text: 'Person 12'
});
var $collection = $eleven.add($twelve);
$shuffle.append($collection);
shuffle.appended($collection);
$collection = $eleven.add($twelve);
});
// Already 2 in the items, plus number 11.
expect(shuffle.visibleItems).toBe(3);
afterEach(function() {
$collection = null;
});
done();
}
it('can add items', function(done) {
var shuffle = $shuffle.shuffle({
speed: 100,
group: 'black'
}).data('shuffle');
function first() {
$shuffle.append($collection);
shuffle.appended($collection);
// Already 2 in the items, plus number 11.
expect(shuffle.visibleItems).toBe(3);
$shuffle.one('layout.shuffle', function() {
expect(shuffle.isTransitioning).toBe(false);
done();
});
}
$shuffle.one('done.shuffle', first);
});
it('can add items without transitions', function(done) {
var shuffle = $shuffle.shuffle({
speed: 100,
group: 'black',
supported: false
}).data('shuffle');
function first() {
$shuffle.append($collection);
shuffle.appended($collection);
// Already 2 in the items, plus number 11.
expect(shuffle.visibleItems).toBe(3);
$shuffle.one('layout.shuffle', function() {
expect(shuffle.isTransitioning).toBe(false);
done();
});
}
$shuffle.one('done.shuffle', first);
});
$shuffle.one('done.shuffle', first);
});
it('can call appended with different options', function() {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -273,36 +336,67 @@ describe('Shuffle.js', function() {
});
it('can remove items', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
describe('removing elements', function() {
var shuffle;
var $itemsToRemove;
var onDone;
var onRemoved;
function first() {
var $itemsToRemove = $shuffle.children().slice(0, 2);
beforeEach(function() {
$itemsToRemove = $shuffle.children().slice(0, 2);
onDone = function() {
shuffle.remove($itemsToRemove);
$shuffle.one('removed.shuffle', onRemoved);
};
});
afterEach(function() {
$itemsToRemove = null;
shuffle = null;
onDone = null;
onRemoved = null;
});
shuffle.remove($itemsToRemove);
$shuffle.one('removed.shuffle', second);
$shuffle.one('layout.shuffle', third);
}
it('can remove items', function(done) {
shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
function second(evt, $items, instance) {
expect(instance.visibleItems).toBe(8);
expect($items[0].id).toBe('item1');
expect($items[1].id).toBe('item2');
expect($shuffle.children().length).toBe(8);
}
onRemoved = function(evt, $items, instance) {
expect(instance.visibleItems).toBe(8);
expect($items[0].id).toBe('item1');
expect($items[1].id).toBe('item2');
expect($shuffle.children().length).toBe(8);
expect(instance.isTransitioning).toBe(false);
function third() {
done();
}
done();
};
$shuffle.one('done.shuffle', first);
$shuffle.one('done.shuffle', onDone);
});
it('can remove items without transitions', function(done) {
shuffle = $shuffle.shuffle({
speed: 100,
supported: false
}).data('shuffle');
onRemoved = function(evt, $items, instance) {
expect(instance.visibleItems).toBe(8);
expect($items[0].id).toBe('item1');
expect($items[1].id).toBe('item2');
expect($shuffle.children().length).toBe(8);
expect(instance.isTransitioning).toBe(false);
done();
};
$shuffle.one('done.shuffle', onDone);
});
});
it('can get an element option', function() {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -321,7 +415,6 @@ describe('Shuffle.js', function() {
});
it('can test elements against filters', function() {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -339,7 +432,6 @@ describe('Shuffle.js', function() {
});
it('can initialize a collection of items', function() {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -372,7 +464,6 @@ describe('Shuffle.js', function() {
it('should reset columns', function() {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -388,7 +479,6 @@ describe('Shuffle.js', function() {
});
it('should destroy properly', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -417,7 +507,6 @@ describe('Shuffle.js', function() {
});
it('should not update or shuffle when disabled or destroyed', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');
@ -445,7 +534,6 @@ describe('Shuffle.js', function() {
});
it('should not update when the container is the same size', function(done) {
var $shuffle = $('#regular-shuffle');
var shuffle = $shuffle.shuffle({
speed: 100
}).data('shuffle');

Loading…
Cancel
Save