578 lines
19 KiB
JavaScript
578 lines
19 KiB
JavaScript
/**
|
|
* 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);
|