/*! * Isotope v3.0.6 * * Licensed GPLv3 for open source use * or Isotope Commercial License for commercial use * * https://isotope.metafizzy.co * Copyright 2010-2018 Metafizzy */ ( function( window, factory ) { // universal module definition /* jshint strict: false */ /*globals define, module, require */ if ( typeof define == 'function' && define.amd ) { // AMD define( [ 'outlayer/outlayer', 'get-size/get-size', 'desandro-matches-selector/matches-selector', 'fizzy-ui-utils/utils', './item', './layout-mode', // include default layout modes './layout-modes/masonry', './layout-modes/fit-rows', './layout-modes/vertical' ], function( Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ) { return factory( window, Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ); }); } else if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('outlayer'), require('get-size'), require('desandro-matches-selector'), require('fizzy-ui-utils'), require('./item'), require('./layout-mode'), // include default layout modes require('./layout-modes/masonry'), require('./layout-modes/fit-rows'), require('./layout-modes/vertical') ); } else { // browser global window.Isotope = factory( window, window.Outlayer, window.getSize, window.matchesSelector, window.fizzyUIUtils, window.Isotope.Item, window.Isotope.LayoutMode ); } }( window, function factory( window, Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ) { 'use strict'; // -------------------------- vars -------------------------- // var jQuery = window.jQuery; // -------------------------- helpers -------------------------- // var trim = String.prototype.trim ? function( str ) { return str.trim(); } : function( str ) { return str.replace( /^\s+|\s+$/g, '' ); }; // -------------------------- isotopeDefinition -------------------------- // // create an Outlayer layout class var Isotope = Outlayer.create( 'isotope', { layoutMode: 'masonry', isJQueryFiltering: true, sortAscending: true }); Isotope.Item = Item; Isotope.LayoutMode = LayoutMode; var proto = Isotope.prototype; proto._create = function() { this.itemGUID = 0; // functions that sort items this._sorters = {}; this._getSorters(); // 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 ); } }; proto.reloadItems = function() { // reset item ID counter this.itemGUID = 0; // call super Outlayer.prototype.reloadItems.call( this ); }; proto._itemize = function() { var items = Outlayer.prototype._itemize.apply( this, arguments ); // assign ID for original-order for ( var i=0; i < items.length; i++ ) { var item = items[i]; item.id = this.itemGUID++; } this._updateItemsSortData( items ); return items; }; // -------------------------- layout -------------------------- // proto._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 ? utils.extend( Mode.options, initialOpts ) : initialOpts; // init layout mode instance this.modes[ name ] = new Mode( this ); }; proto.layout = function() { // if first time doing layout, do all magic if ( !this._isLayoutInited && this._getOption('initLayout') ) { this.arrange(); return; } this._layout(); }; // private method to be used in layout() & magic() proto._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 proto.arrange = function( opts ) { // set any options pass this.option( opts ); this._getIsInstant(); // filter, sort, and layout // filter var filtered = this._filter( this.items ); this.filteredItems = filtered.matches; this._bindArrangeComplete(); if ( this._isInstant ) { this._noTransition( this._hideReveal, [ filtered ] ); } else { this._hideReveal( filtered ); } this._sort(); this._layout(); }; // alias to _init for main plugin method proto._init = proto.arrange; proto._hideReveal = function( filtered ) { this.reveal( filtered.needReveal ); this.hide( filtered.needHide ); }; // HACK // Don't animate/transition first layout // Or don't animate/transition other layouts proto._getIsInstant = function() { var isLayoutInstant = this._getOption('layoutInstant'); var isInstant = isLayoutInstant !== undefined ? isLayoutInstant : !this._isLayoutInited; this._isInstant = isInstant; return isInstant; }; // listen for layoutComplete, hideComplete and revealComplete // to trigger arrangeComplete proto._bindArrangeComplete = function() { // listen for 3 events to trigger arrangeComplete var isLayoutComplete, isHideComplete, isRevealComplete; var _this = this; function arrangeParallelCallback() { if ( isLayoutComplete && isHideComplete && isRevealComplete ) { _this.dispatchEvent( 'arrangeComplete', null, [ _this.filteredItems ] ); } } this.once( 'layoutComplete', function() { isLayoutComplete = true; arrangeParallelCallback(); }); this.once( 'hideComplete', function() { isHideComplete = true; arrangeParallelCallback(); }); this.once( 'revealComplete', function() { isRevealComplete = true; arrangeParallelCallback(); }); }; // -------------------------- filter -------------------------- // proto._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; i < items.length; 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 ); } } // return collections of items to be manipulated return { matches: matches, needReveal: hiddenMatched, needHide: visibleUnmatched }; }; // get a jQuery, function, or a matchesSelector test given the filter proto._getFilterTest = function( filter ) { if ( jQuery && this.options.isJQueryFiltering ) { // use jQuery return function( item ) { return jQuery( item.element ).is( filter ); }; } if ( typeof filter == 'function' ) { // use filter as function return function( item ) { return filter( item.element ); }; } // default, use filter as selector string return function( item ) { return matchesSelector( item.element, filter ); }; }; // -------------------------- sorting -------------------------- // /** * @params {Array} elems * @public */ proto.updateSortData = function( elems ) { // get items var items; if ( elems ) { elems = utils.makeArray( elems ); items = this.getItems( elems ); } else { // update all items if no elems provided items = this.items; } this._getSorters(); this._updateItemsSortData( items ); }; proto._getSorters = function() { var getSortData = this.options.getSortData; for ( var key in getSortData ) { var sorter = getSortData[ key ]; this._sorters[ key ] = mungeSorter( sorter ); } }; /** * @params {Array} items - of Isotope.Items * @private */ proto._updateItemsSortData = function( items ) { // do not update if no items var len = items && items.length; for ( var i=0; len && i < len; i++ ) { var item = items[i]; 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 = Isotope.sortDataParsers[ 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 ) { // if query looks like [foo-bar], get attribute if ( attr ) { return function getAttribute( elem ) { return elem.getAttribute( attr ); }; } // otherwise, assume its a querySelector, and get its text return function getChildText( elem ) { var child = elem.querySelector( query ); return child && child.textContent; }; } return mungeSorter; })(); // parsers used in getSortData shortcut strings Isotope.sortDataParsers = { 'parseInt': function( val ) { return parseInt( val, 10 ); }, 'parseFloat': function( val ) { return parseFloat( val ); } }; // ----- sort method ----- // // sort filteredItem order proto._sort = function() { if ( !this.options.sortBy ) { return; } // keep track of sortBy History var sortBys = utils.makeArray( this.options.sortBy ); if ( !this._getIsSameSortBy( sortBys ) ) { // concat all sortBy and sortHistory, add to front, oldest goes in last this.sortHistory = sortBys.concat( this.sortHistory ); } // sort magic var itemSorter = getItemSorter( this.sortHistory, this.options.sortAscending ); this.filteredItems.sort( itemSorter ); }; // check if sortBys is same as start of sortHistory proto._getIsSameSortBy = function( sortBys ) { for ( var i=0; i < sortBys.length; i++ ) { if ( sortBys[i] != this.sortHistory[i] ) { return false; } } return true; }; // returns a function used for sorting function getItemSorter( sortBys, sortAsc ) { return function sorter( itemA, itemB ) { // cycle through all sortKeys for ( var i = 0; i < sortBys.length; 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 proto._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; }; proto._resetLayout = function() { // trigger original reset layout Outlayer.prototype._resetLayout.call( this ); this._mode()._resetLayout(); }; proto._getItemLayoutPosition = function( item ) { return this._mode()._getItemLayoutPosition( item ); }; proto._manageStamp = function( stamp ) { this._mode()._manageStamp( stamp ); }; proto._getContainerSize = function() { return this._mode()._getContainerSize(); }; proto.needsResizeLayout = function() { return this._mode().needsResizeLayout(); }; // -------------------------- adding & removing -------------------------- // // HEADS UP overwrites default Outlayer appended proto.appended = function( elems ) { var items = this.addItems( elems ); if ( !items.length ) { return; } // filter, layout, reveal new items var filteredItems = this._filterRevealAdded( items ); // add to filteredItems this.filteredItems = this.filteredItems.concat( filteredItems ); }; // HEADS UP overwrites default Outlayer prepended proto.prepended = function( elems ) { var items = this._itemize( elems ); if ( !items.length ) { return; } // start new layout this._resetLayout(); this._manageStamps(); // filter, layout, reveal new items var filteredItems = this._filterRevealAdded( items ); // layout previous items this.layoutItems( this.filteredItems ); // add to items and filteredItems this.filteredItems = filteredItems.concat( this.filteredItems ); this.items = items.concat( this.items ); }; proto._filterRevealAdded = function( items ) { var filtered = this._filter( items ); this.hide( filtered.needHide ); // reveal all new items this.reveal( filtered.matches ); // layout new items, no transition this.layoutItems( filtered.matches, true ); return filtered.matches; }; /** * Filter, sort, and layout newly-appended item elements * @param {Array or NodeList or Element} elems */ proto.insert = function( elems ) { var items = this.addItems( elems ); if ( !items.length ) { return; } // append item elements var i, item; var len = items.length; for ( i=0; i < len; i++ ) { item = items[i]; this.element.appendChild( item.element ); } // filter new stuff var filteredInsertItems = this._filter( items ).matches; // set flag for ( i=0; i < len; i++ ) { items[i].isLayoutInstant = true; } this.arrange(); // reset flag for ( i=0; i < len; i++ ) { delete items[i].isLayoutInstant; } this.reveal( filteredInsertItems ); }; var _remove = proto.remove; proto.remove = function( elems ) { elems = utils.makeArray( elems ); var removeItems = this.getItems( elems ); // do regular thing _remove.call( this, elems ); // bail if no items to remove var len = removeItems && removeItems.length; // remove elems from filteredItems for ( var i=0; len && i < len; i++ ) { var item = removeItems[i]; // remove item from collection utils.removeFrom( this.filteredItems, item ); } }; proto.shuffle = function() { // update random sortData for ( var i=0; i < this.items.length; i++ ) { var item = this.items[i]; item.sortData.random = Math.random(); } this.options.sortBy = 'random'; this._sort(); this._layout(); }; /** * trigger fn without transition * kind of hacky to have this in the first place * @param {Function} fn * @param {Array} args * @returns ret * @private */ proto._noTransition = function( fn, args ) { // save transitionDuration before disabling var transitionDuration = this.options.transitionDuration; // disable transition this.options.transitionDuration = 0; // do it var returnValue = fn.apply( this, args ); // re-enable transition for reveal this.options.transitionDuration = transitionDuration; return returnValue; }; // ----- helper methods ----- // /** * getter method for getting filtered item elements * @returns {Array} elems - collection of item elements */ proto.getFilteredItemElements = function() { return this.filteredItems.map( function( item ) { return item.element; }); }; // ----- ----- // return Isotope; }));