mirror of https://github.com/metafizzy/isotope
Filter & sort magical layouts
http://isotope.metafizzy.co
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
628 lines
17 KiB
628 lines
17 KiB
/*! |
|
* Isotope v2.2.0 |
|
* |
|
* Licensed GPLv3 for open source use |
|
* or Isotope Commercial License for commercial use |
|
* |
|
* http://isotope.metafizzy.co |
|
* Copyright 2015 Metafizzy |
|
*/ |
|
|
|
( function( window, factory ) { |
|
'use strict'; |
|
// universal module definition |
|
|
|
if ( typeof define == 'function' && define.amd ) { |
|
// AMD |
|
define( [ |
|
'outlayer/outlayer', |
|
'get-size/get-size', |
|
'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 exports == 'object' ) { |
|
// 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, '' ); |
|
}; |
|
|
|
var docElem = document.documentElement; |
|
|
|
var getText = docElem.textContent ? |
|
function( elem ) { |
|
return elem.textContent; |
|
} : |
|
function( elem ) { |
|
return elem.innerText; |
|
}; |
|
|
|
// -------------------------- isotopeDefinition -------------------------- // |
|
|
|
// create an Outlayer layout class |
|
var Isotope = Outlayer.create( 'isotope', { |
|
layoutMode: "masonry", |
|
isJQueryFiltering: true, |
|
sortAscending: true |
|
}); |
|
|
|
Isotope.Item = Item; |
|
Isotope.LayoutMode = LayoutMode; |
|
|
|
Isotope.prototype._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 ); |
|
} |
|
}; |
|
|
|
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._updateItemsSortData( 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 ? |
|
utils.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 |
|
|
|
// filter |
|
var filtered = this._filter( this.items ); |
|
this.filteredItems = filtered.matches; |
|
|
|
var _this = this; |
|
function hideReveal() { |
|
_this.reveal( filtered.needReveal ); |
|
_this.hide( filtered.needHide ); |
|
} |
|
|
|
this._bindArrangeComplete(); |
|
|
|
if ( this._isInstant ) { |
|
this._noTransition( hideReveal ); |
|
} else { |
|
hideReveal(); |
|
} |
|
|
|
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; |
|
this._isInstant = isInstant; |
|
return isInstant; |
|
}; |
|
|
|
// listen for layoutComplete, hideComplete and revealComplete |
|
// to trigger arrangeComplete |
|
Isotope.prototype._bindArrangeComplete = function() { |
|
// listen for 3 events to trigger arrangeComplete |
|
var isLayoutComplete, isHideComplete, isRevealComplete; |
|
var _this = this; |
|
function arrangeParallelCallback() { |
|
if ( isLayoutComplete && isHideComplete && isRevealComplete ) { |
|
_this.emitEvent( 'arrangeComplete', [ _this.filteredItems ] ); |
|
} |
|
} |
|
this.once( 'layoutComplete', function() { |
|
isLayoutComplete = true; |
|
arrangeParallelCallback(); |
|
}); |
|
this.once( 'hideComplete', function() { |
|
isHideComplete = true; |
|
arrangeParallelCallback(); |
|
}); |
|
this.once( 'revealComplete', function() { |
|
isRevealComplete = true; |
|
arrangeParallelCallback(); |
|
}); |
|
}; |
|
|
|
// -------------------------- 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 ); |
|
} |
|
} |
|
|
|
// 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 |
|
Isotope.prototype._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 |
|
*/ |
|
Isotope.prototype.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 ); |
|
}; |
|
|
|
Isotope.prototype._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 |
|
*/ |
|
Isotope.prototype._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 ) { |
|
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 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 |
|
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 ) { |
|
this._mode()._manageStamp( stamp ); |
|
}; |
|
|
|
Isotope.prototype._getContainerSize = function() { |
|
return this._mode()._getContainerSize(); |
|
}; |
|
|
|
Isotope.prototype.needsResizeLayout = function() { |
|
return this._mode().needsResizeLayout(); |
|
}; |
|
|
|
// -------------------------- adding & removing -------------------------- // |
|
|
|
// HEADS UP overwrites default Outlayer appended |
|
Isotope.prototype.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 |
|
Isotope.prototype.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 ); |
|
}; |
|
|
|
Isotope.prototype._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 |
|
*/ |
|
Isotope.prototype.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 = Isotope.prototype.remove; |
|
Isotope.prototype.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; |
|
if ( !len ) { |
|
return; |
|
} |
|
// remove elems from filteredItems |
|
for ( var i=0; i < len; i++ ) { |
|
var item = removeItems[i]; |
|
// remove item from collection |
|
utils.removeFrom( this.filteredItems, item ); |
|
} |
|
}; |
|
|
|
Isotope.prototype.shuffle = function() { |
|
// update random sortData |
|
for ( var i=0, len = this.items.length; i < len; 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 |
|
* @returns ret |
|
* @private |
|
*/ |
|
Isotope.prototype._noTransition = function( fn ) { |
|
// save transitionDuration before disabling |
|
var transitionDuration = this.options.transitionDuration; |
|
// disable transition |
|
this.options.transitionDuration = 0; |
|
// do it |
|
var returnValue = fn.call( this ); |
|
// 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 |
|
*/ |
|
Isotope.prototype.getFilteredItemElements = function() { |
|
var elems = []; |
|
for ( var i=0, len = this.filteredItems.length; i < len; i++ ) { |
|
elems.push( this.filteredItems[i].element ); |
|
} |
|
return elems; |
|
}; |
|
|
|
// ----- ----- // |
|
|
|
return Isotope; |
|
|
|
}));
|
|
|