/*! jquery wookmark plugin @name jquery.wookmark.js @author christoph ono (chri@sto.ph or @gbks) @author sebastian helzle (sebastian@helzle.net or @sebobo) @version 1.4.8 @date 07/08/2013 @category jquery plugin @copyright (c) 2009-2014 christoph ono (www.wookmark.com) @license licensed under the mit (http://www.opensource.org/licenses/mit-license.php) license. */ (function (factory) { if (typeof define === 'function' && define.amd) define(['jquery'], factory); else factory(jquery); }(function ($) { var wookmark, defaultoptions, __bind; __bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; // wookmark default options defaultoptions = { align: 'center', autoresize: false, comparator: null, container: $('body'), direction: undefined, ignoreinactiveitems: true, itemwidth: 0, fillemptyspace: false, flexiblewidth: 0, offset: 2, outeroffset: 0, onlayoutchanged: undefined, possiblefilters: [], resizedelay: 50, verticaloffset: undefined }; // function for executing css writes to dom on the next animation frame if supported var executenextframe = window.requestanimationframe || function(callback) {callback();}, $window = $(window); function bulkupdatecss(data) { executenextframe(function() { var i, item; for (i = 0; i < data.length; i++) { item = data[i]; item.obj.css(item.css); } }); } function cleanfiltername(filtername) { return $.trim(filtername).tolowercase(); } // main wookmark plugin class wookmark = (function() { function wookmark(handler, options) { // instance variables. this.handler = handler; this.columns = this.containerwidth = this.resizetimer = null; this.activeitemcount = 0; this.itemheightsdirty = true; this.placeholders = []; $.extend(true, this, defaultoptions, options); this.verticaloffset = this.verticaloffset || this.offset; // bind instance methods this.update = __bind(this.update, this); this.onresize = __bind(this.onresize, this); this.onrefresh = __bind(this.onrefresh, this); this.getitemwidth = __bind(this.getitemwidth, this); this.layout = __bind(this.layout, this); this.layoutfull = __bind(this.layoutfull, this); this.layoutcolumns = __bind(this.layoutcolumns, this); this.filter = __bind(this.filter, this); this.clear = __bind(this.clear, this); this.getactiveitems = __bind(this.getactiveitems, this); this.refreshplaceholders = __bind(this.refreshplaceholders, this); this.sortelements = __bind(this.sortelements, this); this.updatefilterclasses = __bind(this.updatefilterclasses, this); // initial update of the filter classes this.updatefilterclasses(); // listen to resize event if requested. if (this.autoresize) $window.bind('resize.wookmark', this.onresize); this.container.bind('refreshwookmark', this.onrefresh); } wookmark.prototype.updatefilterclasses = function() { // collect filter data var i = 0, j = 0, k = 0, filterclasses = {}, itemfilterclasses, $item, filterclass, possiblefilters = this.possiblefilters, possiblefilter; for (; i < this.handler.length; i++) { $item = this.handler.eq(i); // read filter classes and globally store each filter class as object and the fitting items in the array itemfilterclasses = $item.data('filterclass'); if (typeof itemfilterclasses == 'object' && itemfilterclasses.length > 0) { for (j = 0; j < itemfilterclasses.length; j++) { filterclass = cleanfiltername(itemfilterclasses[j]); if (typeof(filterclasses[filterclass]) === 'undefined') { filterclasses[filterclass] = []; } filterclasses[filterclass].push($item[0]); } } } for (; k < possiblefilters.length; k++) { possiblefilter = cleanfiltername(possiblefilters[k]); if (!(possiblefilter in filterclasses)) { filterclasses[possiblefilter] = []; } } this.filterclasses = filterclasses; }; // method for updating the plugins options wookmark.prototype.update = function(options) { this.itemheightsdirty = true; $.extend(true, this, options); }; // this timer ensures that layout is not continuously called as window is being dragged. wookmark.prototype.onresize = function() { cleartimeout(this.resizetimer); this.itemheightsdirty = this.flexiblewidth !== 0; this.resizetimer = settimeout(this.layout, this.resizedelay); }; // marks the items heights as dirty and does a relayout wookmark.prototype.onrefresh = function() { this.itemheightsdirty = true; this.layout(); }; /** * filters the active items with the given string filters. * @param filters array of string * @param mode 'or' or 'and' */ wookmark.prototype.filter = function(filters, mode, dryrun) { var activefilters = [], activefilterslength, activeitems = $(), i, j, k, filter; filters = filters || []; mode = mode || 'or'; dryrun = dryrun || false; if (filters.length) { // collect active filters for (i = 0; i < filters.length; i++) { filter = cleanfiltername(filters[i]); if (filter in this.filterclasses) { activefilters.push(this.filterclasses[filter]); } } // get items for active filters with the selected mode activefilterslength = activefilters.length; if (mode == 'or' || activefilterslength == 1) { // set all items in all active filters active for (i = 0; i < activefilterslength; i++) { activeitems = activeitems.add(activefilters[i]); } } else if (mode == 'and') { var shortestfilter = activefilters[0], itemvalid = true, foundinfilter, currentitem, currentfilter; // find shortest filter class for (i = 1; i < activefilterslength; i++) { if (activefilters[i].length < shortestfilter.length) { shortestfilter = activefilters[i]; } } // iterate over shortest filter and find elements in other filter classes shortestfilter = shortestfilter || []; for (i = 0; i < shortestfilter.length; i++) { currentitem = shortestfilter[i]; itemvalid = true; for (j = 0; j < activefilters.length && itemvalid; j++) { currentfilter = activefilters[j]; if (shortestfilter == currentfilter) continue; // search for current item in each active filter class for (k = 0, foundinfilter = false; k < currentfilter.length && !foundinfilter; k++) { foundinfilter = currentfilter[k] == currentitem; } itemvalid &= foundinfilter; } if (itemvalid) activeitems.push(shortestfilter[i]); } } // hide inactive items if (!dryrun) this.handler.not(activeitems).addclass('inactive'); } else { // show all items if no filter is selected activeitems = this.handler; } // show active items if (!dryrun) { activeitems.removeclass('inactive'); // unset columns and refresh grid for a full layout this.columns = null; this.layout(); } return activeitems; }; /** * creates or updates existing placeholders to create columns of even height */ wookmark.prototype.refreshplaceholders = function(columnwidth, sideoffset) { var i = this.placeholders.length, $placeholder, $lastcolumnitem, columnslength = this.columns.length, column, height, top, inneroffset, containerheight = this.container.innerheight(); for (; i < columnslength; i++) { $placeholder = $('
').appendto(this.container); this.placeholders.push($placeholder); } inneroffset = this.offset + parseint(this.placeholders[0].css('borderleftwidth'), 10) * 2; for (i = 0; i < this.placeholders.length; i++) { $placeholder = this.placeholders[i]; column = this.columns[i]; if (i >= columnslength || !column[column.length - 1]) { $placeholder.css('display', 'none'); } else { $lastcolumnitem = column[column.length - 1]; if (!$lastcolumnitem) continue; top = $lastcolumnitem.data('wookmark-top') + $lastcolumnitem.data('wookmark-height') + this.verticaloffset; height = containerheight - top - inneroffset; $placeholder.css({ position: 'absolute', display: height > 0 ? 'block' : 'none', left: i * columnwidth + sideoffset, top: top, width: columnwidth - inneroffset, height: height }); } } }; // method the get active items which are not disabled and visible wookmark.prototype.getactiveitems = function() { return this.ignoreinactiveitems ? this.handler.not('.inactive') : this.handler; }; // method to get the standard item width wookmark.prototype.getitemwidth = function() { var itemwidth = this.itemwidth, innerwidth = this.container.width() - 2 * this.outeroffset, firstelement = this.handler.eq(0), flexiblewidth = this.flexiblewidth; if (this.itemwidth === undefined || this.itemwidth === 0 && !this.flexiblewidth) { itemwidth = firstelement.outerwidth(); } else if (typeof this.itemwidth == 'string' && this.itemwidth.indexof('%') >= 0) { itemwidth = parsefloat(this.itemwidth) / 100 * innerwidth; } // calculate flexible item width if option is set if (flexiblewidth) { if (typeof flexiblewidth == 'string' && flexiblewidth.indexof('%') >= 0) { flexiblewidth = parsefloat(flexiblewidth) / 100 * innerwidth; } // find highest column count var paddedinnerwidth = (innerwidth + this.offset), flexiblecolumns = ~~(0.5 + paddedinnerwidth / (flexiblewidth + this.offset)), fixedcolumns = ~~(paddedinnerwidth / (itemwidth + this.offset)), columns = math.max(flexiblecolumns, fixedcolumns), columnwidth = math.min(flexiblewidth, ~~((innerwidth - (columns - 1) * this.offset) / columns)); itemwidth = math.max(itemwidth, columnwidth); // stretch items to fill calculated width this.handler.css('width', itemwidth); } return itemwidth; }; // main layout method. wookmark.prototype.layout = function(force) { // do nothing if container isn't visible if (!this.container.is(':visible')) return; // calculate basic layout parameters. var columnwidth = this.getitemwidth() + this.offset, containerwidth = this.container.width(), innerwidth = containerwidth - 2 * this.outeroffset, columns = ~~((innerwidth + this.offset) / columnwidth), offset = 0, maxheight = 0, i = 0, activeitems = this.getactiveitems(), activeitemslength = activeitems.length, $item; // cache item height if (this.itemheightsdirty || !this.container.data('itemheightsinitialized')) { for (; i < activeitemslength; i++) { $item = activeitems.eq(i); $item.data('wookmark-height', $item.outerheight()); } this.itemheightsdirty = false; this.container.data('itemheightsinitialized', true); } // use less columns if there are to few items columns = math.max(1, math.min(columns, activeitemslength)); // calculate the offset based on the alignment of columns to the parent container offset = this.outeroffset; if (this.align == 'center') { offset += ~~(0.5 + (innerwidth - (columns * columnwidth - this.offset)) >> 1); } // get direction for positioning this.direction = this.direction || (this.align == 'right' ? 'right' : 'left'); // if container and column count hasn't changed, we can only update the columns. if (!force && this.columns !== null && this.columns.length == columns && this.activeitemcount == activeitemslength) { maxheight = this.layoutcolumns(columnwidth, offset); } else { maxheight = this.layoutfull(columnwidth, columns, offset); } this.activeitemcount = activeitemslength; // set container height to height of the grid. this.container.css('height', maxheight); // update placeholders if (this.fillemptyspace) { this.refreshplaceholders(columnwidth, offset); } if (this.onlayoutchanged !== undefined && typeof this.onlayoutchanged === 'function') { this.onlayoutchanged(); } }; /** * sort elements with configurable comparator */ wookmark.prototype.sortelements = function(elements) { return typeof(this.comparator) === 'function' ? elements.sort(this.comparator) : elements; }; /** * perform a full layout update. */ wookmark.prototype.layoutfull = function(columnwidth, columns, offset) { var $item, i = 0, k = 0, activeitems = $.makearray(this.getactiveitems()), length = activeitems.length, shortest = null, shortestindex = null, sideoffset, heights = [], itembulkcss = [], leftaligned = this.align == 'left' ? true : false; this.columns = []; // sort elements before layouting activeitems = this.sortelements(activeitems); // prepare arrays to store height of columns and items. while (heights.length < columns) { heights.push(this.outeroffset); this.columns.push([]); } // loop over items. for (; i < length; i++ ) { $item = $(activeitems[i]); // find the shortest column. shortest = heights[0]; shortestindex = 0; for (k = 0; k < columns; k++) { if (heights[k] < shortest) { shortest = heights[k]; shortestindex = k; } } $item.data('wookmark-top', shortest); // stick to left side if alignment is left and this is the first column sideoffset = offset; if (shortestindex > 0 || !leftaligned) sideoffset += shortestindex * columnwidth; // position the item. (itembulkcss[i] = { obj: $item, css: { position: 'absolute', top: shortest } }).css[this.direction] = sideoffset; // update column height and store item in shortest column heights[shortestindex] += $item.data('wookmark-height') + this.verticaloffset; this.columns[shortestindex].push($item); } bulkupdatecss(itembulkcss); // return longest column return math.max.apply(math, heights); }; /** * this layout method only updates the vertical position of the * existing column assignments. */ wookmark.prototype.layoutcolumns = function(columnwidth, offset) { var heights = [], itembulkcss = [], i = 0, k = 0, j = 0, currentheight, column, $item, itemdata, sideoffset; for (; i < this.columns.length; i++) { heights.push(this.outeroffset); column = this.columns[i]; sideoffset = i * columnwidth + offset; currentheight = heights[i]; for (k = 0; k < column.length; k++, j++) { $item = column[k].data('wookmark-top', currentheight); (itembulkcss[j] = { obj: $item, css: { top: currentheight } }).css[this.direction] = sideoffset; currentheight += $item.data('wookmark-height') + this.verticaloffset; } heights[i] = currentheight; } bulkupdatecss(itembulkcss); // return longest column return math.max.apply(math, heights); }; /** * clear event listeners and time outs and the instance itself */ wookmark.prototype.clear = function() { cleartimeout(this.resizetimer); $window.unbind('resize.wookmark', this.onresize); this.container.unbind('refreshwookmark', this.onrefresh); this.handler.wookmarkinstance = null; }; return wookmark; })(); $.fn.wookmark = function(options) { // create a wookmark instance if not available if (!this.wookmarkinstance) { this.wookmarkinstance = new wookmark(this, options || {}); } else { this.wookmarkinstance.update(options || {}); } // apply layout this.wookmarkinstance.layout(true); // display items (if hidden) and return jquery object to maintain chainability return this.show(); }; }));