jquery.shuffle.min.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588
  1. /*!
  2. * Shuffle.js by @Vestride
  3. * Categorize, sort, and filter a responsive grid of items.
  4. * Dependencies: jQuery 1.9+, Modernizr 2.6.2+
  5. * @license MIT license
  6. * @version 3.0.0
  7. */
  8. /* Modernizr 2.6.2 (Custom Build) | MIT & BSD
  9. * Build: http://modernizr.com/download/#-csstransforms-csstransforms3d-csstransitions-cssclasses-prefixed-teststyles-testprop-testallprops-prefixes-domprefixes
  10. */
  11. 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=["&#173;",'<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);
  12. (function (factory) {
  13. if (typeof define === 'function' && define.amd) {
  14. define(['jquery', 'modernizr'], factory);
  15. } else {
  16. window.Shuffle = factory(window.jQuery, window.Modernizr);
  17. }
  18. })(function($, Modernizr, undefined) {
  19. 'use strict';
  20. // Validate Modernizr exists.
  21. // Shuffle requires `csstransitions`, `csstransforms`, `csstransforms3d`,
  22. // and `prefixed` to exist on the Modernizr object.
  23. if (typeof Modernizr !== 'object') {
  24. throw new Error('Shuffle.js requires Modernizr.\n' +
  25. 'http://vestride.github.io/Shuffle/#dependencies');
  26. }
  27. /**
  28. * Returns css prefixed properties like `-webkit-transition` or `box-sizing`
  29. * from `transition` or `boxSizing`, respectively.
  30. * @param {(string|boolean)} prop Property to be prefixed.
  31. * @return {string} The prefixed css property.
  32. */
  33. function dashify( prop ) {
  34. if (!prop) {
  35. return '';
  36. }
  37. // Replace upper case with dash-lowercase,
  38. // then fix ms- prefixes because they're not capitalized.
  39. return prop.replace(/([A-Z])/g, function( str, m1 ) {
  40. return '-' + m1.toLowerCase();
  41. }).replace(/^ms-/,'-ms-');
  42. }
  43. // Constant, prefixed variables.
  44. var TRANSITION = Modernizr.prefixed('transition');
  45. var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
  46. var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
  47. // Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
  48. // says it supports non-prefixed transitions.
  49. // https://github.com/Modernizr/Modernizr/issues/897
  50. var TRANSITIONEND = {
  51. 'WebkitTransition' : 'webkitTransitionEnd',
  52. 'transition' : 'transitionend'
  53. }[ TRANSITION ];
  54. var TRANSFORM = Modernizr.prefixed('transform');
  55. var CSS_TRANSFORM = dashify(TRANSFORM);
  56. // Constants
  57. var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
  58. var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
  59. var SHUFFLE = 'shuffle';
  60. var COLUMN_THRESHOLD = 0.3;
  61. // Configurable. You can change these constants to fit your application.
  62. // The default scale and concealed scale, however, have to be different values.
  63. var ALL_ITEMS = 'all';
  64. var FILTER_ATTRIBUTE_KEY = 'groups';
  65. var DEFAULT_SCALE = 1;
  66. var CONCEALED_SCALE = 0.001;
  67. // Underscore's throttle function.
  68. function throttle(func, wait, options) {
  69. var context, args, result;
  70. var timeout = null;
  71. var previous = 0;
  72. options = options || {};
  73. var later = function() {
  74. previous = options.leading === false ? 0 : $.now();
  75. timeout = null;
  76. result = func.apply(context, args);
  77. context = args = null;
  78. };
  79. return function() {
  80. var now = $.now();
  81. if (!previous && options.leading === false) {
  82. previous = now;
  83. }
  84. var remaining = wait - (now - previous);
  85. context = this;
  86. args = arguments;
  87. if (remaining <= 0 || remaining > wait) {
  88. clearTimeout(timeout);
  89. timeout = null;
  90. previous = now;
  91. result = func.apply(context, args);
  92. context = args = null;
  93. } else if (!timeout && options.trailing !== false) {
  94. timeout = setTimeout(later, remaining);
  95. }
  96. return result;
  97. };
  98. }
  99. function each(obj, iterator, context) {
  100. for (var i = 0, length = obj.length; i < length; i++) {
  101. if (iterator.call(context, obj[i], i, obj) === {}) {
  102. return;
  103. }
  104. }
  105. }
  106. function defer(fn, context, wait) {
  107. return setTimeout( $.proxy( fn, context ), wait );
  108. }
  109. function arrayMax( array ) {
  110. return Math.max.apply( Math, array );
  111. }
  112. function arrayMin( array ) {
  113. return Math.min.apply( Math, array );
  114. }
  115. /**
  116. * Always returns a numeric value, given a value.
  117. * @param {*} value Possibly numeric value.
  118. * @return {number} `value` or zero if `value` isn't numeric.
  119. * @private
  120. */
  121. function getNumber(value) {
  122. return $.isNumeric(value) ? value : 0;
  123. }
  124. /**
  125. * Represents a coordinate pair.
  126. * @param {number} [x=0] X.
  127. * @param {number} [y=0] Y.
  128. */
  129. var Point = function(x, y) {
  130. this.x = getNumber( x );
  131. this.y = getNumber( y );
  132. };
  133. /**
  134. * Whether two points are equal.
  135. * @param {Point} a Point A.
  136. * @param {Point} b Point B.
  137. * @return {boolean}
  138. */
  139. Point.equals = function(a, b) {
  140. return a.x === b.x && a.y === b.y;
  141. };
  142. // Used for unique instance variables
  143. var id = 0;
  144. var $window = $( window );
  145. /**
  146. * Categorize, sort, and filter a responsive grid of items.
  147. *
  148. * @param {Element} element An element which is the parent container for the grid items.
  149. * @param {Object} [options=Shuffle.options] Options object.
  150. * @constructor
  151. */
  152. var Shuffle = function( element, options ) {
  153. options = options || {};
  154. $.extend( this, Shuffle.options, options, Shuffle.settings );
  155. this.$el = $(element);
  156. this.element = element;
  157. this.unique = 'shuffle_' + id++;
  158. this._fire( Shuffle.EventType.LOADING );
  159. this._init();
  160. // Dispatch the done event asynchronously so that people can bind to it after
  161. // Shuffle has been initialized.
  162. defer(function() {
  163. this.initialized = true;
  164. this._fire( Shuffle.EventType.DONE );
  165. }, this, 16);
  166. };
  167. /**
  168. * Events the container element emits with the .shuffle namespace.
  169. * For example, "done.shuffle".
  170. * @enum {string}
  171. */
  172. Shuffle.EventType = {
  173. LOADING: 'loading',
  174. DONE: 'done',
  175. LAYOUT: 'layout',
  176. REMOVED: 'removed'
  177. };
  178. /** @enum {string} */
  179. Shuffle.ClassName = {
  180. BASE: SHUFFLE,
  181. SHUFFLE_ITEM: 'shuffle-item',
  182. FILTERED: 'filtered',
  183. CONCEALED: 'concealed'
  184. };
  185. // Overrideable options
  186. Shuffle.options = {
  187. group: ALL_ITEMS, // Initial filter group.
  188. speed: 250, // Transition/animation speed (milliseconds).
  189. easing: 'ease-out', // CSS easing function to use.
  190. itemSelector: '', // e.g. '.picture-item'.
  191. sizer: null, // Sizer element. Use an element to determine the size of columns and gutters.
  192. gutterWidth: 0, // A static number or function that tells the plugin how wide the gutters between columns are (in pixels).
  193. columnWidth: 0, // A static number or function that returns a number which tells the plugin how wide the columns are (in pixels).
  194. delimeter: null, // If your group is not json, and is comma delimeted, you could set delimeter to ','.
  195. buffer: 0, // Useful for percentage based heights when they might not always be exactly the same (in pixels).
  196. initialSort: null, // Shuffle can be initialized with a sort object. It is the same object given to the sort method.
  197. throttle: throttle, // By default, shuffle will throttle resize events. This can be changed or removed.
  198. throttleTime: 300, // How often shuffle can be called on resize (in milliseconds).
  199. sequentialFadeDelay: 150, // Delay between each item that fades in when adding items.
  200. supported: CAN_TRANSITION_TRANSFORMS // Whether to use transforms or absolute positioning.
  201. };
  202. // Not overrideable
  203. Shuffle.settings = {
  204. useSizer: false,
  205. itemCss : { // default CSS for each item
  206. position: 'absolute',
  207. top: 0,
  208. left: 0,
  209. visibility: 'visible'
  210. },
  211. revealAppendedDelay: 300,
  212. lastSort: {},
  213. lastFilter: ALL_ITEMS,
  214. enabled: true,
  215. destroyed: false,
  216. initialized: false,
  217. _animations: [],
  218. styleQueue: []
  219. };
  220. // Expose for testing.
  221. Shuffle.Point = Point;
  222. /**
  223. * Static methods.
  224. */
  225. /**
  226. * If the browser has 3d transforms available, build a string with those,
  227. * otherwise use 2d transforms.
  228. * @param {Point} point X and Y positions.
  229. * @param {number} scale Scale amount.
  230. * @return {string} A normalized string which can be used with the transform style.
  231. * @private
  232. */
  233. Shuffle._getItemTransformString = function(point, scale) {
  234. if ( HAS_TRANSFORMS_3D ) {
  235. return 'translate3d(' + point.x + 'px, ' + point.y + 'px, 0) scale3d(' + scale + ', ' + scale + ', 1)';
  236. } else {
  237. return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')';
  238. }
  239. };
  240. /**
  241. * Retrieve the computed style for an element, parsed as a float. This should
  242. * not be used for width or height values because jQuery mangles them and they
  243. * are not precise enough.
  244. * @param {Element} element Element to get style for.
  245. * @param {string} style Style property.
  246. * @return {number} The parsed computed value or zero if that fails because IE
  247. * will return 'auto' when the element doesn't have margins instead of
  248. * the computed style.
  249. * @private
  250. */
  251. Shuffle._getNumberStyle = function( element, style ) {
  252. return Shuffle._getFloat( $( element ).css( style ) );
  253. };
  254. /**
  255. * Parse a string as an integer.
  256. * @param {string} value String integer.
  257. * @return {number} The string as an integer or zero.
  258. * @private
  259. */
  260. Shuffle._getInt = function(value) {
  261. return getNumber( parseInt( value, 10 ) );
  262. };
  263. /**
  264. * Parse a string as an float.
  265. * @param {string} value String float.
  266. * @return {number} The string as an float or zero.
  267. * @private
  268. */
  269. Shuffle._getFloat = function(value) {
  270. return getNumber( parseFloat( value ) );
  271. };
  272. /**
  273. * Returns the outer width of an element, optionally including its margins.
  274. * The `offsetWidth` property must be used because having a scale transform
  275. * on the element affects the bounding box. Sadly, Firefox doesn't return an
  276. * integer value for offsetWidth (yet).
  277. * @param {Element} element The element.
  278. * @param {boolean} [includeMargins] Whether to include margins. Default is false.
  279. * @return {number} The width.
  280. */
  281. Shuffle._getOuterWidth = function( element, includeMargins ) {
  282. var width = element.offsetWidth;
  283. // Use jQuery here because it uses getComputedStyle internally and is
  284. // cross-browser. Using the style property of the element will only work
  285. // if there are inline styles.
  286. if ( includeMargins ) {
  287. var marginLeft = Shuffle._getNumberStyle( element, 'marginLeft');
  288. var marginRight = Shuffle._getNumberStyle( element, 'marginRight');
  289. width += marginLeft + marginRight;
  290. }
  291. return width;
  292. };
  293. /**
  294. * Returns the outer height of an element, optionally including its margins.
  295. * @param {Element} element The element.
  296. * @param {boolean} [includeMargins] Whether to include margins. Default is false.
  297. * @return {number} The height.
  298. */
  299. Shuffle._getOuterHeight = function( element, includeMargins ) {
  300. var height = element.offsetHeight;
  301. if ( includeMargins ) {
  302. var marginTop = Shuffle._getNumberStyle( element, 'marginTop');
  303. var marginBottom = Shuffle._getNumberStyle( element, 'marginBottom');
  304. height += marginTop + marginBottom;
  305. }
  306. return height;
  307. };
  308. /**
  309. * Change a property or execute a function which will not have a transition
  310. * @param {Element} element DOM element that won't be transitioned
  311. * @param {Function} callback A function which will be called while transition
  312. * is set to 0ms.
  313. * @param {Object} [context] Optional context for the callback function.
  314. * @private
  315. */
  316. Shuffle._skipTransition = function( element, callback, context ) {
  317. var duration = element.style[ TRANSITION_DURATION ];
  318. // Set the duration to zero so it happens immediately
  319. element.style[ TRANSITION_DURATION ] = '0ms'; // ms needed for firefox!
  320. callback.call( context );
  321. // Force reflow
  322. var reflow = element.offsetWidth;
  323. // Avoid jshint warnings: unused variables and expressions.
  324. reflow = null;
  325. // Put the duration back
  326. element.style[ TRANSITION_DURATION ] = duration;
  327. };
  328. /**
  329. * Instance methods.
  330. */
  331. Shuffle.prototype._init = function() {
  332. this.$items = this._getItems();
  333. this.sizer = this._getElementOption( this.sizer );
  334. if ( this.sizer ) {
  335. this.useSizer = true;
  336. }
  337. // Add class and invalidate styles
  338. this.$el.addClass( Shuffle.ClassName.BASE );
  339. // Set initial css for each item
  340. this._initItems();
  341. // Bind resize events
  342. // http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
  343. $window.on('resize.' + SHUFFLE + '.' + this.unique, this._getResizeFunction());
  344. // Get container css all in one request. Causes reflow
  345. var containerCSS = this.$el.css(['position', 'overflow']);
  346. var containerWidth = Shuffle._getOuterWidth( this.element );
  347. // Add styles to the container if it doesn't have them.
  348. this._validateStyles( containerCSS );
  349. // We already got the container's width above, no need to cause another reflow getting it again...
  350. // Calculate the number of columns there will be
  351. this._setColumns( containerWidth );
  352. // Kick off!
  353. this.shuffle( this.group, this.initialSort );
  354. // The shuffle items haven't had transitions set on them yet
  355. // so the user doesn't see the first layout. Set them now that the first layout is done.
  356. if ( this.supported ) {
  357. defer(function() {
  358. this._setTransitions();
  359. this.element.style[ TRANSITION ] = 'height ' + this.speed + 'ms ' + this.easing;
  360. }, this);
  361. }
  362. };
  363. /**
  364. * Returns a throttled and proxied function for the resize handler.
  365. * @return {Function}
  366. * @private
  367. */
  368. Shuffle.prototype._getResizeFunction = function() {
  369. var resizeFunction = $.proxy( this._onResize, this );
  370. return this.throttle ?
  371. this.throttle( resizeFunction, this.throttleTime ) :
  372. resizeFunction;
  373. };
  374. /**
  375. * Retrieve an element from an option.
  376. * @param {string|jQuery|Element} option The option to check.
  377. * @return {?Element} The plain element or null.
  378. * @private
  379. */
  380. Shuffle.prototype._getElementOption = function( option ) {
  381. // If column width is a string, treat is as a selector and search for the
  382. // sizer element within the outermost container
  383. if ( typeof option === 'string' ) {
  384. return this.$el.find( option )[0] || null;
  385. // Check for an element
  386. } else if ( option && option.nodeType && option.nodeType === 1 ) {
  387. return option;
  388. // Check for jQuery object
  389. } else if ( option && option.jquery ) {
  390. return option[0];
  391. }
  392. return null;
  393. };
  394. /**
  395. * Ensures the shuffle container has the css styles it needs applied to it.
  396. * @param {Object} styles Key value pairs for position and overflow.
  397. * @private
  398. */
  399. Shuffle.prototype._validateStyles = function(styles) {
  400. // Position cannot be static.
  401. if ( styles.position === 'static' ) {
  402. this.element.style.position = 'relative';
  403. }
  404. // Overflow has to be hidden
  405. if ( styles.overflow !== 'hidden' ) {
  406. this.element.style.overflow = 'hidden';
  407. }
  408. };
  409. /**
  410. * Filter the elements by a category.
  411. * @param {string} [category] Category to filter by. If it's given, the last
  412. * category will be used to filter the items.
  413. * @param {ArrayLike} [$collection] Optionally filter a collection. Defaults to
  414. * all the items.
  415. * @return {jQuery} Filtered items.
  416. * @private
  417. */
  418. Shuffle.prototype._filter = function( category, $collection ) {
  419. category = category || this.lastFilter;
  420. $collection = $collection || this.$items;
  421. var set = this._getFilteredSets( category, $collection );
  422. // Individually add/remove concealed/filtered classes
  423. this._toggleFilterClasses( set.filtered, set.concealed );
  424. // Save the last filter in case elements are appended.
  425. this.lastFilter = category;
  426. // This is saved mainly because providing a filter function (like searching)
  427. // will overwrite the `lastFilter` property every time its called.
  428. if ( typeof category === 'string' ) {
  429. this.group = category;
  430. }
  431. return set.filtered;
  432. };
  433. /**
  434. * Returns an object containing the filtered and concealed elements.
  435. * @param {string|Function} category Category or function to filter by.
  436. * @param {ArrayLike.<Element>} $items A collection of items to filter.
  437. * @return {!{filtered: jQuery, concealed: jQuery}}
  438. * @private
  439. */
  440. Shuffle.prototype._getFilteredSets = function( category, $items ) {
  441. var $filtered = $();
  442. var $concealed = $();
  443. // category === 'all', add filtered class to everything
  444. if ( category === ALL_ITEMS ) {
  445. $filtered = $items;
  446. // Loop through each item and use provided function to determine
  447. // whether to hide it or not.
  448. } else {
  449. each($items, function( el ) {
  450. var $item = $(el);
  451. if ( this._doesPassFilter( category, $item ) ) {
  452. $filtered = $filtered.add( $item );
  453. } else {
  454. $concealed = $concealed.add( $item );
  455. }
  456. }, this);
  457. }
  458. return {
  459. filtered: $filtered,
  460. concealed: $concealed
  461. };
  462. };
  463. /**
  464. * Test an item to see if it passes a category.
  465. * @param {string|Function} category Category or function to filter by.
  466. * @param {jQuery} $item A single item, wrapped with jQuery.
  467. * @return {boolean} Whether it passes the category/filter.
  468. * @private
  469. */
  470. Shuffle.prototype._doesPassFilter = function( category, $item ) {
  471. if ( $.isFunction( category ) ) {
  472. return category.call( $item[0], $item, this );
  473. // Check each element's data-groups attribute against the given category.
  474. } else {
  475. var groups = $item.data( FILTER_ATTRIBUTE_KEY );
  476. var keys = this.delimeter && !$.isArray( groups ) ?
  477. groups.split( this.delimeter ) :
  478. groups;
  479. return $.inArray(category, keys) > -1;
  480. }
  481. };
  482. /**
  483. * Toggles the filtered and concealed class names.
  484. * @param {jQuery} $filtered Filtered set.
  485. * @param {jQuery} $concealed Concealed set.
  486. * @private
  487. */
  488. Shuffle.prototype._toggleFilterClasses = function( $filtered, $concealed ) {
  489. $filtered
  490. .removeClass( Shuffle.ClassName.CONCEALED )
  491. .addClass( Shuffle.ClassName.FILTERED );
  492. $concealed
  493. .removeClass( Shuffle.ClassName.FILTERED )
  494. .addClass( Shuffle.ClassName.CONCEALED );
  495. };
  496. /**
  497. * Set the initial css for each item
  498. * @param {jQuery} [$items] Optionally specifiy at set to initialize
  499. */
  500. Shuffle.prototype._initItems = function( $items ) {
  501. $items = $items || this.$items;
  502. $items.addClass([
  503. Shuffle.ClassName.SHUFFLE_ITEM,
  504. Shuffle.ClassName.FILTERED
  505. ].join(' '));
  506. $items.css( this.itemCss ).data('point', new Point()).data('scale', DEFAULT_SCALE);
  507. };
  508. /**
  509. * Updates the filtered item count.
  510. * @private
  511. */
  512. Shuffle.prototype._updateItemCount = function() {
  513. this.visibleItems = this._getFilteredItems().length;
  514. };
  515. /**
  516. * Sets css transform transition on a an element.
  517. * @param {Element} element Element to set transition on.
  518. * @private
  519. */
  520. Shuffle.prototype._setTransition = function( element ) {
  521. element.style[ TRANSITION ] = CSS_TRANSFORM + ' ' + this.speed + 'ms ' +
  522. this.easing + ', opacity ' + this.speed + 'ms ' + this.easing;
  523. };
  524. /**
  525. * Sets css transform transition on a group of elements.
  526. * @param {ArrayLike.<Element>} $items Elements to set transitions on.
  527. * @private
  528. */
  529. Shuffle.prototype._setTransitions = function( $items ) {
  530. $items = $items || this.$items;
  531. each($items, function( el ) {
  532. this._setTransition( el );
  533. }, this);
  534. };
  535. /**
  536. * Sets a transition delay on a collection of elements, making each delay
  537. * greater than the last.
  538. * @param {ArrayLike.<Element>} $collection Array to iterate over.
  539. */
  540. Shuffle.prototype._setSequentialDelay = function( $collection ) {
  541. if ( !this.supported ) {
  542. return;
  543. }
  544. // $collection can be an array of dom elements or jquery object
  545. each($collection, function( el, i ) {
  546. // This works because the transition-property: transform, opacity;
  547. el.style[ TRANSITION_DELAY ] = '0ms,' + ((i + 1) * this.sequentialFadeDelay) + 'ms';
  548. }, this);
  549. };
  550. Shuffle.prototype._getItems = function() {
  551. return this.$el.children( this.itemSelector );
  552. };
  553. Shuffle.prototype._getFilteredItems = function() {
  554. return this.$items.filter('.' + Shuffle.ClassName.FILTERED);
  555. };
  556. Shuffle.prototype._getConcealedItems = function() {
  557. return this.$items.filter('.' + Shuffle.ClassName.CONCEALED);
  558. };
  559. /**
  560. * Returns the column size, based on column width and sizer options.
  561. * @param {number} containerWidth Size of the parent container.
  562. * @param {number} gutterSize Size of the gutters.
  563. * @return {number}
  564. * @private
  565. */
  566. Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
  567. var size;
  568. // If the columnWidth property is a function, then the grid is fluid
  569. if ( $.isFunction( this.columnWidth ) ) {
  570. size = this.columnWidth(containerWidth);
  571. // columnWidth option isn't a function, are they using a sizing element?
  572. } else if ( this.useSizer ) {
  573. size = Shuffle._getOuterWidth(this.sizer);
  574. // if not, how about the explicitly set option?
  575. } else if ( this.columnWidth ) {
  576. size = this.columnWidth;
  577. // or use the size of the first item
  578. } else if ( this.$items.length > 0 ) {
  579. size = Shuffle._getOuterWidth(this.$items[0], true);
  580. // if there's no items, use size of container
  581. } else {
  582. size = containerWidth;
  583. }
  584. // Don't let them set a column width of zero.
  585. if ( size === 0 ) {
  586. size = containerWidth;
  587. }
  588. return size + gutterSize;
  589. };
  590. /**
  591. * Returns the gutter size, based on gutter width and sizer options.
  592. * @param {number} containerWidth Size of the parent container.
  593. * @return {number}
  594. * @private
  595. */
  596. Shuffle.prototype._getGutterSize = function( containerWidth ) {
  597. var size;
  598. if ( $.isFunction( this.gutterWidth ) ) {
  599. size = this.gutterWidth(containerWidth);
  600. } else if ( this.useSizer ) {
  601. size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
  602. } else {
  603. size = this.gutterWidth;
  604. }
  605. return size;
  606. };
  607. /**
  608. * Calculate the number of columns to be used. Gets css if using sizer element.
  609. * @param {number} [theContainerWidth] Optionally specify a container width if it's already available.
  610. */
  611. Shuffle.prototype._setColumns = function( theContainerWidth ) {
  612. var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
  613. var gutter = this._getGutterSize( containerWidth );
  614. var columnWidth = this._getColumnSize( containerWidth, gutter );
  615. var calculatedColumns = (containerWidth + gutter) / columnWidth;
  616. // Widths given from getComputedStyle are not precise enough...
  617. if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < COLUMN_THRESHOLD ) {
  618. // e.g. calculatedColumns = 11.998876
  619. calculatedColumns = Math.round( calculatedColumns );
  620. }
  621. this.cols = Math.max( Math.floor(calculatedColumns), 1 );
  622. this.containerWidth = containerWidth;
  623. this.colWidth = columnWidth;
  624. };
  625. /**
  626. * Adjust the height of the grid
  627. */
  628. Shuffle.prototype._setContainerSize = function() {
  629. this.$el.css( 'height', this._getContainerSize() );
  630. };
  631. /**
  632. * Based on the column heights, it returns the biggest one.
  633. * @return {number}
  634. * @private
  635. */
  636. Shuffle.prototype._getContainerSize = function() {
  637. return arrayMax( this.positions );
  638. };
  639. /**
  640. * Fire events with .shuffle namespace
  641. */
  642. Shuffle.prototype._fire = function( name, args ) {
  643. this.$el.trigger( name + '.' + SHUFFLE, args && args.length ? args : [ this ] );
  644. };
  645. /**
  646. * Zeros out the y columns array, which is used to determine item placement.
  647. * @private
  648. */
  649. Shuffle.prototype._resetCols = function() {
  650. var i = this.cols;
  651. this.positions = [];
  652. while (i--) {
  653. this.positions.push( 0 );
  654. }
  655. };
  656. /**
  657. * Loops through each item that should be shown and calculates the x, y position.
  658. * @param {Array.<Element>} items Array of items that will be shown/layed out in order in their array.
  659. * Because jQuery collection are always ordered in DOM order, we can't pass a jq collection.
  660. * @param {boolean} [isOnlyPosition=false] If true this will position the items with zero opacity.
  661. */
  662. Shuffle.prototype._layout = function( items, isOnlyPosition ) {
  663. each(items, function( item ) {
  664. this._layoutItem( item, !!isOnlyPosition );
  665. }, this);
  666. // `_layout` always happens after `_shrink`, so it's safe to process the style
  667. // queue here with styles from the shrink method.
  668. this._processStyleQueue();
  669. // Adjust the height of the container.
  670. this._setContainerSize();
  671. };
  672. /**
  673. * Calculates the position of the item and pushes it onto the style queue.
  674. * @param {Element} item Element which is being positioned.
  675. * @param {boolean} isOnlyPosition Whether to position the item, but with zero
  676. * opacity so that it can fade in later.
  677. * @private
  678. */
  679. Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
  680. var $item = $(item);
  681. var itemData = $item.data();
  682. var currPos = itemData.point;
  683. var currScale = itemData.scale;
  684. var itemSize = {
  685. width: Shuffle._getOuterWidth( item, true ),
  686. height: Shuffle._getOuterHeight( item, true )
  687. };
  688. var pos = this._getItemPosition( itemSize );
  689. // If the item will not change its position, do not add it to the render
  690. // queue. Transitions don't fire when setting a property to the same value.
  691. if ( Point.equals(currPos, pos) && currScale === DEFAULT_SCALE ) {
  692. return;
  693. }
  694. // Save data for shrink
  695. itemData.point = pos;
  696. itemData.scale = DEFAULT_SCALE;
  697. this.styleQueue.push({
  698. $item: $item,
  699. point: pos,
  700. scale: DEFAULT_SCALE,
  701. opacity: isOnlyPosition ? 0 : 1,
  702. skipTransition: isOnlyPosition,
  703. callfront: function() {
  704. if ( !isOnlyPosition ) {
  705. $item.css( 'visibility', 'visible' );
  706. }
  707. },
  708. callback: function() {
  709. if ( isOnlyPosition ) {
  710. $item.css( 'visibility', 'hidden' );
  711. }
  712. }
  713. });
  714. };
  715. /**
  716. * Determine the location of the next item, based on its size.
  717. * @param {{width: number, height: number}} itemSize Object with width and height.
  718. * @return {Point}
  719. * @private
  720. */
  721. Shuffle.prototype._getItemPosition = function( itemSize ) {
  722. var columnSpan = this._getColumnSpan( itemSize.width, this.colWidth, this.cols );
  723. var setY = this._getColumnSet( columnSpan, this.cols );
  724. // Finds the index of the smallest number in the set.
  725. var shortColumnIndex = this._getShortColumn( setY, this.buffer );
  726. // Position the item
  727. var point = new Point(
  728. Math.round( this.colWidth * shortColumnIndex ),
  729. Math.round( setY[shortColumnIndex] ));
  730. // Update the columns array with the new values for each column.
  731. // e.g. before the update the columns could be [250, 0, 0, 0] for an item
  732. // which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
  733. var setHeight = setY[shortColumnIndex] + itemSize.height;
  734. var setSpan = this.cols + 1 - setY.length;
  735. for ( var i = 0; i < setSpan; i++ ) {
  736. this.positions[ shortColumnIndex + i ] = setHeight;
  737. }
  738. return point;
  739. };
  740. /**
  741. * Determine the number of columns an items spans.
  742. * @param {number} itemWidth Width of the item.
  743. * @param {number} columnWidth Width of the column (includes gutter).
  744. * @param {number} columns Total number of columns
  745. * @return {number}
  746. * @private
  747. */
  748. Shuffle.prototype._getColumnSpan = function( itemWidth, columnWidth, columns ) {
  749. var columnSpan = itemWidth / columnWidth;
  750. // If the difference between the rounded column span number and the
  751. // calculated column span number is really small, round the number to
  752. // make it fit.
  753. if ( Math.abs(Math.round( columnSpan ) - columnSpan ) < COLUMN_THRESHOLD ) {
  754. // e.g. columnSpan = 4.0089945390298745
  755. columnSpan = Math.round( columnSpan );
  756. }
  757. // Ensure the column span is not more than the amount of columns in the whole layout.
  758. return Math.min( Math.ceil( columnSpan ), columns );
  759. };
  760. /**
  761. * Retrieves the column set to use for placement.
  762. * @param {number} columnSpan The number of columns this current item spans.
  763. * @param {number} columns The total columns in the grid.
  764. * @return {Array.<number>} An array of numbers represeting the column set.
  765. * @private
  766. */
  767. Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
  768. // The item spans only one column.
  769. if ( columnSpan === 1 ) {
  770. return this.positions;
  771. // The item spans more than one column, figure out how many different
  772. // places it could fit horizontally.
  773. // The group count is the number of places within the positions this block
  774. // could fit, ignoring the current positions of items.
  775. // Imagine a 2 column brick as the second item in a 4 column grid with
  776. // 10px height each. Find the places it would fit:
  777. // [10, 0, 0, 0]
  778. // | | |
  779. // * * *
  780. //
  781. // Then take the places which fit and get the bigger of the two:
  782. // max([10, 0]), max([0, 0]), max([0, 0]) = [10, 0, 0]
  783. //
  784. // Next, find the first smallest number (the short column).
  785. // [10, 0, 0]
  786. // |
  787. // *
  788. //
  789. // And that's where it should be placed!
  790. } else {
  791. var groupCount = columns + 1 - columnSpan;
  792. var groupY = [];
  793. // For how many possible positions for this item there are.
  794. for ( var i = 0; i < groupCount; i++ ) {
  795. // Find the bigger value for each place it could fit.
  796. groupY[i] = arrayMax( this.positions.slice( i, i + columnSpan ) );
  797. }
  798. return groupY;
  799. }
  800. };
  801. /**
  802. * Find index of short column, the first from the left where this item will go.
  803. *
  804. * @param {Array.<number>} positions The array to search for the smallest number.
  805. * @param {number} buffer Optional buffer which is very useful when the height
  806. * is a percentage of the width.
  807. * @return {number} Index of the short column.
  808. * @private
  809. */
  810. Shuffle.prototype._getShortColumn = function( positions, buffer ) {
  811. var minPosition = arrayMin( positions );
  812. for (var i = 0, len = positions.length; i < len; i++) {
  813. if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
  814. return i;
  815. }
  816. }
  817. return 0;
  818. };
  819. /**
  820. * Hides the elements that don't match our filter.
  821. * @param {jQuery} $collection jQuery collection to shrink.
  822. * @private
  823. */
  824. Shuffle.prototype._shrink = function( $collection ) {
  825. var $concealed = $collection || this._getConcealedItems();
  826. each($concealed, function( item ) {
  827. var $item = $(item);
  828. var itemData = $item.data();
  829. // Continuing would add a transitionend event listener to the element, but
  830. // that listener would not execute because the transform and opacity would
  831. // stay the same.
  832. if ( itemData.scale === CONCEALED_SCALE ) {
  833. return;
  834. }
  835. itemData.scale = CONCEALED_SCALE;
  836. this.styleQueue.push({
  837. $item: $item,
  838. point: itemData.point,
  839. scale : CONCEALED_SCALE,
  840. opacity: 0,
  841. callback: function() {
  842. $item.css( 'visibility', 'hidden' );
  843. }
  844. });
  845. }, this);
  846. };
  847. /**
  848. * Resize handler.
  849. * @private
  850. */
  851. Shuffle.prototype._onResize = function() {
  852. // If shuffle is disabled, destroyed, don't do anything
  853. if ( !this.enabled || this.destroyed || this.isTransitioning ) {
  854. return;
  855. }
  856. // Will need to check height in the future if it's layed out horizontaly
  857. var containerWidth = Shuffle._getOuterWidth( this.element );
  858. // containerWidth hasn't changed, don't do anything
  859. if ( containerWidth === this.containerWidth ) {
  860. return;
  861. }
  862. this.update();
  863. };
  864. /**
  865. * Returns styles for either jQuery animate or transition.
  866. * @param {Object} opts Transition options.
  867. * @return {!Object} Transforms for transitions, left/top for animate.
  868. * @private
  869. */
  870. Shuffle.prototype._getStylesForTransition = function( opts ) {
  871. var styles = {
  872. opacity: opts.opacity
  873. };
  874. if ( this.supported ) {
  875. styles[ TRANSFORM ] = Shuffle._getItemTransformString( opts.point, opts.scale );
  876. } else {
  877. styles.left = opts.point.x;
  878. styles.top = opts.point.y;
  879. }
  880. return styles;
  881. };
  882. /**
  883. * Transitions an item in the grid
  884. *
  885. * @param {Object} opts options.
  886. * @param {jQuery} opts.$item jQuery object representing the current item.
  887. * @param {Point} opts.point A point object with the x and y coordinates.
  888. * @param {number} opts.scale Amount to scale the item.
  889. * @param {number} opts.opacity Opacity of the item.
  890. * @param {Function} opts.callback Complete function for the animation.
  891. * @param {Function} opts.callfront Function to call before transitioning.
  892. * @private
  893. */
  894. Shuffle.prototype._transition = function( opts ) {
  895. var styles = this._getStylesForTransition( opts );
  896. this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
  897. };
  898. Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
  899. // Transition end handler removes its listener.
  900. function handleTransitionEnd( evt ) {
  901. // Make sure this event handler has not bubbled up from a child.
  902. if ( evt.target === evt.currentTarget ) {
  903. $( evt.target ).off( TRANSITIONEND, handleTransitionEnd );
  904. callback();
  905. }
  906. }
  907. callfront();
  908. // Transitions are not set until shuffle has loaded to avoid the initial transition.
  909. if ( !this.initialized ) {
  910. $item.css( styles );
  911. callback();
  912. return;
  913. }
  914. // Use CSS Transforms if we have them
  915. if ( this.supported ) {
  916. $item.css( styles );
  917. $item.on( TRANSITIONEND, handleTransitionEnd );
  918. // Use jQuery to animate left/top
  919. } else {
  920. // Save the deferred object which jQuery returns.
  921. var anim = $item.stop( true ).animate( styles, this.speed, 'swing', callback );
  922. // Push the animation to the list of pending animations.
  923. this._animations.push( anim.promise() );
  924. }
  925. };
  926. /**
  927. * Execute the styles gathered in the style queue. This applies styles to elements,
  928. * triggering transitions.
  929. * @param {boolean} noLayout Whether to trigger a layout event.
  930. * @private
  931. */
  932. Shuffle.prototype._processStyleQueue = function( noLayout ) {
  933. var $transitions = $();
  934. // Iterate over the queue and keep track of ones that use transitions.
  935. each(this.styleQueue, function( transitionObj ) {
  936. if ( transitionObj.skipTransition ) {
  937. this._styleImmediately( transitionObj );
  938. } else {
  939. $transitions = $transitions.add( transitionObj.$item );
  940. this._transition( transitionObj );
  941. }
  942. }, this);
  943. if ( $transitions.length > 0 && this.initialized ) {
  944. // Set flag that shuffle is currently in motion.
  945. this.isTransitioning = true;
  946. if ( this.supported ) {
  947. this._whenCollectionDone( $transitions, TRANSITIONEND, this._movementFinished );
  948. // The _transition function appends a promise to the animations array.
  949. // When they're all complete, do things.
  950. } else {
  951. this._whenAnimationsDone( this._movementFinished );
  952. }
  953. // A call to layout happened, but none of the newly filtered items will
  954. // change position. Asynchronously fire the callback here.
  955. } else if ( !noLayout ) {
  956. defer( this._layoutEnd, this );
  957. }
  958. // Remove everything in the style queue
  959. this.styleQueue.length = 0;
  960. };
  961. /**
  962. * Apply styles without a transition.
  963. * @param {Object} opts Transitions options object.
  964. * @private
  965. */
  966. Shuffle.prototype._styleImmediately = function( opts ) {
  967. Shuffle._skipTransition(opts.$item[0], function() {
  968. opts.$item.css( this._getStylesForTransition( opts ) );
  969. }, this);
  970. };
  971. Shuffle.prototype._movementFinished = function() {
  972. this.isTransitioning = false;
  973. this._layoutEnd();
  974. };
  975. Shuffle.prototype._layoutEnd = function() {
  976. this._fire( Shuffle.EventType.LAYOUT );
  977. };
  978. Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
  979. // Add classes and set initial positions.
  980. this._initItems( $newItems );
  981. // Add transition to each item.
  982. this._setTransitions( $newItems );
  983. // Update the list of
  984. this.$items = this._getItems();
  985. // Shrink all items (without transitions).
  986. this._shrink( $newItems );
  987. each(this.styleQueue, function( transitionObj ) {
  988. transitionObj.skipTransition = true;
  989. });
  990. // Apply shrink positions, but do not cause a layout event.
  991. this._processStyleQueue( true );
  992. if ( addToEnd ) {
  993. this._addItemsToEnd( $newItems, isSequential );
  994. } else {
  995. this.shuffle( this.lastFilter );
  996. }
  997. };
  998. Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
  999. // Get ones that passed the current filter
  1000. var $passed = this._filter( null, $newItems );
  1001. var passed = $passed.get();
  1002. // How many filtered elements?
  1003. this._updateItemCount();
  1004. this._layout( passed, true );
  1005. if ( isSequential && this.supported ) {
  1006. this._setSequentialDelay( passed );
  1007. }
  1008. this._revealAppended( passed );
  1009. };
  1010. /**
  1011. * Triggers appended elements to fade in.
  1012. * @param {ArrayLike.<Element>} $newFilteredItems Collection of elements.
  1013. * @private
  1014. */
  1015. Shuffle.prototype._revealAppended = function( newFilteredItems ) {
  1016. defer(function() {
  1017. each(newFilteredItems, function( el ) {
  1018. var $item = $( el );
  1019. this._transition({
  1020. $item: $item,
  1021. opacity: 1,
  1022. point: $item.data('point'),
  1023. scale: DEFAULT_SCALE
  1024. });
  1025. }, this);
  1026. this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
  1027. $(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
  1028. this._movementFinished();
  1029. });
  1030. }, this, this.revealAppendedDelay);
  1031. };
  1032. /**
  1033. * Execute a function when an event has been triggered for every item in a collection.
  1034. * @param {jQuery} $collection Collection of elements.
  1035. * @param {string} eventName Event to listen for.
  1036. * @param {Function} callback Callback to execute when they're done.
  1037. * @private
  1038. */
  1039. Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callback ) {
  1040. var done = 0;
  1041. var items = $collection.length;
  1042. var self = this;
  1043. function handleEventName( evt ) {
  1044. if ( evt.target === evt.currentTarget ) {
  1045. $( evt.target ).off( eventName, handleEventName );
  1046. done++;
  1047. // Execute callback if all items have emitted the correct event.
  1048. if ( done === items ) {
  1049. callback.call( self );
  1050. }
  1051. }
  1052. }
  1053. // Bind the event to all items.
  1054. $collection.on( eventName, handleEventName );
  1055. };
  1056. /**
  1057. * Execute a callback after jQuery `animate` for a collection has finished.
  1058. * @param {Function} callback Callback to execute when they're done.
  1059. * @private
  1060. */
  1061. Shuffle.prototype._whenAnimationsDone = function( callback ) {
  1062. $.when.apply( null, this._animations ).always( $.proxy( function() {
  1063. this._animations.length = 0;
  1064. callback.call( this );
  1065. }, this ));
  1066. };
  1067. /**
  1068. * Public Methods
  1069. */
  1070. /**
  1071. * The magic. This is what makes the plugin 'shuffle'
  1072. * @param {string|Function} [category] Category to filter by. Can be a function
  1073. * @param {Object} [sortObj] A sort object which can sort the filtered set
  1074. */
  1075. Shuffle.prototype.shuffle = function( category, sortObj ) {
  1076. if ( !this.enabled || this.isTransitioning ) {
  1077. return;
  1078. }
  1079. if ( !category ) {
  1080. category = ALL_ITEMS;
  1081. }
  1082. this._filter( category );
  1083. // How many filtered elements?
  1084. this._updateItemCount();
  1085. // Shrink each concealed item
  1086. this._shrink();
  1087. // Update transforms on .filtered elements so they will animate to their new positions
  1088. this.sort( sortObj );
  1089. };
  1090. /**
  1091. * Gets the .filtered elements, sorts them, and passes them to layout.
  1092. * @param {Object} opts the options object for the sorted plugin
  1093. */
  1094. Shuffle.prototype.sort = function( opts ) {
  1095. if ( this.enabled && !this.isTransitioning ) {
  1096. this._resetCols();
  1097. var sortOptions = opts || this.lastSort;
  1098. var items = this._getFilteredItems().sorted( sortOptions );
  1099. this._layout( items );
  1100. this.lastSort = sortOptions;
  1101. }
  1102. };
  1103. /**
  1104. * Reposition everything.
  1105. * @param {boolean} isOnlyLayout If true, column and gutter widths won't be
  1106. * recalculated.
  1107. */
  1108. Shuffle.prototype.update = function( isOnlyLayout ) {
  1109. if ( this.enabled && !this.isTransitioning ) {
  1110. if ( !isOnlyLayout ) {
  1111. // Get updated colCount
  1112. this._setColumns();
  1113. }
  1114. // Layout items
  1115. this.sort();
  1116. }
  1117. };
  1118. /**
  1119. * Use this instead of `update()` if you don't need the columns and gutters updated
  1120. * Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
  1121. * could be off.
  1122. */
  1123. Shuffle.prototype.layout = function() {
  1124. this.update( true );
  1125. };
  1126. /**
  1127. * New items have been appended to shuffle. Fade them in sequentially
  1128. * @param {jQuery} $newItems jQuery collection of new items
  1129. * @param {boolean} [addToEnd=false] If true, new items will be added to the end / bottom
  1130. * of the items. If not true, items will be mixed in with the current sort order.
  1131. * @param {boolean} [isSequential=true] If false, new items won't sequentially fade in
  1132. */
  1133. Shuffle.prototype.appended = function( $newItems, addToEnd, isSequential ) {
  1134. this._addItems( $newItems, addToEnd === true, isSequential !== false );
  1135. };
  1136. /**
  1137. * Disables shuffle from updating dimensions and layout on resize
  1138. */
  1139. Shuffle.prototype.disable = function() {
  1140. this.enabled = false;
  1141. };
  1142. /**
  1143. * Enables shuffle again
  1144. * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
  1145. */
  1146. Shuffle.prototype.enable = function( isUpdateLayout ) {
  1147. this.enabled = true;
  1148. if ( isUpdateLayout !== false ) {
  1149. this.update();
  1150. }
  1151. };
  1152. /**
  1153. * Remove 1 or more shuffle items
  1154. * @param {jQuery} $collection A jQuery object containing one or more element in shuffle
  1155. * @return {Shuffle} The shuffle object
  1156. */
  1157. Shuffle.prototype.remove = function( $collection ) {
  1158. // If this isn't a jquery object, exit
  1159. if ( !$collection.length || !$collection.jquery ) {
  1160. return;
  1161. }
  1162. function handleRemoved() {
  1163. // Remove the collection in the callback
  1164. $collection.remove();
  1165. // Update things now that elements have been removed.
  1166. this.$items = this._getItems();
  1167. this._updateItemCount();
  1168. this._fire( Shuffle.EventType.REMOVED, [ $collection, this ] );
  1169. // Let it get garbage collected
  1170. $collection = null;
  1171. }
  1172. // Hide collection first.
  1173. this._toggleFilterClasses( $(), $collection );
  1174. this._shrink( $collection );
  1175. this.sort();
  1176. this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
  1177. };
  1178. /**
  1179. * Destroys shuffle, removes events, styles, and classes
  1180. */
  1181. Shuffle.prototype.destroy = function() {
  1182. // If there is more than one shuffle instance on the page,
  1183. // removing the resize handler from the window would remove them
  1184. // all. This is why a unique value is needed.
  1185. $window.off('.' + this.unique);
  1186. // Reset container styles
  1187. this.$el
  1188. .removeClass( SHUFFLE )
  1189. .removeAttr('style')
  1190. .removeData( SHUFFLE );
  1191. // Reset individual item styles
  1192. this.$items
  1193. .removeAttr('style')
  1194. .removeData('point')
  1195. .removeData('scale')
  1196. .removeClass([
  1197. Shuffle.ClassName.CONCEALED,
  1198. Shuffle.ClassName.FILTERED,
  1199. Shuffle.ClassName.SHUFFLE_ITEM
  1200. ].join(' '));
  1201. // Null DOM references
  1202. this.$items = null;
  1203. this.$el = null;
  1204. this.sizer = null;
  1205. this.element = null;
  1206. // Set a flag so if a debounced resize has been triggered,
  1207. // it can first check if it is actually destroyed and not doing anything
  1208. this.destroyed = true;
  1209. };
  1210. // Plugin definition
  1211. $.fn.shuffle = function( opts ) {
  1212. var args = Array.prototype.slice.call( arguments, 1 );
  1213. return this.each(function() {
  1214. var $this = $( this );
  1215. var shuffle = $this.data( SHUFFLE );
  1216. // If we don't have a stored shuffle, make a new one and save it
  1217. if ( !shuffle ) {
  1218. shuffle = new Shuffle( this, opts );
  1219. $this.data( SHUFFLE, shuffle );
  1220. } else if ( typeof opts === 'string' && shuffle[ opts ] ) {
  1221. shuffle[ opts ].apply( shuffle, args );
  1222. }
  1223. });
  1224. };
  1225. // http://stackoverflow.com/a/962890/373422
  1226. function randomize( array ) {
  1227. var tmp, current;
  1228. var top = array.length;
  1229. if ( !top ) {
  1230. return array;
  1231. }
  1232. while ( --top ) {
  1233. current = Math.floor( Math.random() * (top + 1) );
  1234. tmp = array[ current ];
  1235. array[ current ] = array[ top ];
  1236. array[ top ] = tmp;
  1237. }
  1238. return array;
  1239. }
  1240. // You can return `undefined` from the `by` function to revert to DOM order
  1241. // This plugin does NOT return a jQuery object. It returns a plain array because
  1242. // jQuery sorts everything in DOM order.
  1243. $.fn.sorted = function(options) {
  1244. var opts = $.extend({}, $.fn.sorted.defaults, options);
  1245. var arr = this.get();
  1246. var revert = false;
  1247. if ( !arr.length ) {
  1248. return [];
  1249. }
  1250. if ( opts.randomize ) {
  1251. return randomize( arr );
  1252. }
  1253. // Sort the elements by the opts.by function.
  1254. // If we don't have opts.by, default to DOM order
  1255. if ( $.isFunction( opts.by ) ) {
  1256. arr.sort(function(a, b) {
  1257. // Exit early if we already know we want to revert
  1258. if ( revert ) {
  1259. return 0;
  1260. }
  1261. var valA = opts.by($(a));
  1262. var valB = opts.by($(b));
  1263. // If both values are undefined, use the DOM order
  1264. if ( valA === undefined && valB === undefined ) {
  1265. revert = true;
  1266. return 0;
  1267. }
  1268. if ( valA < valB || valA === 'sortFirst' || valB === 'sortLast' ) {
  1269. return -1;
  1270. }
  1271. if ( valA > valB || valA === 'sortLast' || valB === 'sortFirst' ) {
  1272. return 1;
  1273. }
  1274. return 0;
  1275. });
  1276. }
  1277. // Revert to the original array if necessary
  1278. if ( revert ) {
  1279. return this.get();
  1280. }
  1281. if ( opts.reverse ) {
  1282. arr.reverse();
  1283. }
  1284. return arr;
  1285. };
  1286. $.fn.sorted.defaults = {
  1287. reverse: false, // Use array.reverse() to reverse the results
  1288. by: null, // Sorting function
  1289. randomize: false // If true, this will skip the sorting and return a randomized order in the array
  1290. };
  1291. return Shuffle;
  1292. });