/*! * Colcade v0.2.0 * Lightweight masonry layout * by David DeSandro * MIT license */ /*jshint browser: true, undef: true, unused: true */ ( function( window, factory ) { // universal module definition /*jshint strict: false */ /*global define: false, module: false */ if ( typeof define == 'function' && define.amd ) { // AMD define( factory ); } else if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global window.Colcade = factory(); } }( window, function factory() { // -------------------------- Colcade -------------------------- // function Colcade( element, options ) { element = getQueryElement( element ); // do not initialize twice on same element if ( element && element.colcadeGUID ) { var instance = instances[ element.colcadeGUID ]; instance.option( options ); return instance; } this.element = element; // options this.options = {}; this.option( options ); // kick things off this.create(); } var proto = Colcade.prototype; proto.option = function( options ) { this.options = extend( this.options, options ); }; // globally unique identifiers var GUID = 0; // internal store of all Colcade intances var instances = {}; proto.create = function() { this.errorCheck(); // add guid for Colcade.data var guid = this.guid = ++GUID; this.element.colcadeGUID = guid; instances[ guid ] = this; // associate via id // update initial properties & layout this.reload(); // events this._windowResizeHandler = this.onWindowResize.bind( this ); this._loadHandler = this.onLoad.bind( this ); window.addEventListener( 'resize', this._windowResizeHandler ); this.element.addEventListener( 'load', this._loadHandler, true ); }; proto.errorCheck = function() { var errors = []; if ( !this.element ) { errors.push( 'Bad element: ' + this.element ); } if ( !this.options.columns ) { errors.push( 'columns option required: ' + this.options.columns ); } if ( !this.options.items ) { errors.push( 'items option required: ' + this.options.items ); } if ( errors.length ) { throw new Error( '[Colcade error] ' + errors.join('. ') ); } }; // update properties and do layout proto.reload = function() { this.updateColumns(); this.updateItems(); this.layout(); }; proto.updateColumns = function() { this.columns = querySelect( this.options.columns, this.element ); }; proto.updateItems = function() { this.items = querySelect( this.options.items, this.element ); }; proto.getActiveColumns = function() { return this.columns.filter( function( column ) { var style = getComputedStyle( column ); return style.display != 'none'; }); }; // ----- layout ----- // // public, updates activeColumns proto.layout = function() { this.activeColumns = this.getActiveColumns(); this._layout(); }; // private, does not update activeColumns proto._layout = function() { // reset column heights this.columnHeights = this.activeColumns.map( function() { return 0; }); // layout all items this.layoutItems( this.items ); }; proto.layoutItems = function( items ) { items.forEach( this.layoutItem, this ); }; proto.layoutItem = function( item ) { // layout item by appending to column var minHeight = Math.min.apply( Math, this.columnHeights ); var index = this.columnHeights.indexOf( minHeight ); this.activeColumns[ index ].appendChild( item ); // at least 1px, if item hasn't loaded // Not exactly accurate, but it's cool this.columnHeights[ index ] += item.offsetHeight || 1; }; // ----- adding items ----- // proto.append = function( elems ) { var items = this.getQueryItems( elems ); // add items to collection this.items = this.items.concat( items ); // lay them out this.layoutItems( items ); }; proto.prepend = function( elems ) { var items = this.getQueryItems( elems ); // add items to collection this.items = items.concat( this.items ); // lay out everything this._layout(); }; proto.getQueryItems = function( elems ) { elems = makeArray( elems ); var fragment = document.createDocumentFragment(); elems.forEach( function( elem ) { fragment.appendChild( elem ); }); return querySelect( this.options.items, fragment ); }; // ----- measure column height ----- // proto.measureColumnHeight = function( elem ) { var boundingRect = this.element.getBoundingClientRect(); this.activeColumns.forEach( function( column, i ) { // if elem, measure only that column // if no elem, measure all columns if ( !elem || column.contains( elem ) ) { var lastChildRect = column.lastElementChild.getBoundingClientRect(); // not an exact calculation as it includes top border, and excludes item bottom margin this.columnHeights[ i ] = lastChildRect.bottom - boundingRect.top; } }, this ); }; // ----- events ----- // proto.onWindowResize = function() { clearTimeout( this.resizeTimeout ); this.resizeTimeout = setTimeout( function() { this.onDebouncedResize(); }.bind( this ), 100 ); }; proto.onDebouncedResize = function() { var activeColumns = this.getActiveColumns(); // check if columns changed var isSameLength = activeColumns.length == this.activeColumns.length; var isSameColumns = true; this.activeColumns.forEach( function( column, i ) { isSameColumns = isSameColumns && column == activeColumns[i]; }); if ( isSameLength && isSameColumns ) { return; } // activeColumns changed this.activeColumns = activeColumns; this._layout(); }; proto.onLoad = function( event ) { this.measureColumnHeight( event.target ); }; // ----- destroy ----- // proto.destroy = function() { // move items back to container this.items.forEach( function( item ) { this.element.appendChild( item ); }, this ); // remove events window.removeEventListener( 'resize', this._windowResizeHandler ); this.element.removeEventListener( 'load', this._loadHandler, true ); // remove data delete this.element.colcadeGUID; delete instances[ this.guid ]; }; // -------------------------- HTML init -------------------------- // docReady( function() { var dataElems = querySelect('[data-colcade]'); dataElems.forEach( htmlInit ); }); function htmlInit( elem ) { // convert attribute "foo: bar, qux: baz" into object var attr = elem.getAttribute('data-colcade'); var attrParts = attr.split(','); var options = {}; attrParts.forEach( function( part ) { var pair = part.split(':'); var key = pair[0].trim(); var value = pair[1].trim(); options[ key ] = value; }); new Colcade( elem, options ); } Colcade.data = function( elem ) { elem = getQueryElement( elem ); var id = elem && elem.colcadeGUID; return id && instances[ id ]; }; // -------------------------- jQuery -------------------------- // Colcade.makeJQueryPlugin = function( $ ) { $ = $ || window.jQuery; if ( !$ ) { return; } $.fn.colcade = function( arg0 /*, arg1 */) { // method call $().colcade( 'method', { options } ) if ( typeof arg0 == 'string' ) { // shift arguments by 1 var args = Array.prototype.slice.call( arguments, 1 ); return methodCall( this, arg0, args ); } // just $().colcade({ options }) plainCall( this, arg0 ); return this; }; function methodCall( $elems, methodName, args ) { var returnValue; $elems.each( function( i, elem ) { // get instance var colcade = $.data( elem, 'colcade' ); if ( !colcade ) { return; } // apply method, get return value var value = colcade[ methodName ].apply( colcade, args ); // set return value if value is returned, use only first value returnValue = returnValue === undefined ? value : returnValue; }); return returnValue !== undefined ? returnValue : $elems; } function plainCall( $elems, options ) { $elems.each( function( i, elem ) { var colcade = $.data( elem, 'colcade' ); if ( colcade ) { // set options & init colcade.option( options ); colcade.layout(); } else { // initialize new instance colcade = new Colcade( elem, options ); $.data( elem, 'colcade', colcade ); } }); } }; // try making plugin Colcade.makeJQueryPlugin(); // -------------------------- utils -------------------------- // function extend( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; } // turn element or nodeList into an array function makeArray( obj ) { var ary = []; if ( Array.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; i < obj.length; i++ ) { ary.push( obj[i] ); } } else { // array of single index ary.push( obj ); } return ary; } // get array of elements function querySelect( selector, elem ) { elem = elem || document; var elems = elem.querySelectorAll( selector ); return makeArray( elems ); } function getQueryElement( elem ) { if ( typeof elem == 'string' ) { elem = document.querySelector( elem ); } return elem; } function docReady( onReady ) { if ( document.readyState == 'complete' ) { onReady(); return; } document.addEventListener( 'DOMContentLoaded', onReady ); } // -------------------------- end -------------------------- // return Colcade; }));