/*! * 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 */ (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.} $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.} $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.} $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.} 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.} 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.} 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.} $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; });