/*! * Isotope v2.0.0-beta.6 * Magical sorting and filtering layouts * http://isotope.metafizzy.co */ ( function( window ) { 'use strict'; // -------------------------- vars -------------------------- // var jQuery = window.jQuery; // -------------------------- helpers -------------------------- // // extend objects function extend( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; } var trim = String.prototype.trim ? function( str ) { return str.trim(); } : function( str ) { return str.replace( /^\s+|\s+$/g, '' ); }; var docElem = document.documentElement; var getText = docElem.textContent ? function( elem ) { return elem.textContent; } : function( elem ) { return elem.innerText; }; var objToString = Object.prototype.toString; function isArray( obj ) { return objToString.call( obj ) === '[object Array]'; } // index of helper cause IE8 var indexOf = Array.prototype.indexOf ? function( ary, obj ) { return ary.indexOf( obj ); } : function( ary, obj ) { for ( var i=0, len = ary.length; i < len; i++ ) { if ( ary[i] === obj ) { return i; } } return -1; }; // turn element or nodeList into an array function makeArray( obj ) { var ary = []; if ( isArray( obj ) ) { // use object if already an array ary = obj; } else if ( obj && typeof obj.length === 'number' ) { // convert nodeList to array for ( var i=0, len = obj.length; i < len; i++ ) { ary.push( obj[i] ); } } else { // array of single index ary.push( obj ); } return ary; } function removeFrom( obj, ary ) { var index = indexOf( ary, obj ); if ( index !== -1 ) { ary.splice( index, 1 ); } } // -------------------------- isotopeDefinition -------------------------- // // used for AMD definition and requires function isotopeDefinition( Outlayer, getSize, matchesSelector, Item, LayoutMode ) { // create an Outlayer layout class var Isotope = Outlayer.create( 'isotope', { layoutMode: "masonry", isJQueryFiltering: true, sortAscending: true }); Isotope.Item = Isotope.prototype.settings.item = Item; Isotope.LayoutMode = LayoutMode; Isotope.prototype._create = function() { this.itemGUID = 0; // functions that sort items this._sorters = {}; // call super Outlayer.prototype._create.call( this ); // create layout modes this.modes = {}; // start filteredItems with all items this.filteredItems = this.items; // keep of track of sortBys this.sortHistory = [ 'original-order' ]; // create from registered layout modes for ( var name in LayoutMode.modes ) { this._initLayoutMode( name ); } }; Isotope.prototype.reloadItems = function() { // reset item ID counter this.itemGUID = 0; // call super Outlayer.prototype.reloadItems.call( this ); }; Isotope.prototype._itemize = function() { var items = Outlayer.prototype._itemize.apply( this, arguments ); // assign ID for original-order for ( var i=0, len = items.length; i < len; i++ ) { var item = items[i]; item.id = this.itemGUID++; } this.updateSortData( items ); return items; }; // -------------------------- layout -------------------------- // Isotope.prototype._initLayoutMode = function( name ) { var Mode = LayoutMode.modes[ name ]; // set mode options // HACK extend initial options, back-fill in default options var initialOpts = this.options[ name ] || {}; this.options[ name ] = Mode.options ? extend( Mode.options, initialOpts ) : initialOpts; // init layout mode instance this.modes[ name ] = new Mode( this ); }; Isotope.prototype.layout = function() { // if first time doing layout, do all magic if ( !this._isLayoutInited && this.options.isInitLayout ) { this.arrange(); return; } this._layout(); }; // private method to be used in layout() & magic() Isotope.prototype._layout = function() { // don't animate first layout var isInstant = this._getIsInstant(); // layout flow this._resetLayout(); this._manageStamps(); this.layoutItems( this.filteredItems, isInstant ); // flag for initalized this._isLayoutInited = true; }; // filter + sort + layout Isotope.prototype.arrange = function( opts ) { // set any options pass this.option( opts ); this._getIsInstant(); // filter, sort, and layout this.filteredItems = this._filter( this.items ); this._sort(); this._layout(); }; // alias to _init for main plugin method Isotope.prototype._init = Isotope.prototype.arrange; // HACK // Don't animate/transition first layout // Or don't animate/transition other layouts Isotope.prototype._getIsInstant = function() { var isInstant = this.options.isLayoutInstant !== undefined ? this.options.isLayoutInstant : !this._isLayoutInited; return this._isInstant = isInstant; }; // -------------------------- filter -------------------------- // Isotope.prototype._filter = function( items ) { var filter = this.options.filter; filter = filter || '*'; var matches = []; var hiddenMatched = []; var visibleUnmatched = []; var test = this._getFilterTest( filter ); // test each item for ( var i=0, len = items.length; i < len; i++ ) { var item = items[i]; if ( item.isIgnored ) { continue; } // add item to either matched or unmatched group var isMatched = test( item ); // item.isFilterMatched = isMatched; // add to matches if its a match if ( isMatched ) { matches.push( item ); } // add to additional group if item needs to be hidden or revealed if ( isMatched && item.isHidden ) { hiddenMatched.push( item ); } else if ( !isMatched && !item.isHidden ) { visibleUnmatched.push( item ); } } // HACK // disable transition if instant var _transitionDuration = this.options.transitionDuration; if ( this._isInstant ) { this.options.transitionDuration = 0; } this.reveal( hiddenMatched ); this.hide( visibleUnmatched ); // set back if ( this._isInstant ) { this.options.transitionDuration = _transitionDuration; } return matches; }; // get a jQuery, function, or a matchesSelector test given the filter Isotope.prototype._getFilterTest = function( filter ) { var test; if ( jQuery && this.options.isJQueryFiltering ) { test = function( item ) { return jQuery( item.element ).is( filter ); }; } else if ( typeof filter === 'function' ) { test = function( item ) { return filter( item.element ); }; } else { test = function( item ) { return matchesSelector( item.element, filter ); }; } return test; }; // -------------------------- sorting -------------------------- // Isotope.prototype.updateSortData = function( items ) { // update sorters var getSortData = this.options.getSortData; for ( var key in getSortData ) { var sorter = getSortData[ key ]; this._sorters[ key ] = mungeSorter( sorter ); } // update item sort data // default to all items if none are passed in items = items || this.items; for ( var i=0, len = items.length; i < len; i++ ) { var item = items[i]; if ( item.isIgnored ) { continue; } item.updateSortData(); } }; // ----- munge sorter ----- // // encapsulate this, as we just need mungeSorter // other functions in here are just for munging var mungeSorter = ( function() { // add a magic layer to sorters for convienent shorthands // `.foo-bar` will use the text of .foo-bar querySelector // `[foo-bar]` will use attribute // you can also add parser // `.foo-bar parseInt` will parse that as a number function mungeSorter( sorter ) { // if not a string, return function or whatever it is if ( typeof sorter !== 'string' ) { return sorter; } // parse the sorter string var args = trim( sorter ).split(' '); var query = args[0]; // check if query looks like [an-attribute] var attrMatch = query.match( /^\[(.+)\]$/ ); var attr = attrMatch && attrMatch[1]; var getValue = getValueGetter( attr, query ); // use second argument as a parser var parser = getParser( args[1] ); // parse the value, if there was a parser sorter = parser ? function( elem ) { return elem && parser( getValue( elem ) ); } : // otherwise just return value function( elem ) { return elem && getValue( elem ); }; return sorter; } // get an attribute getter, or get text of the querySelector function getValueGetter( attr, query ) { var getValue; // if query looks like [foo-bar], get attribute if ( attr ) { getValue = function( elem ) { return elem.getAttribute( attr ); }; } else { // otherwise, assume its a querySelector, and get its text getValue = function( elem ) { var child = elem.querySelector( query ); return child && getText( child ); }; } return getValue; } // return a parser function if arg matches function getParser( arg ) { var parser; switch ( arg ) { case 'parseInt' : parser = function( val ) { return parseInt( val, 10 ); }; break; case 'parseFloat' : parser = function( val ) { return parseFloat( val ); }; break; default : // just return val if parser isn't one of these // TODO - console log that that parser doesn't exist parser = function( val ) { return val; }; } return parser; } return mungeSorter; })(); // ----- sort method ----- // // sort filteredItem order Isotope.prototype._sort = function() { var sortByOpt = this.options.sortBy; if ( !sortByOpt ) { return; } // concat all sortBy and sortHistory var sortBys = [].concat.apply( sortByOpt, this.sortHistory ); // sort magic var itemSorter = getItemSorter( sortBys, this.options.sortAscending ); this.filteredItems.sort( itemSorter ); // keep track of sortBy History if ( sortByOpt !== this.sortHistory[0] ) { // add to front, oldest goes in last this.sortHistory.unshift( sortByOpt ); } }; // returns a function used for sorting function getItemSorter( sortBys, sortAsc ) { return function sorter( itemA, itemB ) { // cycle through all sortKeys for ( var i = 0, len = sortBys.length; i < len; i++ ) { var sortBy = sortBys[i]; var a = itemA.sortData[ sortBy ]; var b = itemB.sortData[ sortBy ]; if ( a > b || a < b ) { // if sortAsc is an object, use the value given the sortBy key var isAscending = sortAsc[ sortBy ] !== undefined ? sortAsc[ sortBy ] : sortAsc; var direction = isAscending ? 1 : -1; return ( a > b ? 1 : -1 ) * direction; } } return 0; }; } // -------------------------- methods -------------------------- // // get layout mode Isotope.prototype._mode = function() { var layoutMode = this.options.layoutMode; var mode = this.modes[ layoutMode ]; if ( !mode ) { // TODO console.error throw new Error( 'No layout mode: ' + layoutMode ); } // HACK sync mode's options // any options set after init for layout mode need to be synced mode.options = this.options[ layoutMode ]; return mode; }; Isotope.prototype._resetLayout = function() { // trigger original reset layout Outlayer.prototype._resetLayout.call( this ); this._mode()._resetLayout(); }; Isotope.prototype._getItemLayoutPosition = function( item ) { return this._mode()._getItemLayoutPosition( item ); }; Isotope.prototype._manageStamp = function( stamp ) { var mode = this._mode(); // HACK copy over some options mode.options.isOriginLeft = this.options.isOriginLeft; mode.options.isOriginTop = this.options.isOriginTop; mode._manageStamp( stamp ); }; Isotope.prototype._getContainerSize = function() { return this._mode()._getContainerSize(); }; Isotope.prototype.resize = function() { this._mode().resize(); }; // -------------------------- adding & removing -------------------------- // // HEADS UP overwrites default Outlayer appended Isotope.prototype.appended = function( elems ) { var items = this.addItems( elems ); if ( !items.length ) { return; } var filteredItems = this._filterRevealAdded( items ); // add to filteredItems this.filteredItems = this.filteredItems.concat( filteredItems ); }; // HEADS UP overwrites default Outlayer prepended Isotope.prototype.prepended = function( elems ) { var items = this._itemize( elems ); if ( !items.length ) { return; } // add items to beginning of collection var previousItems = this.items.slice(0); this.items = items.concat( previousItems ); // start new layout this._resetLayout(); this._manageStamps(); // layout new stuff without transition var filteredItems = this._filterRevealAdded( items ); // layout previous items this.layoutItems( previousItems ); // add to filteredItems this.filteredItems = filteredItems.concat( this.filteredItems ); }; Isotope.prototype._filterRevealAdded = function( items ) { // disable transition for filtering var transitionDuration = this.options.transitionDuration; this.options.transitionDuration = 0; var filteredItems = this._filter( items ); // re-enable transition for reveal this.options.transitionDuration = transitionDuration; // layout and reveal just the new items this.layoutItems( filteredItems, true ); this.reveal( filteredItems ); return items; }; /** * Filter, sort, and layout newly-appended item elements * @param {Array or NodeList or Element} elems */ Isotope.prototype.insert = function( elems ) { var items = this.addItems( elems ); if ( !items.length ) { return; } this.arrange(); }; var _remove = Isotope.prototype.remove; Isotope.prototype.remove = function( elems ) { elems = makeArray( elems ); var removeItems = this.getItems( elems ); // do regular thing _remove.call( this, elems ); // bail if no items to remove if ( !removeItems || !removeItems.length ) { return; } // remove elems from filteredItems for ( var i=0, len = removeItems.length; i < len; i++ ) { var item = removeItems[i]; // remove item from collection removeFrom( item, this.filteredItems ); } }; // ----- ----- // return Isotope; } // -------------------------- transport -------------------------- // if ( typeof define === 'function' && define.amd ) { // AMD define( [ 'outlayer/outlayer', 'get-size/get-size', 'matches-selector/matches-selector', './item', './layout-mode' ], isotopeDefinition ); } else { // browser global window.Isotope = isotopeDefinition( window.Outlayer, window.getSize, window.matchesSelector, window.Isotope.Item, window.Isotope.LayoutMode ); } })( window );