/** * Mosaic Flow * * Pinterest like responsive image grid that doesnt suck * * @requires jQuery * @author Artem Sapegin * @copyright 2012 Artem Sapegin, http://sapegin.me * @license MIT */ /* taken at commit https://github.com/sapegin/jquery.mosaicflow/commit/20cea9ae73bab6bfeda54434564aaf0f86bed9ac * which includes fn.load() /* /*jshint browser:true, jquery:true, white:false, smarttabs:true */ /*global jQuery:false, define:false*/ (function(factory) { // Try to register as an anonymous AMD module if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else { factory(jQuery); } }(function($) { 'use strict'; var cnt = 0; $.fn.mosaicflow = function(options) { var args = Array.prototype.slice.call(arguments, 0); return this.each(function() { var elm = $(this); var data = elm.data('mosaicflow'); if (!data) { options = $.extend({}, $.fn.mosaicflow.defaults, options, dataToOptions(elm)); data = new Mosaicflow(elm, options); elm.data('mosaicflow', data); } else if (typeof options === 'string') { data[options](args[1]); } }); }; $.fn.mosaicflow.defaults = { itemSelector: '> *', columnClass: 'mosaicflow__column', minItemWidth: 240, minColumns: 2, itemHeightCalculation: 'auto', threshold: 40 }; function Mosaicflow(container, options) { this.container = container; this.options = options; this.container.trigger('mosaicflow-start'); this.init(); this.container.trigger('mosaicflow-ready'); } Mosaicflow.prototype = { init: function() { this.__uid = cnt++; this.__uidItemCounter = 0; this.items = this.container.find(this.options.itemSelector); this.columns = $([]); this.columnsHeights = []; this.itemsHeights = {}; this.tempContainer = $('
').css({'visibility': 'hidden', 'width': '100%'}); this.workOnTemp = false; this.autoCalculation = this.options.itemHeightCalculation === 'auto'; this.container.append(this.tempContainer); var that = this; this.items.each(function() { var elm = $(this); var id = elm.attr('id'); if (!id) { // Generate an unique id id = that.generateUniqueId(); elm.attr('id', id); } }); this.container.css('visibility', 'hidden'); if (this.autoCalculation) { $(window).on('load', $.proxy(this.refill, this)); } else { this.refill(); } $(window).resize($.proxy(this.refill, this)); }, refill: function() { this.container.trigger('mosaicflow-fill'); this.numberOfColumns = Math.floor(this.container.width() / this.options.minItemWidth); // Always keep min columns number if (this.numberOfColumns < this.options.minColumns) this.numberOfColumns = this.options.minColumns; var needToRefill = this.ensureColumns(); if (needToRefill) { this.fillColumns(); // Remove excess columns, only if there are visible columns remaining if (this.columns.filter(':visible').length > 0) { this.columns.filter(':hidden').remove(); } } this.container.css('visibility', 'visible'); this.container.trigger('mosaicflow-filled'); }, ensureColumns: function() { var createdCnt = this.columns.filter(':visible').length; var calculatedCnt = this.numberOfColumns; this.workingContainer = createdCnt === 0 ? this.tempContainer : this.container; if (calculatedCnt > createdCnt) { var neededCnt = calculatedCnt - createdCnt; for (var columnIdx = 0; columnIdx < neededCnt; columnIdx++) { var column = $('
', { 'class': this.options.columnClass }); this.workingContainer.append(column); } } else if (calculatedCnt < createdCnt) { var lastColumn = createdCnt; while (calculatedCnt <= lastColumn) { // We can't remove columns here becase it will remove items to. So we hide it and will remove later. this.columns.eq(lastColumn).hide(); lastColumn--; } var diff = createdCnt - calculatedCnt; this.columnsHeights.splice(this.columnsHeights.length - diff, diff); } if (calculatedCnt !== createdCnt) { this.columns = this.workingContainer.find('.' + this.options.columnClass); this.columns.css('width', (100 / calculatedCnt) + '%'); return true; } return false; }, fillColumns: function() { var columnsCnt = this.numberOfColumns; var itemsCnt = this.items.length; for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { var column = this.columns.eq(columnIdx); this.columnsHeights[columnIdx] = 0; for (var itemIdx = columnIdx; itemIdx < itemsCnt; itemIdx += columnsCnt) { var item = this.items.eq(itemIdx); var height = 0; column.append(item); if (this.autoCalculation) { // Check height after being placed in its column height = item.outerHeight(); } else { // Read img height attribute height = parseInt(item.find('img').attr('height'), 10); } this.itemsHeights[item.attr('id')] = height; this.columnsHeights[columnIdx] += height; } } this.levelBottomEdge(this.itemsHeights, this.columnsHeights); if (this.workingContainer === this.tempContainer) { this.container.append(this.tempContainer.children()); } this.container.trigger('mosaicflow-layout'); }, levelBottomEdge: function(itemsHeights, columnsHeights) { while (true) { var lowestColumn = $.inArray(Math.min.apply(null, columnsHeights), columnsHeights); var highestColumn = $.inArray(Math.max.apply(null, columnsHeights), columnsHeights); if (lowestColumn === highestColumn) return; var lastInHighestColumn = this.columns.eq(highestColumn).children().last(); var lastInHighestColumnHeight = itemsHeights[lastInHighestColumn.attr('id')]; var lowestHeight = columnsHeights[lowestColumn]; var highestHeight = columnsHeights[highestColumn]; var newLowestHeight = lowestHeight + lastInHighestColumnHeight; if (newLowestHeight >= highestHeight) return; if (highestHeight - newLowestHeight < this.options.threshold) return; this.columns.eq(lowestColumn).append(lastInHighestColumn); columnsHeights[highestColumn] -= lastInHighestColumnHeight; columnsHeights[lowestColumn] += lastInHighestColumnHeight; } }, add: function(elm) { this.container.trigger('mosaicflow-item-add', [elm]); var lowestColumn = $.inArray(Math.min.apply(null, this.columnsHeights), this.columnsHeights); var height = 0; if (this.autoCalculation) { // Get height of elm elm.css({ position: 'static', visibility: 'hidden', display: 'block' }).appendTo(this.columns.eq(lowestColumn)); height = elm.outerHeight(); var inlineImages = elm.find('img'); if (inlineImages.length !== 0) { inlineImages.each(function() { var image = $(this); var imageSizes = getImageSizes(image); var actualHeight = (image.width() * imageSizes.height) / imageSizes.width; height += actualHeight; }); } elm.detach().css({ position: 'static', visibility: 'visible' }); } else { height = parseInt(elm.find('img').attr('height'), 10); } if (!elm.attr('id')) { // Generate a unique id elm.attr('id', this.generateUniqueId()); } // Update item collection. // Item needs to be placed at the end of this.items to keep order of elements var itemsArr = this.items.toArray(); itemsArr.push(elm); this.items = $(itemsArr); this.itemsHeights[elm.attr('id')] = height; this.columnsHeights[lowestColumn] += height; this.columns.eq(lowestColumn).append(elm); this.levelBottomEdge(this.itemsHeights, this.columnsHeights); this.container.trigger('mosaicflow-layout'); this.container.trigger('mosaicflow-item-added', [elm]); }, remove: function(elm) { this.container.trigger('mosaicflow-item-remove', [elm]); var column = elm.parents('.' + this.options.columnClass); // Update column height this.columnsHeights[column.index() - 1] -= this.itemsHeights[elm.attr('id')]; elm.detach(); // Update item collection this.items = this.items.not(elm); this.levelBottomEdge(this.itemsHeights, this.columnsHeights); this.container.trigger('mosaicflow-layout'); this.container.trigger('mosaicflow-item-removed', [elm]); }, empty: function() { var columnsCnt = this.numberOfColumns; this.items = $([]); this.itemsHeights = {}; for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { var column = this.columns.eq(columnIdx); this.columnsHeights[columnIdx] = 0; column.empty(); } this.container.trigger('mosaicflow-layout'); }, recomputeHeights: function() { function computeHeight(idx, item) { item = $(item); var height = 0; if (that.autoCalculation) { // Check height after being placed in its column height = item.outerHeight(); } else { // Read img height attribute height = parseInt(item.find('img').attr('height'), 10); } that.itemsHeights[item.attr('id')] = height; that.columnsHeights[columnIdx] += height; } var that = this; var columnsCnt = this.numberOfColumns; for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { var column = this.columns.eq(columnIdx); this.columnsHeights[columnIdx] = 0; column.children().each(computeHeight); } }, generateUniqueId: function() { // Increment the counter this.__uidItemCounter++; // Return an unique ID return 'mosaic-' + this.__uid + '-itemid-' + this.__uidItemCounter; } }; // Camelize data-attributes function dataToOptions(elem) { function upper(m, l) { return l.toUpper(); } var options = {}; var data = elem.data(); for (var key in data) { options[key.replace(/-(\w)/g, upper)] = data[key]; } return options; } function getImageSizes(image) { var sizes = {}; sizes.height = parseInt(image.attr('height'), 10); sizes.width = parseInt(image.attr('width'), 10); if (sizes.height === 0 || sizes.width === 0) { var utilImage = new Image(); utilImage.src = image.attr('src'); sizes.width = utilImage.width; sizes.height = utilImage.height; } return sizes; } // Auto init $(function() { $('.mosaicflow').mosaicflow(); }); }));