Skip to content

Instantly share code, notes, and snippets.

Created June 15, 2015 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/4afab5fbcc138510e4ca to your computer and use it in GitHub Desktop.
Save anonymous/4afab5fbcc138510e4ca to your computer and use it in GitHub Desktop.
wookmark.js
/*!
jQuery Wookmark plugin
@name wookmark.js
@author Christoph Ono (chri@sto.ph or @gbks)
@author Sebastian Helzle (sebastian@helzle.net or @sebobo)
@version 2.0.0
@date 02/15/2015
@category jQuery plugin
@copyright (c) 2009-2015 Christoph Ono (www.wookmark.com)
@license Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*/
/*global define, window, jQuery*/
/*jslint plusplus: true, bitwise: true */
(function (factory) {
if (typeof define === 'function' && define.amd) {
define(['window', 'document'], factory);
} else {
factory(window, document);
}
}(function (window, document) {
// Wookmark default options
// ------------------------
var defaultOptions = {
align: 'center',
autoResize: true,
comparator: null,
direction: undefined,
ignoreInactiveItems: true,
inactiveClass: 'wookmark-inactive',
itemSelector: undefined,
itemWidth: 0,
fillEmptySpace: false,
flexibleWidth: 0,
offset: 5,
outerOffset: 0,
onLayoutChanged: undefined,
placeholderClass: 'wookmark-placeholder',
possibleFilters: [],
resizeDelay: 50,
verticalOffset: undefined
};
// Helper functions
// ----------------
// Bind function to set the context for the Wookmark instance function
function __bind(fn, me) {
return function () {
return fn.apply(me, arguments);
};
}
// Function for executing css writes to dom on the next animation frame if supported
var executeNextFrame = window.requestAnimationFrame || function (callback) { callback(); };
// Update multiple css values on an object
function setCSS(el, properties) {
var key;
for (key in properties) {
if (properties.hasOwnProperty(key)) {
el.style[key] = properties[key];
}
}
}
// Update the css properties of multiple elements at the same time
// befor the browsers next animation frame.
// The parameter `data` has to be an array containing objects, each
// with the element and the desired css properties.
function bulkUpdateCSS(data) {
executeNextFrame(function () {
var i, item;
for (i = 0; i < data.length; i++) {
item = data[i];
setCSS(item.el, item.css);
}
});
}
// Remove whitespace around filter names
function cleanFilterName(filterName) {
return filterName.replace(/^\s+|\s+$/g, '').toLowerCase();
}
// Remove listener from an element (IE8 compatible)
function removeEventListener(el, eventName, handler) {
if (el.removeEventListener) {
el.removeEventListener(eventName, handler);
} else {
el.detachEvent('on' + eventName, handler);
}
}
// Add listener to an element (IE8 compatible)
function addEventListener(el, eventName, handler) {
removeEventListener(el, eventName, handler);
if (el.addEventListener) {
el.addEventListener(eventName, handler);
} else {
el.attachEvent('on' + eventName, function () {
handler.call(el);
});
}
}
// Checks if element `el` is not visible in the browser
function isHidden(el) {
return el.offsetParent === null;
}
// Returns the elements height without margin
function getHeight(el) {
return el.offsetHeight;
}
// Returns the elements width without margin
function getWidth(el) {
return el.offsetWidth;
}
// Return true if element has class
function hasClass(el, className) {
if (el.classList) {
return el.classList.contains(className);
}
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
}
// Add class to element (IE8+)
function addClass(el, className) {
if (el.classList) {
el.classList.add(className);
} else {
el.className += ' ' + className;
}
}
// Remove class from element (IE8+)
function removeClass(el, className) {
if (el.classList) {
el.classList.remove(className);
} else {
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
// Get value of specified data attribute
function getData(el, attr, isInt, prefix) {
if (prefix === undefined) {
prefix = 'wookmark-';
}
var val = el.getAttribute('data-' + prefix + attr);
if (isInt === true) {
return parseInt(val, 10);
}
return val;
}
// Set value of specified data attribute
function setData(el, attr, val, prefix) {
if (prefix === undefined) {
prefix = 'wookmark-';
}
el.setAttribute('data-' + prefix + attr, val);
}
// Remove duplicates from given array
function removeDuplicates(items) {
var temp = {}, result = [], x, i = items.length;
while (i--) {
x = getData(items[i], 'id', true);
if (!temp.hasOwnProperty(x)) {
temp[x] = 1;
result.push(items[i]);
}
}
return result;
}
// Get the computed style from an element (IE 8 compatible)
function getStyle(el, prop) {
return window.getComputedStyle !== undefined ? window.getComputedStyle(el, null).getPropertyValue(prop) : el.currentStyle[prop];
}
// IE 8 compatible indexOf
function indexOf(items, item) {
var len = items.length, i;
for (i = 0; i < len; i++) {
if (items[i] === item) {
return i;
}
}
return -1;
}
// Main wookmark plugin class
// --------------------------
function Wookmark(container, options) {
options = options || {};
if (typeof container === 'string') {
container = document.querySelector(container);
}
// Instance variables.
this.container = container;
this.columns = this.containerWidth = this.resizeTimer = null;
this.activeItemCount = 0;
this.placeholders = [];
this.itemHeightsInitialized = false;
this.itemHeightsDirty = false;
this.elementTag = 'div';
// Bind instance methods
this.initItems = __bind(this.initItems, this);
this.updateOptions = __bind(this.updateOptions, 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);
// Initialize children of the container
this.initItems();
// Initial update and layout
this.container.style.display = 'block';
this.updateOptions(options);
// Collect filter classes after items have been initialized
this.updateFilterClasses();
// Listen to resize event of the browser if enabled
if (this.autoResize) {
addEventListener(window, 'resize', this.onResize);
}
// Listen to external refresh event
addEventListener(this.container, 'refreshWookmark', this.onRefresh);
}
// Get all valid children of the container object and store them
Wookmark.prototype.initItems = function () {
// By select all children of the container if no selector is specified
if (this.itemSelector === undefined) {
var items = [], child, children = this.container.children,
i = children.length;
while (i--) {
child = children[i];
// Skip comment nodes on IE8
if (child.nodeType !== 8) {
// Show item
child.style.display = '';
setData(child, 'id', i);
items.unshift(child);
}
}
this.items = items;
} else {
this.items = this.container.querySelectorAll(this.itemSelector);
}
if (this.items.length) {
this.elementTag = this.items[0].tagName;
}
this.itemHeightsDirty = true;
};
// Reload all filter classes from all items and cache them
Wookmark.prototype.updateFilterClasses = function () {
// Collect filter data
var i = this.items.length, j, filterClasses = {}, itemFilterClasses,
item, filterClass, possibleFilters = this.possibleFilters,
k = possibleFilters.length, possibleFilter;
while (i--) {
item = this.items[i];
// Read filter classes and globally store each filter class as object and the fitting items in the array
itemFilterClasses = JSON.parse(getData(item, 'filter-class', false, ''));
if (itemFilterClasses && typeof itemFilterClasses === 'object') {
j = itemFilterClasses.length;
while (j--) {
filterClass = cleanFilterName(itemFilterClasses[j]);
if (!filterClasses.hasOwnProperty(filterClass)) {
filterClasses[filterClass] = [];
}
filterClasses[filterClass].push(item);
}
}
}
while (k--) {
possibleFilter = cleanFilterName(possibleFilters[k]);
if (!filterClasses.hasOwnProperty(possibleFilter)) {
filterClasses[possibleFilter] = [];
}
}
this.filterClasses = filterClasses;
};
// Method for updating the plugins options
Wookmark.prototype.updateOptions = function (options) {
var key;
this.itemHeightsDirty = true;
options = options || {};
// Overwrite non existing instance variables with the ones from options or the defaults
for (key in defaultOptions) {
if (defaultOptions.hasOwnProperty(key)) {
if (options.hasOwnProperty(key)) {
this[key] = options[key];
} else if (!this.hasOwnProperty(key)) {
this[key] = defaultOptions[key];
}
}
}
// Vertical offset uses a fallback to offset
this.verticalOffset = this.verticalOffset || this.offset;
// Update layout so changes apply
this.layout(true);
};
// 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 (this.filterClasses.hasOwnProperty(filter)) {
activeFilters.push(this.filterClasses[filter]);
}
}
// Get items for active filters with the selected mode
i = activeFiltersLength = activeFilters.length;
if (mode === 'or' || activeFiltersLength === 1) {
// Set all items in all active filters active
while (i--) {
activeItems = activeItems.concat(activeFilters[i]);
}
} else if (mode === 'and') {
var shortestFilter = activeFilters[0], itemValid = true,
foundInFilter, currentItem, currentFilter;
// Find shortest filter class
while (i--) {
if (activeFilters[i].length < shortestFilter.length) {
shortestFilter = activeFilters[i];
}
}
// Iterate over shortest filter and find elements in other filter classes
shortestFilter = shortestFilter || [];
i = shortestFilter.length;
while (i--) {
currentItem = shortestFilter[i];
j = activeFiltersLength;
itemValid = true;
while (j-- && itemValid) {
currentFilter = activeFilters[j];
if (shortestFilter !== currentFilter) {
// Search for current item in each active filter class
foundInFilter = false;
k = currentFilter.length;
while (k-- && !foundInFilter) {
foundInFilter = currentFilter[k] === currentItem;
}
itemValid &= foundInFilter;
}
}
if (itemValid) {
activeItems = activeItems.concat(shortestFilter[i]);
}
}
}
// Remove duplicates from active items
if (activeFiltersLength > 1) {
activeItems = removeDuplicates(activeItems);
}
// Hide inactive items
if (!dryRun) {
i = this.items.length;
while (i--) {
if (indexOf(activeItems, this.items[i]) === -1) {
addClass(this.items[i], this.inactiveClass);
}
}
}
} else {
// Show all items if no filter is selected
activeItems = this.items;
}
// Show active items
if (!dryRun) {
i = activeItems.length;
while (i--) {
removeClass(activeItems[i], this.inactiveClass);
}
// 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,
containerHeight = getHeight(this.container),
columnsLength = this.columns.length,
column,
height,
innerOffset,
lastColumnItem,
placeholdersHtml = '',
placeholder,
top;
// Add more placeholders if necessary
if (this.placeholders.length < columnsLength) {
for (i = 0; i < columnsLength - this.placeholders.length; i++) {
placeholdersHtml += '<' + this.elementTag + ' class="' + this.placeholderClass + '"/>';
}
this.container.insertAdjacentHTML('beforeend', placeholdersHtml);
this.placeholders = this.container.querySelectorAll('.' + this.placeholderClass);
}
innerOffset = (this.offset + parseInt(getStyle(this.placeholders[0], 'border-left-width'), 10) * 2) || 0;
innerOffset += (parseInt(getStyle(this.placeholders[0], 'padding-left'), 10) * 2) || 0;
// Update each placeholder
for (i = 0; i < this.placeholders.length; i++) {
placeholder = this.placeholders[i];
column = this.columns[i];
if (i >= columnsLength || column.length === 0) {
placeholder.style.display = 'none';
} else {
lastColumnItem = column[column.length - 1];
top = getData(lastColumnItem, 'top', true) + getData(lastColumnItem, 'height', true) + this.verticalOffset;
height = Math.max(0, containerHeight - top - innerOffset);
setCSS(placeholder, {
position: 'absolute',
display: height > 0 ? 'block' : 'none',
left: (i * columnWidth + sideOffset) + 'px',
top: top + 'px',
width: (columnWidth - innerOffset) + 'px',
height: height + 'px'
});
}
}
};
// Method the get active items which are not disabled and visible
Wookmark.prototype.getActiveItems = function () {
var inactiveClass = this.inactiveClass,
i,
result = [],
item,
items = this.items;
if (this.ignoreInactiveItems) {
for (i = 0; i < items.length; i++) {
item = items[i];
if (!hasClass(item, inactiveClass)) {
result.push(item);
}
}
}
return result;
};
// Method to get the standard item width
Wookmark.prototype.getItemWidth = function () {
var itemWidth = this.itemWidth,
innerWidth = getWidth(this.container) - 2 * this.outerOffset,
flexibleWidth = this.flexibleWidth;
if (typeof itemWidth === 'function') {
itemWidth = this.itemWidth();
}
if (this.items.length > 0 && (itemWidth === undefined || (itemWidth === 0 && !this.flexibleWidth))) {
itemWidth = getWidth(this.items[0]);
} else if (typeof itemWidth === 'string' && itemWidth.indexOf('%') >= 0) {
itemWidth = parseFloat(this.itemWidth) / 100 * innerWidth;
}
// Calculate flexible item width if option is set
if (flexibleWidth) {
if (typeof flexibleWidth === 'function') {
flexibleWidth = flexibleWidth();
}
if (typeof flexibleWidth === 'string' && flexibleWidth.indexOf('%') >= 0) {
flexibleWidth = parseFloat(flexibleWidth) / 100 * innerWidth;
}
// Find highest column count
var paddedInnerWidth = (innerWidth + this.offset),
flexibleColumns = Math.floor(0.5 + paddedInnerWidth / (flexibleWidth + this.offset)),
fixedColumns = Math.floor(paddedInnerWidth / (itemWidth + this.offset)),
columns = Math.max(flexibleColumns, fixedColumns),
columnWidth = Math.min(flexibleWidth, Math.floor((innerWidth - (columns - 1) * this.offset) / columns));
itemWidth = Math.max(itemWidth, columnWidth);
}
return itemWidth;
};
// Main layout method.
Wookmark.prototype.layout = function (force, callback) {
// Do nothing if container isn't visible
if (isHidden(this.container)) { return; }
// Calculate basic layout parameters.
var calculatedItemWidth = this.getItemWidth(),
columnWidth = calculatedItemWidth + this.offset,
containerWidth = getWidth(this.container),
innerWidth = containerWidth - 2 * this.outerOffset,
columns = Math.floor((innerWidth + this.offset) / columnWidth),
offset = 0,
maxHeight = 0,
activeItems = this.getActiveItems(),
activeItemsLength = activeItems.length,
item,
i = activeItemsLength;
// Cache item heights
if (this.itemHeightsDirty || !this.itemHeightsInitialized) {
while (i--) {
item = activeItems[i];
if (this.flexibleWidth) {
// Stretch items to fill calculated width
item.style.width = calculatedItemWidth + 'px';
}
setData(item, 'height', item.offsetHeight);
}
this.itemHeightsDirty = false;
this.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 += Math.floor(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.style.height = maxHeight + 'px';
// Update placeholders
if (this.fillEmptySpace) {
this.refreshPlaceholders(columnWidth, offset);
}
if (this.onLayoutChanged !== undefined && typeof this.onLayoutChanged === 'function') {
this.onLayoutChanged();
}
// Run optional callback
if (typeof callback === 'function') {
callback();
}
};
// 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, k = 0, i = 0, activeItems, activeItemCount, shortest = null, shortestIndex = null,
sideOffset, heights = [], itemBulkCSS = [], leftAligned = this.align === 'left';
this.columns = [];
// Sort elements before layouting
activeItems = this.sortElements(this.getActiveItems());
activeItemCount = activeItems.length;
// Prepare arrays to store height of columns and items.
while (heights.length < columns) {
heights.push(this.outerOffset);
this.columns.push([]);
}
// Loop over items.
while (i < activeItemCount) {
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;
}
}
setData(item, '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] = {
el: item,
css: {
position: 'absolute',
top: shortest + 'px'
}
};
itemBulkCSS[i].css[this.direction] = sideOffset + 'px';
// Update column height and store item in shortest column
heights[shortestIndex] += getData(item, 'height', true) + this.verticalOffset;
this.columns[shortestIndex].push(item);
i++;
}
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 = [], k = 0, j = 0,
i = this.columns.length, currentHeight,
column, item, sideOffset;
while (i--) {
currentHeight = this.outerOffset;
heights.push(currentHeight);
column = this.columns[i];
sideOffset = i * columnWidth + offset;
for (k = 0; k < column.length; k++, j++) {
item = column[k];
setData(item, 'top', currentHeight);
itemBulkCSS[j] = {
el: item,
css: {
top: currentHeight + 'px'
}
};
itemBulkCSS[j].css[this.direction] = sideOffset + 'px';
currentHeight += getData(item, 'height', true) + 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);
var i = this.placeholders.length;
while (i--) {
this.container.removeChild(this.placeholders[i]);
}
removeEventListener(window, 'resize', this.onResize);
removeEventListener(this.container, 'refreshWookmark', this.onRefresh);
};
// Register as jQuery plugin if jQuery is loaded
if (window.jQuery !== undefined) {
jQuery.fn.wookmark = function (options) {
var i = this.length;
// Use first element if container is an jQuery object
if (options !== undefined && options.container instanceof jQuery) {
options.container = options.container[0];
}
// Call plugin multiple times if there are multiple elements selected
if (i > 1) {
while (i--) {
this[i].wookmark(options);
}
} else if (i === 1) {
// Create a wookmark instance or update an existing one
if (!this.wookmarkInstance) {
this.wookmarkInstance = new Wookmark(this[0], options || {});
} else {
this.wookmarkInstance.updateOptions(options || {});
}
}
return this;
};
}
window.Wookmark = Wookmark;
return Wookmark;
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment