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