-
-
Save anonymous/4afab5fbcc138510e4ca to your computer and use it in GitHub Desktop.
wookmark.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
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