272 lines
12 KiB
JavaScript
272 lines
12 KiB
JavaScript
/**
|
|
* Flowr.js - Simple jQuery plugin to emulate Flickr's justified view
|
|
* For usage information refer to http://github.com/kalyan02/flowr-js
|
|
*
|
|
*
|
|
* @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net)
|
|
* @version: v0.1
|
|
*/
|
|
(function($) {
|
|
//$("#container2").css( 'border', '1px solid #ccc');
|
|
$.fn.flowr = function(options) {
|
|
|
|
$this = this;
|
|
var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy
|
|
var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed
|
|
var NO_COPY_FIELDS = ['complete', 'data', 'responsive']; // these attributes will not be carried forward for append related calls
|
|
var DEFAULTS = {
|
|
'data': [],
|
|
'padding': 5, // whats the padding between flowy items
|
|
'height': 240, // Minimum height an image row should take
|
|
'render': null, // callback function to get the tag
|
|
'append': false, // TODO
|
|
'widthAttr': 'width', // a custom data structure can specify which attribute refers to height/width
|
|
'heightAttr': 'height',
|
|
'maxScale': 1.5, // In case there is only 1 elment in last row
|
|
'maxWidth': this.width() - 1, // 1px is just for offset
|
|
'itemWidth': null, // callback function for width
|
|
'itemHeight': null, // callback function for height
|
|
'complete': null, // complete callback
|
|
'rowClassName': ROW_CLASS_NAME,
|
|
'rows': -1, // Maximum number of rows to render. -1 for no limit.
|
|
'responsive': true // make content responsive
|
|
};
|
|
var settings = $.extend(DEFAULTS, options);
|
|
|
|
// If data is being appended, we already have settings
|
|
// If we already have settings, retrieve them
|
|
if (settings.append && $this.data('lastSettings')) {
|
|
lastSettings = $this.data('lastSettings');
|
|
|
|
// Copy over the settings from previous init
|
|
for (attr in DEFAULTS) {
|
|
if (NO_COPY_FIELDS.indexOf(attr) < 0 && settings[attr] == DEFAULTS[attr]) {
|
|
settings[attr] = lastSettings[attr];
|
|
}
|
|
}
|
|
|
|
// Check if we have an incomplete last row
|
|
lastRow = $this.data('lastRow');
|
|
if (lastRow.data.length > 0 && settings.maxWidth - lastRow.width > MAX_LAST_ROW_GAP) {
|
|
// Prepend the incomplete row to newly loaded data and redraw
|
|
lastRowData = lastSettings.data.slice(lastSettings.data.length - lastRow.data.length - 1);
|
|
settings.data = lastRowData.concat(settings.data);
|
|
|
|
// Remove the incomplete row
|
|
// TODO: Don't reload this stuff later. Reattach to new row.
|
|
$('.' + settings.rowClassName + ':last', $this).detach();
|
|
} else {
|
|
// console.log( lastRow.data.length );
|
|
// console.log( lastRow.width );
|
|
}
|
|
}
|
|
|
|
// only on the first initial call
|
|
if (!settings.responsive && !settings.append)
|
|
$this.width($this.width());
|
|
|
|
// Basic sanity checks
|
|
if (!(settings.data instanceof Array))
|
|
return;
|
|
|
|
if (typeof(settings.padding) != 'number')
|
|
settings.padding = parseInt(settings.padding);
|
|
|
|
if (typeof(settings.itemWidth) != 'function') {
|
|
settings.itemWidth = function(data) {
|
|
return data[settings.widthAttr];
|
|
}
|
|
}
|
|
|
|
if (typeof(settings.itemHeight) != 'function') {
|
|
settings.itemHeight = function(data) {
|
|
return data[settings.heightAttr];
|
|
}
|
|
}
|
|
|
|
// A standalone utility to calculate the item widths for a particular row
|
|
// Returns rowWidth: width occupied & data : the items in the new row
|
|
var utils = {
|
|
getNextRow: function(data, settings) {
|
|
var itemIndex = 0;
|
|
var itemsLength = data.length;
|
|
var lineItems = [];
|
|
var lineWidth = 0;
|
|
var maxWidth = settings.maxWidth;
|
|
var paddingSize = settings.padding;
|
|
|
|
// console.log( 'maxItems=' + data.length );
|
|
|
|
requiredPadding = function() {
|
|
var extraPads = arguments.length == 1 ? arguments[0] : 0;
|
|
return (lineItems.length - 1 + extraPads) * settings.padding;
|
|
}
|
|
|
|
while (lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength)) {
|
|
var itemData = data[itemIndex];
|
|
var itemWidth = settings.itemWidth.call($this, itemData);
|
|
var itemHeight = settings.itemHeight.call($this, itemData);
|
|
|
|
var minHeight = settings.height;
|
|
var minWidth = Math.floor(itemWidth * settings.height / itemHeight);
|
|
|
|
|
|
if (minWidth > settings.maxWidth) {
|
|
// very short+wide images like panoramas
|
|
// show them even if ugly, as wide as possible
|
|
minWidth = settings.maxWidth - 1 - requiredPadding(1);
|
|
minHeight = settings.height * minHeight / minWidth;
|
|
}
|
|
var newLineWidth = lineWidth + minWidth;
|
|
|
|
// console.log( 'lineWidth = ' + lineWidth );
|
|
// console.log( 'newLineWidth = ' + newLineWidth );
|
|
if (newLineWidth < settings.maxWidth) {
|
|
lineItems.push({
|
|
'height': minHeight,
|
|
'width': minWidth,
|
|
'itemData': itemData
|
|
});
|
|
|
|
lineWidth += minWidth;
|
|
itemIndex++;
|
|
} else {
|
|
// We'd have exceeded width. So break off to scale.
|
|
// console.log( 'breaking off = ' + itemIndex );
|
|
// console.log( 'leave off size = ' + lineItems.length );
|
|
break;
|
|
}
|
|
} //while
|
|
|
|
// Scale the size to max width
|
|
testWidth = 0;
|
|
if (lineWidth < settings.maxWidth) {
|
|
var fullScaleWidth = settings.maxWidth - requiredPadding() - 10;
|
|
var currScaleWidth = lineWidth;
|
|
var scaleFactor = fullScaleWidth / currScaleWidth;
|
|
if (scaleFactor > settings.maxScale)
|
|
scaleFactor = 1;
|
|
|
|
var newHeight = Math.round(settings.height * scaleFactor);
|
|
for (i = 0; i < lineItems.length; i++) {
|
|
var lineItem = lineItems[i];
|
|
lineItem.width = Math.floor(lineItem.width * scaleFactor);
|
|
lineItem.height = newHeight;
|
|
|
|
testWidth += lineItem.width;
|
|
}
|
|
}
|
|
|
|
return {
|
|
data: lineItems,
|
|
width: testWidth + requiredPadding()
|
|
};
|
|
}, //getNextRow
|
|
reorderContent: function() {
|
|
/*
|
|
TODO: optimize for faster resizing by reusing dom objects instead of killing the dom
|
|
*/
|
|
var _initialWidth = $this.data('width');
|
|
var _newWidth = $this.width();
|
|
var _change = _initialWidth - _newWidth;
|
|
|
|
if (_initialWidth != _newWidth) {
|
|
$this.html('');
|
|
var _settings = $this.data('lastSettings');
|
|
_settings.data = $this.data('data');
|
|
_settings.maxWidth = $this.width() - 1;
|
|
$this.flowr(_settings);
|
|
}
|
|
}
|
|
} //utils
|
|
|
|
// If the responsive var is set to true then listen for resize method
|
|
// and prevent resizing from happening twice if responsive is set again during append phase!
|
|
if (settings.responsive && !$this.data('__responsive')) {
|
|
$(window).resize(function() {
|
|
initialWidth = $this.data('width');
|
|
newWidth = $this.width();
|
|
|
|
//initiate resize
|
|
if (initialWidth != newWidth) {
|
|
var task_id = $this.data('task_id');
|
|
if (task_id) {
|
|
task_id = clearTimeout(task_id);
|
|
task_id = null;
|
|
}
|
|
task_id = setTimeout(utils.reorderContent, 80);
|
|
$this.data('task_id', task_id);
|
|
}
|
|
});
|
|
$this.data('__responsive', true);
|
|
}
|
|
|
|
|
|
return this.each(function() {
|
|
|
|
// Get a copy of original data. 1 level deep copy is sufficient.
|
|
var data = settings.data.slice(0);
|
|
var rowData = null;
|
|
var currentRow = 0;
|
|
var currentItem = 0;
|
|
|
|
// Store all the data
|
|
var allData = [];
|
|
for (i = 0; i < data.length; i++) {
|
|
allData.push(data[i]);
|
|
}
|
|
$this.data('data', allData);
|
|
|
|
// While we have a new row
|
|
while ((rowData = utils.getNextRow(data, settings)) != null && rowData.data.length > 0) {
|
|
if (settings.rows > 0 && currentRow >= settings.rows)
|
|
break;
|
|
// remove the number of elements in the new row from the top of data stack
|
|
data.splice(0, rowData.data.length);
|
|
|
|
// Create a new row div, add class, append the htmls and insert the flowy items
|
|
var $row = $('<div>').addClass(settings.rowClassName);
|
|
var slack = $this[0].clientWidth - rowData.width - 2 * settings.padding
|
|
for (i = 0; i < rowData.data.length; i++) {
|
|
var displayData = rowData.data[i];
|
|
// Get the HTML object from custom render function passed as argument
|
|
var displayObject = settings.render.call($this, displayData);
|
|
displayObject = $(displayObject);
|
|
extraw = Math.floor(slack/rowData.data.length)
|
|
if (i == 0) {
|
|
extraw += slack % rowData.data.length
|
|
}
|
|
// Set some basic stuff
|
|
displayObject
|
|
.css('width', displayData.width + extraw)
|
|
.css('height', displayData.height)
|
|
.css('margin-bottom', settings.padding + "px")
|
|
.css('margin-left', i == 0 ? '0' : settings.padding + "px"); //TODO:Refactor
|
|
$row.append(displayObject);
|
|
|
|
currentItem++;
|
|
}
|
|
$this.append($row);
|
|
// console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width );
|
|
|
|
currentRow++;
|
|
$this.data('lastRow', rowData);
|
|
}
|
|
// store the current state of settings and the items in last row
|
|
// we'll need this info when we append more items
|
|
$this.data('lastSettings', settings);
|
|
|
|
// onComplete callback
|
|
// pass back info about list of rows and items rendered
|
|
if (typeof(settings.complete) == 'function') {
|
|
var completeData = {
|
|
renderedRows: currentRow,
|
|
renderedItems: currentItem
|
|
}
|
|
settings.complete.call($this, completeData);
|
|
}
|
|
});
|
|
};
|
|
|
|
})(jQuery);
|