/*! * 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; });