1588 строки
46 KiB
JavaScript
1588 строки
46 KiB
JavaScript
/*!
|
|
* Shuffle.js by @Vestride
|
|
* Categorize, sort, and filter a responsive grid of items.
|
|
* Dependencies: jQuery 1.9+, Modernizr 2.6.2+
|
|
* @license MIT license
|
|
* @version 3.0.0
|
|
*/
|
|
|
|
/* Modernizr 2.6.2 (Custom Build) | MIT & BSD
|
|
* Build: http://modernizr.com/download/#-csstransforms-csstransforms3d-csstransitions-cssclasses-prefixed-teststyles-testprop-testallprops-prefixes-domprefixes
|
|
*/
|
|
window.Modernizr=function(a,b,c){function z(a){j.cssText=a}function A(a,b){return z(m.join(a+";")+(b||""))}function B(a,b){return typeof a===b}function C(a,b){return!!~(""+a).indexOf(b)}function D(a,b){for(var d in a){var e=a[d];if(!C(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function E(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:B(f,"function")?f.bind(d||b):f}return!1}function F(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return B(b,"string")||B(b,"undefined")?D(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),E(e,b,c))}var d="2.6.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={},r={},s={},t=[],u=t.slice,v,w=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'<style id="s',h,'">',a,"</style>"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},x={}.hasOwnProperty,y;!B(x,"undefined")&&!B(x.call,"undefined")?y=function(a,b){return x.call(a,b)}:y=function(a,b){return b in a&&B(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=u.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(u.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(u.call(arguments)))};return e}),q.csstransforms=function(){return!!F("transform")},q.csstransforms3d=function(){var a=!!F("perspective");return a&&"webkitPerspective"in g.style&&w("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},q.csstransitions=function(){return F("transition")};for(var G in q)y(q,G)&&(v=G.toLowerCase(),e[v]=q[G](),t.push((e[v]?"":"no-")+v));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)y(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},z(""),i=k=null,e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.testProp=function(a){return D([a])},e.testAllProps=F,e.testStyles=w,e.prefixed=function(a,b,c){return b?F(a,b,c):F(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+t.join(" "):""),e}(this,this.document);
|
|
|
|
(function (factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(['jquery', 'modernizr'], factory);
|
|
} else {
|
|
window.Shuffle = factory(window.jQuery, window.Modernizr);
|
|
}
|
|
})(function($, Modernizr, undefined) {
|
|
|
|
'use strict';
|
|
|
|
|
|
// Validate Modernizr exists.
|
|
// Shuffle requires `csstransitions`, `csstransforms`, `csstransforms3d`,
|
|
// and `prefixed` to exist on the Modernizr object.
|
|
if (typeof Modernizr !== 'object') {
|
|
throw new Error('Shuffle.js requires Modernizr.\n' +
|
|
'http://vestride.github.io/Shuffle/#dependencies');
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns css prefixed properties like `-webkit-transition` or `box-sizing`
|
|
* from `transition` or `boxSizing`, respectively.
|
|
* @param {(string|boolean)} prop Property to be prefixed.
|
|
* @return {string} The prefixed css property.
|
|
*/
|
|
function dashify( prop ) {
|
|
if (!prop) {
|
|
return '';
|
|
}
|
|
|
|
// Replace upper case with dash-lowercase,
|
|
// then fix ms- prefixes because they're not capitalized.
|
|
return prop.replace(/([A-Z])/g, function( str, m1 ) {
|
|
return '-' + m1.toLowerCase();
|
|
}).replace(/^ms-/,'-ms-');
|
|
}
|
|
|
|
// Constant, prefixed variables.
|
|
var TRANSITION = Modernizr.prefixed('transition');
|
|
var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
|
|
var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
|
|
|
|
// Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
|
|
// says it supports non-prefixed transitions.
|
|
// https://github.com/Modernizr/Modernizr/issues/897
|
|
var TRANSITIONEND = {
|
|
'WebkitTransition' : 'webkitTransitionEnd',
|
|
'transition' : 'transitionend'
|
|
}[ TRANSITION ];
|
|
|
|
var TRANSFORM = Modernizr.prefixed('transform');
|
|
var CSS_TRANSFORM = dashify(TRANSFORM);
|
|
|
|
// Constants
|
|
var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
|
|
var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
|
|
var SHUFFLE = 'shuffle';
|
|
var COLUMN_THRESHOLD = 0.3;
|
|
|
|
// Configurable. You can change these constants to fit your application.
|
|
// The default scale and concealed scale, however, have to be different values.
|
|
var ALL_ITEMS = 'all';
|
|
var FILTER_ATTRIBUTE_KEY = 'groups';
|
|
var DEFAULT_SCALE = 1;
|
|
var CONCEALED_SCALE = 0.001;
|
|
|
|
|
|
// Underscore's throttle function.
|
|
function throttle(func, wait, options) {
|
|
var context, args, result;
|
|
var timeout = null;
|
|
var previous = 0;
|
|
options = options || {};
|
|
var later = function() {
|
|
previous = options.leading === false ? 0 : $.now();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
context = args = null;
|
|
};
|
|
return function() {
|
|
var now = $.now();
|
|
if (!previous && options.leading === false) {
|
|
previous = now;
|
|
}
|
|
var remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0 || remaining > wait) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
context = args = null;
|
|
} else if (!timeout && options.trailing !== false) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
function each(obj, iterator, context) {
|
|
for (var i = 0, length = obj.length; i < length; i++) {
|
|
if (iterator.call(context, obj[i], i, obj) === {}) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function defer(fn, context, wait) {
|
|
return setTimeout( $.proxy( fn, context ), wait );
|
|
}
|
|
|
|
function arrayMax( array ) {
|
|
return Math.max.apply( Math, array );
|
|
}
|
|
|
|
function arrayMin( array ) {
|
|
return Math.min.apply( Math, array );
|
|
}
|
|
|
|
|
|
/**
|
|
* Always returns a numeric value, given a value.
|
|
* @param {*} value Possibly numeric value.
|
|
* @return {number} `value` or zero if `value` isn't numeric.
|
|
* @private
|
|
*/
|
|
function getNumber(value) {
|
|
return $.isNumeric(value) ? value : 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Represents a coordinate pair.
|
|
* @param {number} [x=0] X.
|
|
* @param {number} [y=0] Y.
|
|
*/
|
|
var Point = function(x, y) {
|
|
this.x = getNumber( x );
|
|
this.y = getNumber( y );
|
|
};
|
|
|
|
|
|
/**
|
|
* Whether two points are equal.
|
|
* @param {Point} a Point A.
|
|
* @param {Point} b Point B.
|
|
* @return {boolean}
|
|
*/
|
|
Point.equals = function(a, b) {
|
|
return a.x === b.x && a.y === b.y;
|
|
};
|
|
|
|
|
|
// Used for unique instance variables
|
|
var id = 0;
|
|
var $window = $( window );
|
|
|
|
|
|
/**
|
|
* Categorize, sort, and filter a responsive grid of items.
|
|
*
|
|
* @param {Element} element An element which is the parent container for the grid items.
|
|
* @param {Object} [options=Shuffle.options] Options object.
|
|
* @constructor
|
|
*/
|
|
var Shuffle = function( element, options ) {
|
|
options = options || {};
|
|
$.extend( this, Shuffle.options, options, Shuffle.settings );
|
|
|
|
this.$el = $(element);
|
|
this.element = element;
|
|
this.unique = 'shuffle_' + id++;
|
|
|
|
this._fire( Shuffle.EventType.LOADING );
|
|
this._init();
|
|
|
|
// Dispatch the done event asynchronously so that people can bind to it after
|
|
// Shuffle has been initialized.
|
|
defer(function() {
|
|
this.initialized = true;
|
|
this._fire( Shuffle.EventType.DONE );
|
|
}, this, 16);
|
|
};
|
|
|
|
|
|
/**
|
|
* Events the container element emits with the .shuffle namespace.
|
|
* For example, "done.shuffle".
|
|
* @enum {string}
|
|
*/
|
|
Shuffle.EventType = {
|
|
LOADING: 'loading',
|
|
DONE: 'done',
|
|
LAYOUT: 'layout',
|
|
REMOVED: 'removed'
|
|
};
|
|
|
|
|
|
/** @enum {string} */
|
|
Shuffle.ClassName = {
|
|
BASE: SHUFFLE,
|
|
SHUFFLE_ITEM: 'shuffle-item',
|
|
FILTERED: 'filtered',
|
|
CONCEALED: 'concealed'
|
|
};
|
|
|
|
|
|
// Overrideable options
|
|
Shuffle.options = {
|
|
group: ALL_ITEMS, // Initial filter group.
|
|
speed: 250, // Transition/animation speed (milliseconds).
|
|
easing: 'ease-out', // CSS easing function to use.
|
|
itemSelector: '', // e.g. '.picture-item'.
|
|
sizer: null, // Sizer element. Use an element to determine the size of columns and gutters.
|
|
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).
|
|
delimeter: null, // If your group is not json, and is comma delimeted, you could set delimeter to ','.
|
|
buffer: 0, // Useful for percentage based heights when they might not always be exactly the same (in pixels).
|
|
initialSort: null, // Shuffle can be initialized with a sort object. It is the same object given to the sort method.
|
|
throttle: throttle, // By default, shuffle will throttle resize events. This can be changed or removed.
|
|
throttleTime: 300, // How often shuffle can be called on resize (in milliseconds).
|
|
sequentialFadeDelay: 150, // Delay between each item that fades in when adding items.
|
|
supported: CAN_TRANSITION_TRANSFORMS // Whether to use transforms or absolute positioning.
|
|
};
|
|
|
|
|
|
// Not overrideable
|
|
Shuffle.settings = {
|
|
useSizer: false,
|
|
itemCss : { // default CSS for each item
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
visibility: 'visible'
|
|
},
|
|
revealAppendedDelay: 300,
|
|
lastSort: {},
|
|
lastFilter: ALL_ITEMS,
|
|
enabled: true,
|
|
destroyed: false,
|
|
initialized: false,
|
|
_animations: [],
|
|
styleQueue: []
|
|
};
|
|
|
|
|
|
// Expose for testing.
|
|
Shuffle.Point = Point;
|
|
|
|
|
|
/**
|
|
* Static methods.
|
|
*/
|
|
|
|
/**
|
|
* If the browser has 3d transforms available, build a string with those,
|
|
* otherwise use 2d transforms.
|
|
* @param {Point} point X and Y positions.
|
|
* @param {number} scale Scale amount.
|
|
* @return {string} A normalized string which can be used with the transform style.
|
|
* @private
|
|
*/
|
|
Shuffle._getItemTransformString = function(point, scale) {
|
|
if ( HAS_TRANSFORMS_3D ) {
|
|
return 'translate3d(' + point.x + 'px, ' + point.y + 'px, 0) scale3d(' + scale + ', ' + scale + ', 1)';
|
|
} else {
|
|
return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')';
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Retrieve the computed style for an element, parsed as a float. This should
|
|
* not be used for width or height values because jQuery mangles them and they
|
|
* are not precise enough.
|
|
* @param {Element} element Element to get style for.
|
|
* @param {string} style Style property.
|
|
* @return {number} The parsed computed value or zero if that fails because IE
|
|
* will return 'auto' when the element doesn't have margins instead of
|
|
* the computed style.
|
|
* @private
|
|
*/
|
|
Shuffle._getNumberStyle = function( element, style ) {
|
|
return Shuffle._getFloat( $( element ).css( style ) );
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse a string as an integer.
|
|
* @param {string} value String integer.
|
|
* @return {number} The string as an integer or zero.
|
|
* @private
|
|
*/
|
|
Shuffle._getInt = function(value) {
|
|
return getNumber( parseInt( value, 10 ) );
|
|
};
|
|
|
|
/**
|
|
* Parse a string as an float.
|
|
* @param {string} value String float.
|
|
* @return {number} The string as an float or zero.
|
|
* @private
|
|
*/
|
|
Shuffle._getFloat = function(value) {
|
|
return getNumber( parseFloat( value ) );
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the outer width of an element, optionally including its margins.
|
|
* The `offsetWidth` property must be used because having a scale transform
|
|
* on the element affects the bounding box. Sadly, Firefox doesn't return an
|
|
* integer value for offsetWidth (yet).
|
|
* @param {Element} element The element.
|
|
* @param {boolean} [includeMargins] Whether to include margins. Default is false.
|
|
* @return {number} The width.
|
|
*/
|
|
Shuffle._getOuterWidth = function( element, includeMargins ) {
|
|
var width = element.offsetWidth;
|
|
|
|
// Use jQuery here because it uses getComputedStyle internally and is
|
|
// cross-browser. Using the style property of the element will only work
|
|
// if there are inline styles.
|
|
if ( includeMargins ) {
|
|
var marginLeft = Shuffle._getNumberStyle( element, 'marginLeft');
|
|
var marginRight = Shuffle._getNumberStyle( element, 'marginRight');
|
|
width += marginLeft + marginRight;
|
|
}
|
|
|
|
return width;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the outer height of an element, optionally including its margins.
|
|
* @param {Element} element The element.
|
|
* @param {boolean} [includeMargins] Whether to include margins. Default is false.
|
|
* @return {number} The height.
|
|
*/
|
|
Shuffle._getOuterHeight = function( element, includeMargins ) {
|
|
var height = element.offsetHeight;
|
|
|
|
if ( includeMargins ) {
|
|
var marginTop = Shuffle._getNumberStyle( element, 'marginTop');
|
|
var marginBottom = Shuffle._getNumberStyle( element, 'marginBottom');
|
|
height += marginTop + marginBottom;
|
|
}
|
|
|
|
return height;
|
|
};
|
|
|
|
|
|
/**
|
|
* Change a property or execute a function which will not have a transition
|
|
* @param {Element} element DOM element that won't be transitioned
|
|
* @param {Function} callback A function which will be called while transition
|
|
* is set to 0ms.
|
|
* @param {Object} [context] Optional context for the callback function.
|
|
* @private
|
|
*/
|
|
Shuffle._skipTransition = function( element, callback, context ) {
|
|
var duration = element.style[ TRANSITION_DURATION ];
|
|
|
|
// Set the duration to zero so it happens immediately
|
|
element.style[ TRANSITION_DURATION ] = '0ms'; // ms needed for firefox!
|
|
|
|
callback.call( context );
|
|
|
|
// Force reflow
|
|
var reflow = element.offsetWidth;
|
|
// Avoid jshint warnings: unused variables and expressions.
|
|
reflow = null;
|
|
|
|
// Put the duration back
|
|
element.style[ TRANSITION_DURATION ] = duration;
|
|
};
|
|
|
|
|
|
/**
|
|
* Instance methods.
|
|
*/
|
|
|
|
Shuffle.prototype._init = function() {
|
|
this.$items = this._getItems();
|
|
|
|
this.sizer = this._getElementOption( this.sizer );
|
|
|
|
if ( this.sizer ) {
|
|
this.useSizer = true;
|
|
}
|
|
|
|
// Add class and invalidate styles
|
|
this.$el.addClass( Shuffle.ClassName.BASE );
|
|
|
|
// Set initial css for each item
|
|
this._initItems();
|
|
|
|
// Bind resize events
|
|
// http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
|
|
$window.on('resize.' + SHUFFLE + '.' + this.unique, this._getResizeFunction());
|
|
|
|
// Get container css all in one request. Causes reflow
|
|
var containerCSS = this.$el.css(['position', 'overflow']);
|
|
var containerWidth = Shuffle._getOuterWidth( this.element );
|
|
|
|
// Add styles to the container if it doesn't have them.
|
|
this._validateStyles( containerCSS );
|
|
|
|
// We already got the container's width above, no need to cause another reflow getting it again...
|
|
// Calculate the number of columns there will be
|
|
this._setColumns( containerWidth );
|
|
|
|
// Kick off!
|
|
this.shuffle( this.group, this.initialSort );
|
|
|
|
// 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 ) {
|
|
defer(function() {
|
|
this._setTransitions();
|
|
this.element.style[ TRANSITION ] = 'height ' + this.speed + 'ms ' + this.easing;
|
|
}, this);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns a throttled and proxied function for the resize handler.
|
|
* @return {Function}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getResizeFunction = function() {
|
|
var resizeFunction = $.proxy( this._onResize, this );
|
|
return this.throttle ?
|
|
this.throttle( resizeFunction, this.throttleTime ) :
|
|
resizeFunction;
|
|
};
|
|
|
|
|
|
/**
|
|
* Retrieve an element from an option.
|
|
* @param {string|jQuery|Element} option The option to check.
|
|
* @return {?Element} The plain element or null.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getElementOption = function( option ) {
|
|
// If column width is a string, treat is as a selector and search for the
|
|
// sizer element within the outermost container
|
|
if ( typeof option === 'string' ) {
|
|
return this.$el.find( option )[0] || null;
|
|
|
|
// Check for an element
|
|
} else if ( option && option.nodeType && option.nodeType === 1 ) {
|
|
return option;
|
|
|
|
// Check for jQuery object
|
|
} else if ( option && option.jquery ) {
|
|
return option[0];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* Ensures the shuffle container has the css styles it needs applied to it.
|
|
* @param {Object} styles Key value pairs for position and overflow.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._validateStyles = function(styles) {
|
|
// Position cannot be static.
|
|
if ( styles.position === 'static' ) {
|
|
this.element.style.position = 'relative';
|
|
}
|
|
|
|
// Overflow has to be hidden
|
|
if ( styles.overflow !== 'hidden' ) {
|
|
this.element.style.overflow = 'hidden';
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Filter the elements by a category.
|
|
* @param {string} [category] Category to filter by. If it's given, the last
|
|
* category will be used to filter the items.
|
|
* @param {ArrayLike} [$collection] Optionally filter a collection. Defaults to
|
|
* all the items.
|
|
* @return {jQuery} Filtered items.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._filter = function( category, $collection ) {
|
|
category = category || this.lastFilter;
|
|
$collection = $collection || this.$items;
|
|
|
|
var set = this._getFilteredSets( category, $collection );
|
|
|
|
// Individually add/remove concealed/filtered classes
|
|
this._toggleFilterClasses( set.filtered, set.concealed );
|
|
|
|
// Save the last filter in case elements are appended.
|
|
this.lastFilter = category;
|
|
|
|
// This is saved mainly because providing a filter function (like searching)
|
|
// will overwrite the `lastFilter` property every time its called.
|
|
if ( typeof category === 'string' ) {
|
|
this.group = category;
|
|
}
|
|
|
|
return set.filtered;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns an object containing the filtered and concealed elements.
|
|
* @param {string|Function} category Category or function to filter by.
|
|
* @param {ArrayLike.<Element>} $items A collection of items to filter.
|
|
* @return {!{filtered: jQuery, concealed: jQuery}}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getFilteredSets = function( category, $items ) {
|
|
var $filtered = $();
|
|
var $concealed = $();
|
|
|
|
// category === 'all', add filtered class to everything
|
|
if ( category === ALL_ITEMS ) {
|
|
$filtered = $items;
|
|
|
|
// Loop through each item and use provided function to determine
|
|
// whether to hide it or not.
|
|
} else {
|
|
each($items, function( el ) {
|
|
var $item = $(el);
|
|
if ( this._doesPassFilter( category, $item ) ) {
|
|
$filtered = $filtered.add( $item );
|
|
} else {
|
|
$concealed = $concealed.add( $item );
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
return {
|
|
filtered: $filtered,
|
|
concealed: $concealed
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Test an item to see if it passes a category.
|
|
* @param {string|Function} category Category or function to filter by.
|
|
* @param {jQuery} $item A single item, wrapped with jQuery.
|
|
* @return {boolean} Whether it passes the category/filter.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._doesPassFilter = function( category, $item ) {
|
|
if ( $.isFunction( category ) ) {
|
|
return category.call( $item[0], $item, this );
|
|
|
|
// Check each element's data-groups attribute against the given category.
|
|
} else {
|
|
var groups = $item.data( FILTER_ATTRIBUTE_KEY );
|
|
var keys = this.delimeter && !$.isArray( groups ) ?
|
|
groups.split( this.delimeter ) :
|
|
groups;
|
|
return $.inArray(category, keys) > -1;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Toggles the filtered and concealed class names.
|
|
* @param {jQuery} $filtered Filtered set.
|
|
* @param {jQuery} $concealed Concealed set.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._toggleFilterClasses = function( $filtered, $concealed ) {
|
|
$filtered
|
|
.removeClass( Shuffle.ClassName.CONCEALED )
|
|
.addClass( Shuffle.ClassName.FILTERED );
|
|
$concealed
|
|
.removeClass( Shuffle.ClassName.FILTERED )
|
|
.addClass( Shuffle.ClassName.CONCEALED );
|
|
};
|
|
|
|
|
|
/**
|
|
* Set the initial css for each item
|
|
* @param {jQuery} [$items] Optionally specifiy at set to initialize
|
|
*/
|
|
Shuffle.prototype._initItems = function( $items ) {
|
|
$items = $items || this.$items;
|
|
$items.addClass([
|
|
Shuffle.ClassName.SHUFFLE_ITEM,
|
|
Shuffle.ClassName.FILTERED
|
|
].join(' '));
|
|
$items.css( this.itemCss ).data('point', new Point()).data('scale', DEFAULT_SCALE);
|
|
};
|
|
|
|
|
|
/**
|
|
* Updates the filtered item count.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._updateItemCount = function() {
|
|
this.visibleItems = this._getFilteredItems().length;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets css transform transition on a an element.
|
|
* @param {Element} element Element to set transition on.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._setTransition = function( element ) {
|
|
element.style[ TRANSITION ] = CSS_TRANSFORM + ' ' + this.speed + 'ms ' +
|
|
this.easing + ', opacity ' + this.speed + 'ms ' + this.easing;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets css transform transition on a group of elements.
|
|
* @param {ArrayLike.<Element>} $items Elements to set transitions on.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._setTransitions = function( $items ) {
|
|
$items = $items || this.$items;
|
|
each($items, function( el ) {
|
|
this._setTransition( el );
|
|
}, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets a transition delay on a collection of elements, making each delay
|
|
* greater than the last.
|
|
* @param {ArrayLike.<Element>} $collection Array to iterate over.
|
|
*/
|
|
Shuffle.prototype._setSequentialDelay = function( $collection ) {
|
|
if ( !this.supported ) {
|
|
return;
|
|
}
|
|
|
|
// $collection can be an array of dom elements or jquery object
|
|
each($collection, function( el, i ) {
|
|
// This works because the transition-property: transform, opacity;
|
|
el.style[ TRANSITION_DELAY ] = '0ms,' + ((i + 1) * this.sequentialFadeDelay) + 'ms';
|
|
}, this);
|
|
};
|
|
|
|
|
|
Shuffle.prototype._getItems = function() {
|
|
return this.$el.children( this.itemSelector );
|
|
};
|
|
|
|
|
|
Shuffle.prototype._getFilteredItems = function() {
|
|
return this.$items.filter('.' + Shuffle.ClassName.FILTERED);
|
|
};
|
|
|
|
|
|
Shuffle.prototype._getConcealedItems = function() {
|
|
return this.$items.filter('.' + Shuffle.ClassName.CONCEALED);
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the column size, based on column width and sizer options.
|
|
* @param {number} containerWidth Size of the parent container.
|
|
* @param {number} gutterSize Size of the gutters.
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
|
|
var size;
|
|
|
|
// If the columnWidth property is a function, then the grid is fluid
|
|
if ( $.isFunction( this.columnWidth ) ) {
|
|
size = this.columnWidth(containerWidth);
|
|
|
|
// columnWidth option isn't a function, are they using a sizing element?
|
|
} else if ( this.useSizer ) {
|
|
size = Shuffle._getOuterWidth(this.sizer);
|
|
|
|
// if not, how about the explicitly set option?
|
|
} else if ( this.columnWidth ) {
|
|
size = this.columnWidth;
|
|
|
|
// or use the size of the first item
|
|
} else if ( this.$items.length > 0 ) {
|
|
size = Shuffle._getOuterWidth(this.$items[0], true);
|
|
|
|
// if there's no items, use size of container
|
|
} else {
|
|
size = containerWidth;
|
|
}
|
|
|
|
// Don't let them set a column width of zero.
|
|
if ( size === 0 ) {
|
|
size = containerWidth;
|
|
}
|
|
|
|
return size + gutterSize;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the gutter size, based on gutter width and sizer options.
|
|
* @param {number} containerWidth Size of the parent container.
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getGutterSize = function( containerWidth ) {
|
|
var size;
|
|
if ( $.isFunction( this.gutterWidth ) ) {
|
|
size = this.gutterWidth(containerWidth);
|
|
} else if ( this.useSizer ) {
|
|
size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
|
|
} else {
|
|
size = this.gutterWidth;
|
|
}
|
|
|
|
return size;
|
|
};
|
|
|
|
|
|
/**
|
|
* Calculate the number of columns to be used. Gets css if using sizer element.
|
|
* @param {number} [theContainerWidth] Optionally specify a container width if it's already available.
|
|
*/
|
|
Shuffle.prototype._setColumns = function( theContainerWidth ) {
|
|
var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
|
|
var gutter = this._getGutterSize( containerWidth );
|
|
var columnWidth = this._getColumnSize( containerWidth, gutter );
|
|
var calculatedColumns = (containerWidth + gutter) / columnWidth;
|
|
|
|
// Widths given from getComputedStyle are not precise enough...
|
|
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < COLUMN_THRESHOLD ) {
|
|
// e.g. calculatedColumns = 11.998876
|
|
calculatedColumns = Math.round( calculatedColumns );
|
|
}
|
|
|
|
this.cols = Math.max( Math.floor(calculatedColumns), 1 );
|
|
this.containerWidth = containerWidth;
|
|
this.colWidth = columnWidth;
|
|
};
|
|
|
|
/**
|
|
* Adjust the height of the grid
|
|
*/
|
|
Shuffle.prototype._setContainerSize = function() {
|
|
this.$el.css( 'height', this._getContainerSize() );
|
|
};
|
|
|
|
|
|
/**
|
|
* Based on the column heights, it returns the biggest one.
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getContainerSize = function() {
|
|
return arrayMax( this.positions );
|
|
};
|
|
|
|
|
|
/**
|
|
* Fire events with .shuffle namespace
|
|
*/
|
|
Shuffle.prototype._fire = function( name, args ) {
|
|
this.$el.trigger( name + '.' + SHUFFLE, args && args.length ? args : [ this ] );
|
|
};
|
|
|
|
|
|
/**
|
|
* Zeros out the y columns array, which is used to determine item placement.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._resetCols = function() {
|
|
var i = this.cols;
|
|
this.positions = [];
|
|
while (i--) {
|
|
this.positions.push( 0 );
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Loops through each item that should be shown and calculates the x, y position.
|
|
* @param {Array.<Element>} 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 {boolean} [isOnlyPosition=false] If true this will position the items with zero opacity.
|
|
*/
|
|
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
|
|
each(items, function( item ) {
|
|
this._layoutItem( item, !!isOnlyPosition );
|
|
}, this);
|
|
|
|
// `_layout` always happens after `_shrink`, so it's safe to process the style
|
|
// queue here with styles from the shrink method.
|
|
this._processStyleQueue();
|
|
|
|
// Adjust the height of the container.
|
|
this._setContainerSize();
|
|
};
|
|
|
|
|
|
/**
|
|
* Calculates the position of the item and pushes it onto the style queue.
|
|
* @param {Element} item Element which is being positioned.
|
|
* @param {boolean} isOnlyPosition Whether to position the item, but with zero
|
|
* opacity so that it can fade in later.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
|
|
var $item = $(item);
|
|
var itemData = $item.data();
|
|
var currPos = itemData.point;
|
|
var currScale = itemData.scale;
|
|
var itemSize = {
|
|
width: Shuffle._getOuterWidth( item, true ),
|
|
height: Shuffle._getOuterHeight( item, true )
|
|
};
|
|
var pos = this._getItemPosition( itemSize );
|
|
|
|
// 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.
|
|
if ( Point.equals(currPos, pos) && currScale === DEFAULT_SCALE ) {
|
|
return;
|
|
}
|
|
|
|
// Save data for shrink
|
|
itemData.point = pos;
|
|
itemData.scale = DEFAULT_SCALE;
|
|
|
|
this.styleQueue.push({
|
|
$item: $item,
|
|
point: pos,
|
|
scale: DEFAULT_SCALE,
|
|
opacity: isOnlyPosition ? 0 : 1,
|
|
skipTransition: isOnlyPosition,
|
|
callfront: function() {
|
|
if ( !isOnlyPosition ) {
|
|
$item.css( 'visibility', 'visible' );
|
|
}
|
|
},
|
|
callback: function() {
|
|
if ( isOnlyPosition ) {
|
|
$item.css( 'visibility', 'hidden' );
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Determine the location of the next item, based on its size.
|
|
* @param {{width: number, height: number}} itemSize Object with width and height.
|
|
* @return {Point}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getItemPosition = function( itemSize ) {
|
|
var columnSpan = this._getColumnSpan( itemSize.width, this.colWidth, this.cols );
|
|
|
|
var setY = this._getColumnSet( columnSpan, this.cols );
|
|
|
|
// Finds the index of the smallest number in the set.
|
|
var shortColumnIndex = this._getShortColumn( setY, this.buffer );
|
|
|
|
// Position the item
|
|
var point = new Point(
|
|
Math.round( this.colWidth * shortColumnIndex ),
|
|
Math.round( setY[shortColumnIndex] ));
|
|
|
|
// Update the columns array with the new values for each column.
|
|
// e.g. before the update the columns could be [250, 0, 0, 0] for an item
|
|
// which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
|
|
var setHeight = setY[shortColumnIndex] + itemSize.height;
|
|
var setSpan = this.cols + 1 - setY.length;
|
|
for ( var i = 0; i < setSpan; i++ ) {
|
|
this.positions[ shortColumnIndex + i ] = setHeight;
|
|
}
|
|
|
|
return point;
|
|
};
|
|
|
|
|
|
/**
|
|
* Determine the number of columns an items spans.
|
|
* @param {number} itemWidth Width of the item.
|
|
* @param {number} columnWidth Width of the column (includes gutter).
|
|
* @param {number} columns Total number of columns
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getColumnSpan = function( itemWidth, columnWidth, columns ) {
|
|
var columnSpan = itemWidth / columnWidth;
|
|
|
|
// If the difference between the rounded column span number and the
|
|
// calculated column span number is really small, round the number to
|
|
// make it fit.
|
|
if ( Math.abs(Math.round( columnSpan ) - columnSpan ) < COLUMN_THRESHOLD ) {
|
|
// e.g. columnSpan = 4.0089945390298745
|
|
columnSpan = Math.round( columnSpan );
|
|
}
|
|
|
|
// Ensure the column span is not more than the amount of columns in the whole layout.
|
|
return Math.min( Math.ceil( columnSpan ), columns );
|
|
};
|
|
|
|
|
|
/**
|
|
* Retrieves the column set to use for placement.
|
|
* @param {number} columnSpan The number of columns this current item spans.
|
|
* @param {number} columns The total columns in the grid.
|
|
* @return {Array.<number>} An array of numbers represeting the column set.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
|
|
// The item spans only one column.
|
|
if ( columnSpan === 1 ) {
|
|
return this.positions;
|
|
|
|
// The item spans more than one column, figure out how many different
|
|
// places it could fit horizontally.
|
|
// The group count is the number of places within the positions this block
|
|
// could fit, ignoring the current positions of items.
|
|
// Imagine a 2 column brick as the second item in a 4 column grid with
|
|
// 10px height each. Find the places it would fit:
|
|
// [10, 0, 0, 0]
|
|
// | | |
|
|
// * * *
|
|
//
|
|
// Then take the places which fit and get the bigger of the two:
|
|
// max([10, 0]), max([0, 0]), max([0, 0]) = [10, 0, 0]
|
|
//
|
|
// Next, find the first smallest number (the short column).
|
|
// [10, 0, 0]
|
|
// |
|
|
// *
|
|
//
|
|
// And that's where it should be placed!
|
|
} else {
|
|
var groupCount = columns + 1 - columnSpan;
|
|
var groupY = [];
|
|
|
|
// For how many possible positions for this item there are.
|
|
for ( var i = 0; i < groupCount; i++ ) {
|
|
// Find the bigger value for each place it could fit.
|
|
groupY[i] = arrayMax( this.positions.slice( i, i + columnSpan ) );
|
|
}
|
|
|
|
return groupY;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Find index of short column, the first from the left where this item will go.
|
|
*
|
|
* @param {Array.<number>} positions The array to search for the smallest number.
|
|
* @param {number} buffer Optional buffer which is very useful when the height
|
|
* is a percentage of the width.
|
|
* @return {number} Index of the short column.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getShortColumn = function( positions, buffer ) {
|
|
var minPosition = arrayMin( positions );
|
|
for (var i = 0, len = positions.length; i < len; i++) {
|
|
if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Hides the elements that don't match our filter.
|
|
* @param {jQuery} $collection jQuery collection to shrink.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._shrink = function( $collection ) {
|
|
var $concealed = $collection || this._getConcealedItems();
|
|
|
|
each($concealed, function( item ) {
|
|
var $item = $(item);
|
|
var itemData = $item.data();
|
|
|
|
// Continuing would add a transitionend event listener to the element, but
|
|
// that listener would not execute because the transform and opacity would
|
|
// stay the same.
|
|
if ( itemData.scale === CONCEALED_SCALE ) {
|
|
return;
|
|
}
|
|
|
|
itemData.scale = CONCEALED_SCALE;
|
|
|
|
this.styleQueue.push({
|
|
$item: $item,
|
|
point: itemData.point,
|
|
scale : CONCEALED_SCALE,
|
|
opacity: 0,
|
|
callback: function() {
|
|
$item.css( 'visibility', 'hidden' );
|
|
}
|
|
});
|
|
}, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Resize handler.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._onResize = function() {
|
|
// If shuffle is disabled, destroyed, don't do anything
|
|
if ( !this.enabled || this.destroyed || this.isTransitioning ) {
|
|
return;
|
|
}
|
|
|
|
// Will need to check height in the future if it's layed out horizontaly
|
|
var containerWidth = Shuffle._getOuterWidth( this.element );
|
|
|
|
// containerWidth hasn't changed, don't do anything
|
|
if ( containerWidth === this.containerWidth ) {
|
|
return;
|
|
}
|
|
|
|
this.update();
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns styles for either jQuery animate or transition.
|
|
* @param {Object} opts Transition options.
|
|
* @return {!Object} Transforms for transitions, left/top for animate.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._getStylesForTransition = function( opts ) {
|
|
var styles = {
|
|
opacity: opts.opacity
|
|
};
|
|
|
|
if ( this.supported ) {
|
|
styles[ TRANSFORM ] = Shuffle._getItemTransformString( opts.point, opts.scale );
|
|
} else {
|
|
styles.left = opts.point.x;
|
|
styles.top = opts.point.y;
|
|
}
|
|
|
|
return styles;
|
|
};
|
|
|
|
|
|
/**
|
|
* Transitions an item in the grid
|
|
*
|
|
* @param {Object} opts options.
|
|
* @param {jQuery} opts.$item jQuery object representing the current item.
|
|
* @param {Point} opts.point A point object with the x and y coordinates.
|
|
* @param {number} opts.scale Amount to scale the item.
|
|
* @param {number} opts.opacity Opacity of the item.
|
|
* @param {Function} opts.callback Complete function for the animation.
|
|
* @param {Function} opts.callfront Function to call before transitioning.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._transition = function( opts ) {
|
|
var styles = this._getStylesForTransition( opts );
|
|
this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
|
|
};
|
|
|
|
|
|
Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
|
|
// 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 ) {
|
|
$( evt.target ).off( TRANSITIONEND, handleTransitionEnd );
|
|
callback();
|
|
}
|
|
}
|
|
|
|
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 );
|
|
$item.on( TRANSITIONEND, handleTransitionEnd );
|
|
|
|
// Use jQuery to animate left/top
|
|
} else {
|
|
// Save the deferred object which jQuery returns.
|
|
var anim = $item.stop( true ).animate( styles, this.speed, 'swing', callback );
|
|
// Push the animation to the list of pending animations.
|
|
this._animations.push( anim.promise() );
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Execute the styles gathered in the style queue. This applies styles to elements,
|
|
* triggering transitions.
|
|
* @param {boolean} noLayout Whether to trigger a layout event.
|
|
* @private
|
|
*/
|
|
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 );
|
|
} else {
|
|
$transitions = $transitions.add( transitionObj.$item );
|
|
this._transition( transitionObj );
|
|
}
|
|
}, this);
|
|
|
|
|
|
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._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
|
|
this.styleQueue.length = 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Apply styles without a transition.
|
|
* @param {Object} opts Transitions options object.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._styleImmediately = function( opts ) {
|
|
Shuffle._skipTransition(opts.$item[0], function() {
|
|
opts.$item.css( this._getStylesForTransition( opts ) );
|
|
}, this);
|
|
};
|
|
|
|
Shuffle.prototype._movementFinished = function() {
|
|
this.isTransitioning = false;
|
|
this._layoutEnd();
|
|
};
|
|
|
|
Shuffle.prototype._layoutEnd = function() {
|
|
this._fire( Shuffle.EventType.LAYOUT );
|
|
};
|
|
|
|
Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
|
|
// Add classes and set initial positions.
|
|
this._initItems( $newItems );
|
|
|
|
// Add transition to each item.
|
|
this._setTransitions( $newItems );
|
|
|
|
// Update the list of
|
|
this.$items = this._getItems();
|
|
|
|
// Shrink all items (without transitions).
|
|
this._shrink( $newItems );
|
|
each(this.styleQueue, function( transitionObj ) {
|
|
transitionObj.skipTransition = true;
|
|
});
|
|
|
|
// Apply shrink positions, but do not cause a layout event.
|
|
this._processStyleQueue( true );
|
|
|
|
if ( addToEnd ) {
|
|
this._addItemsToEnd( $newItems, isSequential );
|
|
} else {
|
|
this.shuffle( this.lastFilter );
|
|
}
|
|
};
|
|
|
|
|
|
Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
|
|
// Get ones that passed the current filter
|
|
var $passed = this._filter( null, $newItems );
|
|
var passed = $passed.get();
|
|
|
|
// How many filtered elements?
|
|
this._updateItemCount();
|
|
|
|
this._layout( passed, true );
|
|
|
|
if ( isSequential && this.supported ) {
|
|
this._setSequentialDelay( passed );
|
|
}
|
|
|
|
this._revealAppended( passed );
|
|
};
|
|
|
|
|
|
/**
|
|
* Triggers appended elements to fade in.
|
|
* @param {ArrayLike.<Element>} $newFilteredItems Collection of elements.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._revealAppended = function( newFilteredItems ) {
|
|
defer(function() {
|
|
each(newFilteredItems, function( el ) {
|
|
var $item = $( el );
|
|
this._transition({
|
|
$item: $item,
|
|
opacity: 1,
|
|
point: $item.data('point'),
|
|
scale: DEFAULT_SCALE
|
|
});
|
|
}, this);
|
|
|
|
this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
|
|
$(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
|
|
this._movementFinished();
|
|
});
|
|
}, this, this.revealAppendedDelay);
|
|
};
|
|
|
|
|
|
/**
|
|
* Execute a function when an event has been triggered for every item in a collection.
|
|
* @param {jQuery} $collection Collection of elements.
|
|
* @param {string} eventName Event to listen for.
|
|
* @param {Function} callback Callback to execute when they're done.
|
|
* @private
|
|
*/
|
|
Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callback ) {
|
|
var done = 0;
|
|
var items = $collection.length;
|
|
var self = this;
|
|
|
|
function handleEventName( evt ) {
|
|
if ( evt.target === evt.currentTarget ) {
|
|
$( evt.target ).off( eventName, handleEventName );
|
|
done++;
|
|
|
|
// Execute callback if all items have emitted the correct event.
|
|
if ( done === items ) {
|
|
callback.call( self );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bind the event to all items.
|
|
$collection.on( eventName, handleEventName );
|
|
};
|
|
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
/**
|
|
* The magic. This is what makes the plugin 'shuffle'
|
|
* @param {string|Function} [category] Category to filter by. Can be a function
|
|
* @param {Object} [sortObj] A sort object which can sort the filtered set
|
|
*/
|
|
Shuffle.prototype.shuffle = function( category, sortObj ) {
|
|
if ( !this.enabled || this.isTransitioning ) {
|
|
return;
|
|
}
|
|
|
|
if ( !category ) {
|
|
category = ALL_ITEMS;
|
|
}
|
|
|
|
this._filter( category );
|
|
|
|
// How many filtered elements?
|
|
this._updateItemCount();
|
|
|
|
// Shrink each concealed item
|
|
this._shrink();
|
|
|
|
// Update transforms on .filtered elements so they will animate to their new positions
|
|
this.sort( sortObj );
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the .filtered elements, sorts them, and passes them to layout.
|
|
* @param {Object} opts the options object for the sorted plugin
|
|
*/
|
|
Shuffle.prototype.sort = function( opts ) {
|
|
if ( this.enabled && !this.isTransitioning ) {
|
|
this._resetCols();
|
|
|
|
var sortOptions = opts || this.lastSort;
|
|
var items = this._getFilteredItems().sorted( sortOptions );
|
|
|
|
this._layout( items );
|
|
|
|
this.lastSort = sortOptions;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Reposition everything.
|
|
* @param {boolean} isOnlyLayout If true, column and gutter widths won't be
|
|
* recalculated.
|
|
*/
|
|
Shuffle.prototype.update = function( isOnlyLayout ) {
|
|
if ( this.enabled && !this.isTransitioning ) {
|
|
|
|
if ( !isOnlyLayout ) {
|
|
// Get updated colCount
|
|
this._setColumns();
|
|
}
|
|
|
|
// Layout items
|
|
this.sort();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
Shuffle.prototype.layout = function() {
|
|
this.update( true );
|
|
};
|
|
|
|
|
|
/**
|
|
* New items have been appended to shuffle. Fade them in sequentially
|
|
* @param {jQuery} $newItems jQuery collection of new items
|
|
* @param {boolean} [addToEnd=false] If true, new items will be added to the end / bottom
|
|
* of the items. If not true, items will be mixed in with the current sort order.
|
|
* @param {boolean} [isSequential=true] If false, new items won't sequentially fade in
|
|
*/
|
|
Shuffle.prototype.appended = function( $newItems, addToEnd, isSequential ) {
|
|
this._addItems( $newItems, addToEnd === true, isSequential !== false );
|
|
};
|
|
|
|
|
|
/**
|
|
* Disables shuffle from updating dimensions and layout on resize
|
|
*/
|
|
Shuffle.prototype.disable = function() {
|
|
this.enabled = false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Enables shuffle again
|
|
* @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
|
|
*/
|
|
Shuffle.prototype.enable = function( isUpdateLayout ) {
|
|
this.enabled = true;
|
|
if ( isUpdateLayout !== false ) {
|
|
this.update();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove 1 or more shuffle items
|
|
* @param {jQuery} $collection A jQuery object containing one or more element in shuffle
|
|
* @return {Shuffle} The shuffle object
|
|
*/
|
|
Shuffle.prototype.remove = function( $collection ) {
|
|
|
|
// If this isn't a jquery object, exit
|
|
if ( !$collection.length || !$collection.jquery ) {
|
|
return;
|
|
}
|
|
|
|
function handleRemoved() {
|
|
// Remove the collection in the callback
|
|
$collection.remove();
|
|
|
|
// Update things now that elements have been removed.
|
|
this.$items = this._getItems();
|
|
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 );
|
|
|
|
this.sort();
|
|
|
|
this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
|
|
};
|
|
|
|
|
|
/**
|
|
* Destroys shuffle, removes events, styles, and classes
|
|
*/
|
|
Shuffle.prototype.destroy = function() {
|
|
// If there is more than one shuffle instance on the page,
|
|
// removing the resize handler from the window would remove them
|
|
// all. This is why a unique value is needed.
|
|
$window.off('.' + this.unique);
|
|
|
|
// Reset container styles
|
|
this.$el
|
|
.removeClass( SHUFFLE )
|
|
.removeAttr('style')
|
|
.removeData( SHUFFLE );
|
|
|
|
// Reset individual item styles
|
|
this.$items
|
|
.removeAttr('style')
|
|
.removeData('point')
|
|
.removeData('scale')
|
|
.removeClass([
|
|
Shuffle.ClassName.CONCEALED,
|
|
Shuffle.ClassName.FILTERED,
|
|
Shuffle.ClassName.SHUFFLE_ITEM
|
|
].join(' '));
|
|
|
|
// Null DOM references
|
|
this.$items = null;
|
|
this.$el = null;
|
|
this.sizer = null;
|
|
this.element = null;
|
|
|
|
// Set a flag so if a debounced resize has been triggered,
|
|
// it can first check if it is actually destroyed and not doing anything
|
|
this.destroyed = true;
|
|
};
|
|
|
|
|
|
// Plugin definition
|
|
$.fn.shuffle = function( opts ) {
|
|
var args = Array.prototype.slice.call( arguments, 1 );
|
|
return this.each(function() {
|
|
var $this = $( this );
|
|
var shuffle = $this.data( SHUFFLE );
|
|
|
|
// If we don't have a stored shuffle, make a new one and save it
|
|
if ( !shuffle ) {
|
|
shuffle = new Shuffle( this, opts );
|
|
$this.data( SHUFFLE, shuffle );
|
|
} else if ( typeof opts === 'string' && shuffle[ opts ] ) {
|
|
shuffle[ opts ].apply( shuffle, args );
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
// http://stackoverflow.com/a/962890/373422
|
|
function randomize( array ) {
|
|
var tmp, current;
|
|
var top = array.length;
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// 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);
|
|
var arr = this.get();
|
|
var revert = false;
|
|
|
|
if ( !arr.length ) {
|
|
return [];
|
|
}
|
|
|
|
if ( opts.randomize ) {
|
|
return randomize( arr );
|
|
}
|
|
|
|
// Sort the elements by the opts.by function.
|
|
// If we don't have opts.by, default to DOM order
|
|
if ( $.isFunction( opts.by ) ) {
|
|
arr.sort(function(a, b) {
|
|
|
|
// Exit early if we already know we want to revert
|
|
if ( revert ) {
|
|
return 0;
|
|
}
|
|
|
|
var valA = opts.by($(a));
|
|
var valB = opts.by($(b));
|
|
|
|
// If both values are undefined, use the DOM order
|
|
if ( valA === undefined && valB === undefined ) {
|
|
revert = true;
|
|
return 0;
|
|
}
|
|
|
|
if ( valA < valB || valA === 'sortFirst' || valB === 'sortLast' ) {
|
|
return -1;
|
|
}
|
|
|
|
if ( valA > valB || valA === 'sortLast' || valB === 'sortFirst' ) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// Revert to the original array if necessary
|
|
if ( revert ) {
|
|
return this.get();
|
|
}
|
|
|
|
if ( opts.reverse ) {
|
|
arr.reverse();
|
|
}
|
|
|
|
return arr;
|
|
};
|
|
|
|
|
|
$.fn.sorted.defaults = {
|
|
reverse: false, // Use array.reverse() to reverse the results
|
|
by: null, // Sorting function
|
|
randomize: false // If true, this will skip the sorting and return a randomized order in the array
|
|
};
|
|
|
|
return Shuffle;
|
|
|
|
}); |