/** * jQuery Nested v1.03 * * For a (total) gap free, multi column, grid layout experience. * http://suprb.com/apps/nested/ * By Andreas Pihlström and additional brain activity by Jonas Blomdin * * Licensed under the MIT license. */ // Debouncing function from John Hann // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ // Copy pasted from http://paulirish.com/2009/throttled-smartresize-jquery-event-handler/ (function ($, sr) { var debounce = function (func, threshold, execAsap) { var timeout; return function debounced() { var obj = this, args = arguments; function delayed() { if (!execAsap) func.apply(obj, args); timeout = null; }; if (timeout) clearTimeout(timeout); else if (execAsap) func.apply(obj, args); timeout = setTimeout(delayed, threshold || 150); }; }; jQuery.fn[sr] = function (fn) { return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); }; })(jQuery, 'smartresize'); // Simple count object properties if (!Object.keys) { Object.keys = function (obj) { var keys = [], k; for (k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) { keys.push(k); } } return keys; }; } // The Nested magic (function ($) { $.Nested = function (options, element) { this.element = $(element); this._init(options); }; $.Nested.settings = { selector: '.box', minWidth: 50, minColumns: 1, gutter: 1, centered: false, resizeToFit: true, // will resize block bigger than the gap resizeToFitOptions: { resizeAny: true // will resize any block to fit the gap }, animate: true, animationOptions: { speed: 20, duration: 100, queue: true, complete: function () {} } }; $.Nested.prototype = { _init: function (options) { var container = this; this.box = this.element; $(this.box).css('position', 'relative'); this.options = $.extend(true, {}, $.Nested.settings, options); this.elements = []; this._isResizing = false; this._update = true; this.maxy = new Array(); // add smartresize $(window).smartresize(function () { container.resize(); }); // build box dimensions this._setBoxes(); }, _setBoxes: function ($els, method) { var self = this; this.idCounter = 0; this.counter = 0; this.t = 0; this.maxHeight = 0; this.currWidth = 0; this.total = this.box.find(this.options.selector); this.matrix = {}; this.gridrow = new Object; var calcWidth = !this.options.centered ? this.box.innerWidth() : $(window).width(); this.columns = Math.max(this.options.minColumns, parseInt(calcWidth / (this.options.minWidth + this.options.gutter)) + 1); // build columns var minWidth = this.options.minWidth; var gutter = this.options.gutter; var display = "block"; $els = this.box.find(this.options.selector); $.each($els, function () { var dim = parseInt($(this).attr('class').replace(/^.*size([0-9]+).*$/, '$1')).toString().split(''); var x = (dim[0] == "N") ? 1 : parseFloat(dim[0]); var y = (dim[1] == "a") ? 1 : parseFloat(dim[1]); var currWidth = minWidth * x + gutter * (x - 1); var currHeight = minWidth * y + gutter * (y - 1); $(this).css({ 'display': display, 'position': 'absolute', 'width': currWidth, 'height': currHeight, 'top': $(this).position().top, 'left': $(this).position().left }).removeClass('nested-moved').attr('data-box', self.idCounter).attr('data-width', currWidth); self.idCounter++; // render grid self._renderGrid($(this), method); }); // position grid if (self.counter == self.total.length) { // if option resizeToFit is true if (self.options.resizeToFit) { self.elements = self._fillGaps(); } self._renderItems(self.elements); // reset elements self.elements = []; } }, _addMatrixRow: function (y) { if (this.matrix[y]) { return false; } else this.matrix[y] = {}; for (var c = 0; c < (this.columns - 1); c++) { var x = c * (this.options.minWidth + this.options.gutter); this.matrix[y][x] = 'false'; } }, _updateMatrix: function (el) { var height = 0; var t = parseInt(el['y']); var l = parseInt(el['x']); for (var h = 0; h < el['height']; h += (this.options.minWidth + this.options.gutter)) { for (var w = 0; w < el['width']; w += (this.options.minWidth + this.options.gutter)) { var x = l + w; var y = t + h; if (!this.matrix[y]) { this._addMatrixRow(y); } this.matrix[y][x] = 'true'; } } }, _getObjectSize: function (obj) { // Helper to get size of object, should probably be moved var size = 0; $.each(obj, function (p, v) { size++; }); return size; }, _fillGaps: function () { var self = this; var box = {}; $.each(this.elements, function (index, el) { self._updateMatrix(el); }); var arr = this.elements; arr.sort(function (a, b) { return a.y - b.y; }); arr.reverse(); // Used to keep the highest y value for a box in memory var topY = arr[0]['y']; // Used for current y with added offset var actualY = 0; // Current number of rows in matrix var rowsLeft = this._getObjectSize(this.matrix); $.each(this.matrix, function (y, row) { rowsLeft--; actualY = parseInt(y); // + parseInt(self.box.offset().top); $.each(row, function (x, col) { if (col === 'false' && actualY < topY) { if (!box.y) box.y = y; if (!box.x) box.x = x; if (!box.w) box.w = 0; if (!box.h) box.h = self.options.minWidth; box.w += (box.w) ? (self.options.minWidth + self.options.gutter) : self.options.minWidth; var addonHeight = 0; for (var row = 1; row < rowsLeft; row++) { var z = parseInt(y) + parseInt(row * (self.options.minWidth + self.options.gutter)); if (self.matrix[z] && self.matrix[z][x] == 'false') { addonHeight += (self.options.minWidth + self.options.gutter); self.matrix[z][x] = 'true'; } else break; } box.h + (parseInt(addonHeight) / (self.options.minWidth + self.options.gutter) == rowsLeft) ? 0 : parseInt(addonHeight); box.ready = true; } else if (box.ready) { $.each(arr, function (i, el) { if (box.y <= arr[i]['y'] && (self.options.resizeToFitOptions.resizeAny || box.w <= arr[i]['width'] && box.h <= arr[i]['height'])) { arr.splice(i, 1); $(el['$el']).addClass('nested-moved'); self.elements.push({ $el: $(el['$el']), x: parseInt(box.x), y: parseInt(box.y), col: i, width: parseInt(box.w), height: parseInt(box.h) }); return false; } }); box = {}; } }); }); self.elements = arr; return self.elements; }, _renderGrid: function ($box, method) { this.counter++; var ypos, gridy = ypos = 0; var tot = 0; var direction = !method ? "append" : "prepend"; // Width & height var width = $box.width(); var height = $box.height(); // Calculate row and col var col = Math.ceil(width / (this.options.minWidth + this.options.gutter)); var row = Math.ceil(height / (this.options.minWidth + this.options.gutter)); // lock widest box to match minColumns if (col > this.options.minColumns) { this.options.minColumns = col; } while (true) { for (var y = col; y >= 0; y--) { if (this.gridrow[gridy + y]) break; this.gridrow[gridy + y] = new Object; for (var x = 0; x < this.columns; x++) { this.gridrow[gridy + y][x] = false; } } for (var column = 0; column < (this.columns - col); column++) { // Add default empty matrix, used to calculate and update matrix for each box matrixY = gridy * (this.options.minWidth + this.options.gutter); this._addMatrixRow(matrixY); var fits = true; for (var y = 0; y < row; y++) { for (var x = 0; x < col; x++) { if (!this.gridrow[gridy + y]) { break; } if (this.gridrow[gridy + y][column + x]) { fits = false; break; } } if (!fits) { break; } } if (fits) { // Set as taken for (var y = 0; y < row; y++) { for (var x = 0; x < col; x++) { if (!this.gridrow[gridy + y]) { break; } this.gridrow[gridy + y][column + x] = true; } } // Push to elements array this._pushItem($box, column * (this.options.minWidth + this.options.gutter), gridy * (this.options.minWidth + this.options.gutter), width, height, col, row, direction); return; } } gridy++; } }, _pushItem: function ($el, x, y, w, h, cols, rows, method) { if (method == "prepend") { this.elements.unshift({ $el: $el, x: x, y: y, width: w, height: h, cols: cols, rows: rows }); } else { this.elements.push({ $el: $el, x: x, y: y, width: w, height: h, cols: cols, rows: rows }); } }, _setHeight: function ($els) { var self = this; $.each($els, function (index, value) { // set maxHeight var colY = (value['y'] + value['height']); if (colY > self.maxHeight) { self.maxHeight = colY; } }); return self.maxHeight; }, _setWidth: function ($els) { var self = this; $.each($els, function (index, value) { // set maxWidth var colX = (value['x'] + value['width']); if (colX > self.currWidth) { self.currWidth = colX; } }); return self.currWidth; }, _renderItems: function ($els) { var self = this; // set container height and width this.box.css('height', this._setHeight($els)); if (this.options.centered) { this.box.css({'width' : this._setWidth($els), 'margin-left' : 'auto', 'margin-right' : 'auto'}); } $els.reverse(); var speed = this.options.animationOptions.speed; var effect = this.options.animationOptions.effect; var duration = this.options.animationOptions.duration; var queue = this.options.animationOptions.queue; var animate = this.options.animate; var complete = this.options.animationOptions.complete; var item = this; var i = 0; var t = 0; $.each($els, function (index, value) { $currLeft = $(value['$el']).position().left; $currTop = $(value['$el']).position().top; $currWidth = $(value['$el']).width(); $currHeight = $(value['$el']).width(); value['$el'].attr('data-y', $currTop).attr('data-x', $currLeft); //if animate and queue if (animate && queue && ($currLeft != value['x'] || $currTop != value['y'])) { setTimeout(function () { value['$el'].css({ 'display': 'block', 'width': value['width'], 'height': value['height'] }).animate({ 'left': value['x'], 'top': value['y'] }, duration); t++; if (t == i) { complete.call(undefined, $els) } }, i * speed); i++; } //if animate and no queue if (animate && !queue && ($currLeft != value['x'] || $currTop != value['y'])) { setTimeout(function () { value['$el'].css({ 'display': 'block', 'width': value['width'], 'height': value['height'] }).animate({ 'left': value['x'], 'top': value['y'] }, duration); t++; if (t == i) { complete.call(undefined, $els) } }, i); i++; } //if no animation and no queue if (!animate && ($currLeft != value['x'] || $currTop != value['y'])) { value['$el'].css({ 'display': 'block', 'width': value['width'], 'height': value['height'], 'left': value['x'], 'top': value['y'] }); t++; if (t == i) { complete.call(undefined, $els) } } }); if (i == 0) { complete.call(undefined, $els) } }, append: function ($els) { this._isResizing = true; this._setBoxes($els, 'append'); this._isResizing = false; }, prepend: function ($els) { this._isResizing = true; this._setBoxes($els, 'prepend'); this._isResizing = false; }, resize: function ($els) { if (Object.keys(this.matrix[0]).length % Math.floor(this.element.width() / (this.options.minWidth + this.options.gutter)) > 0) { this._isResizing = true; this._setBoxes(this.box.find(this.options.selector)); this._isResizing = false; } }, refresh: function(options) { options = options || this.options; this.options = $.extend(true, {}, $.Nested.settings, options); this.elements = []; this._isResizing = false; // build box dimensions this._setBoxes(); }, destroy: function() { var container = this; $(window).unbind("resize", function () { container.resize(); }); // unbind the resize event $els = this.box.find(this.options.selector); $($els).removeClass('nested-moved').removeAttr('style data-box data-width data-x data-y').removeData(); this.box.removeAttr("style").removeData(); } } var methods = { refresh: function(options) { return this.each(function(){ var $this=$(this); var nested = $this.data('nested'); nested.refresh(options); }); }, destroy: function() { return this.each(function(){ var $this=$(this); var nested = $this.data('nested'); nested.destroy(); }); } }; $.fn.nested = function (options, e) { if(methods[options]) { return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); } if (typeof options === 'string') { this.each(function () { var container = $.data(this, 'nested'); container[options].apply(container, [e]); }); } else { this.each(function () { $.data(this, 'nested', new $.Nested(options, this)); }); } return this; } })(jQuery);