/**
* Ingrid : JQuery Datagrid Control
*
* Copyright (c) 2007-2009 Matthew Knight (http://www.reconstrukt.com http://slu.sh)
*
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* @requires jQuery v1.2+
* @version 0.9.3
* @todo load JSON data, etc.
*
* Revision: 0.9.3.0 2009/06/26 Patrice Blanchardie
* - bug fixes: selection behaviour,
* hscroll width,
* attribute selector,
* result error handler,
* header auto-resize
* - feature: new param: unsortable columns
*
*/
jQuery.fn.ingrid = function(o){
var cfg = {
height: 200, // height of our datagrid (scrolling body area)
savedStateLoad : false, // when Ingrid is initialized, should it load data from a previously saved state?
initialLoad : false, // when Ingrid is initialized, should it load data immediately?
colWidths: [225,225,225,225], // width of each column
minColWidth: 60, // minimum column width
headerHeight: 30, // height of our header
headerClass: 'grid-header-bg', // header bg
resizableCols: true, // make columns resizable via drag + drop
gridClass: 'datagrid', // class of head & body
rowClasses: [], // list of row classes (selected by cursor)
colClasses: [], // array of classes : i.e. ['','grid-col-2','','']
rowHoverClass: 'grid-row-hover', // hovering over a row? use this class
rowSelection: true, // allow row selection?
rowSelectedClass: 'grid-row-sel', // selecting a row? use this class
onRowSelect: function(tr, selected){}, // function to call when row is clicked
/* sorting */
sorting: true,
colSortParams: [], // value to pass as sort param when header clicked (i.e. '&sort=param') ex: ['col1','col2','col3','col4']
sortAscParam: 'asc', // param passed on ascending sort (i.e. '&dir=asc)
sortDescParam: 'desc', // param passed on ascending sort (i.e. '&dir=desc)
sortedCol: 'col1', // current data's sorted column (can be a key from 'colSortParams', or an int 0-n (for n columns)
sortedColDir: 'desc', // current data's sorted directions
sortDefaultDir: 'desc', // on 1st click, sort tihs direction
sortAscClass: 'grid-sort-asc', // class for ascending sorted col
sortDescClass: 'grid-sort-desc', // class for descending sorted col
sortNoneClass: 'grid-sort-none', // ... not sorted? use this class
unsortableCols: [], // do not make theses columns sortable
/* paging */
paging: true, // create a paging toolbar
pageNumber: 1,
recordsPerPage: 0,
totalRecords: 0,
pageToolbarHeight: 25,
pageToolbarClass: 'grid-page-toolbar',
pageStartClass: 'grid-page-start',
pagePrevClass: 'grid-page-prev',
pageInfoClass: 'grid-page-info',
pageInputClass: 'grid-page-input',
pageNextClass: 'grid-page-next',
pageEndClass: 'grid-page-end',
pageLoadingClass: 'grid-page-loading',
pageLoadingDoneClass: 'grid-page-loading-done',
pageViewingRecordsInfoClass: 'grid-page-viewing-records-info',
/* ajax stuff */
url: 'remote.php', // url to fetch data
type: 'GET', // 'POST' or 'GET'
dataType: 'html', // 'html' or 'json' - expected dataType returned
extraParams: {}, // a map of extra params to send to the server
loadingClass: 'grid-loading', // loading modalmask div
loadingHtml: '
',
/* should seldom change */
resizeHandleHtml: '', // resize handle html + css
resizeHandleClass: 'grid-col-resize',
scrollbarW: 22, // width allocated for scrollbar
columnIDAttr: '_colid', // attribute name used to groups TDs in columns
ingridIDPrefix: '_ingrid', // prefix used to create unique IDs for Ingrid
/* cookie, for saving state */
cookieExpiresDays: 360,
cookiePath: '/',
/* not yet implemented */
minHeight: 100,
resizableGrid: true,
dragDropCols: true,
sortType: 'server|client|none',
/* cfg functions */
isSortableCol : function(colIndex) {
if (cfg.unsortableCols.length==0 || jQuery.inArray(colIndex, cfg.unsortableCols)==-1) {
return true;
}
return false;
}
};
jQuery.extend(cfg, o);
// break into 2 tables: header, body.
// create header table
var cols = new Array();
var h = jQuery('
')
.html(this.find('thead'))
.addClass(cfg.gridClass)
.addClass(cfg.headerClass)
.height(cfg.headerHeight)
.extend({
cols : cols
});
// initialize columns
h.find('th').each(function(i){
// init width
jQuery(this).width(cfg.colWidths[i]);
// put column text in a div, make unselectable
var col_label = jQuery('')
.html(jQuery(this).html())
.css({float: 'left', display: 'block'})
.css('-moz-user-select', 'none')
.css('-khtml-user-select', 'none')
.css('user-select', 'none')
.attr('unselectable', 'on');
// column sorting?
if (cfg.sorting && cfg.isSortableCol(i)) {
var key = cfg.colSortParams[i] ? cfg.colSortParams[i] : i;
// is this column the default sorted column?
var cls = (key == cfg.sortedCol || i == cfg.sortedCol) ?
( cfg.sortedColDir == cfg.sortAscParam ? cfg.sortAscClass : cfg.sortDescClass ) :
( cfg.sortNoneClass );
col_label.addClass(cls).click(function(){
var dir = col_label.hasClass(cfg.sortNoneClass) ?
cfg.sortDefaultDir : ( col_label.hasClass(cfg.sortAscClass) ? cfg.sortDescParam : cfg.sortAscParam );
var params = { sort : key, dir : dir };
if (p) jQuery.extend(params, { page : p.getPage() } );
g.load( params, function(){
var cls = col_label.hasClass(cfg.sortNoneClass) ?
( cfg.sortDefaultDir == cfg.sortAscParam ? cfg.sortAscClass : cfg.sortDescClass ) :
( col_label.hasClass(cfg.sortAscClass) ? cfg.sortDescClass : cfg.sortAscClass );
// re-init sortable cols
var i2 = 0;
g.getHeaders(function(col){
col.find('div:first').each(function() {
if(cfg.isSortableCol(i2++))
jQuery(this).addClass(cfg.sortNoneClass).removeClass(cfg.sortAscClass).removeClass(cfg.sortDescClass);
});
});
col_label.removeClass(cfg.sortAscClass).removeClass(cfg.sortDescClass).addClass(cls).removeClass(cfg.sortNoneClass);
});
});
}
// replace contents of
jQuery(this).html(col_label);
// bind an event to easily resize columns
jQuery(this).bind('resizeColumn', {col_num : i}, function(e, w){
jQuery(this).width(w);
// auto enlarge while header is > headerHeight
while(jQuery(this).parent().height()>cfg.headerHeight) {
jQuery(this).width(++w);
}
// set body cells to this width
g.resize();
g.getColumn(e.data.col_num).each(function(){
jQuery(this).width(w);
});
});
// append resize handle?
if (cfg.resizableCols) {
// make column headers resizable
var handle = jQuery('').html(cfg.resizeHandleHtml == '' ? '-' : cfg.resizeHandleHtml).addClass(cfg.resizeHandleClass);
handle.bind('mousedown', function(e){
// start resize drag
var th = jQuery(this).parent();
var left = e.clientX;
z.resizeStart(th, left);
});
jQuery(this).append(handle);
}
});
// create body table. surround body with container div for scrolling
// setting width on first row keeps it from "blinking"
var row = this.find('tr:first')
jQuery(row).find('td').each(function(i){
jQuery(this).width( cfg.colWidths[i] )
});
var b = jQuery('')
.html( jQuery('
').html( this.find('tbody') ).width( h.width() ).addClass(cfg.gridClass) )
.css('overflow', 'auto')
.height(cfg.height);
// resizable cols?
// if so create a vertical resize divider, with unique ID
if (cfg.resizableCols) {
var z_sel = 'vertical-resize-divider' + new Date().getTime();
var z = jQuery('')
.css({
backgroundColor: '#ababab',
height: (cfg.headerHeight + cfg.height),
width: '4px',
position: 'absolute',
zIndex: '10',
display: 'block'
})
.extend({
resizeStart : function(th, eventX){
// this is fired onmousedown of the column's resize handle
var pos = th.offset();
jQuery(this).show().css({
top: pos.top,
left: eventX
})
// when resizing, bind some listeners for mousemove & mouseup events
jQuery('body').bind('mousemove', {col : th}, function(e){
// on mousemove, move the vertical-resize-divider
var th = e.data.col;
var pos = th.offset();
var col_w = e.clientX - pos.left;
// make sure cursor isn't trying to make column smaller than minimum
if (col_w > cfg.minColWidth) {
jQuery('#' + z_sel).css('left', e.clientX);
}
})
jQuery('body').bind('mouseup', {col : th}, function(e){
// on mouseup,
// 1.) unbind resize listener events from body
// 2.) hide the vertical-resize-divider
// 3.) trigger the resize event on the column
jQuery(this).unbind('mousemove').unbind('mouseup');
jQuery('#' + z_sel).hide();
var th = e.data.col;
var pos = th.offset();
var col_w = e.clientX - pos.left;
if (col_w > cfg.minColWidth) {
th.trigger('resizeColumn', [col_w]);
} else {
th.trigger('resizeColumn', [cfg.minColWidth]);
}
})
}
});
}
// paging?
// if so create a paging toolbar
if (cfg.paging) {
// create a paging toolbar
var totr = cfg.recordsPerPage > 0 ? cfg.recordsPerPage : b.find('tr').length;
// total records viewing message (if we know total records)
// total record count might not be passed in config, it's sometimes an expensive hit to the DB
var pv;
if (cfg.totalRecords > 0) {
pv = jQuery('')
.addClass(cfg.pageViewingRecordsInfoClass)
.extend({
updateViewInfo : function(loaded_rows, page){
var _start = ( (loaded_rows * (page - 1) + 1) );
var _end = ( (loaded_rows * page) > cfg.totalRecords ? cfg.totalRecords : loaded_rows * page );
this.html('Viewing Rows ' + _start + ' - ' + _end + ' of ' + cfg.totalRecords);
return this;
}
});
// update the "viewing x of y" record info
pv.updateViewInfo(totr, cfg.pageNumber);
}
// create our paging control container
var p = jQuery('')
.addClass(cfg.pageToolbarClass)
.height(cfg.pageToolbarHeight)
.width(b.width())
.extend({
setPage : function(p){
var input = this.find('input.' + cfg.pageInputClass);
pload.removeClass(cfg.pageLoadingDoneClass);
g.load( {page : p}, function(){
input.val(p);
if (cfg.totalRecords > 0) {
var totr = b.find('tr').length;
pv.updateViewInfo(totr, p);
}
pload.addClass(cfg.pageLoadingDoneClass);
});
return this;
},
getPage : function(){
var p = Number(this.find('input.' + cfg.pageInputClass).val());
return p;
}
});
// start page button
var pb1 = jQuery('«').addClass(cfg.pageStartClass).click(function(){
p.setPage(1);
});
// prev page button
var pb2 = jQuery('<').addClass(cfg.pagePrevClass).click(function(){
var _p = p.getPage();
if (_p > 1) {
_p--;
p.setPage(_p);
}
});
// next page button
if (cfg.totalRecords > 0) {
var totp = Math.ceil(cfg.totalRecords / totr);
}
var pb3 = jQuery('>').addClass(cfg.pageNextClass).click(function(){
var _p = p.getPage(); _p++;
if (totp) {
if (_p <= totp) p.setPage(_p);
} else {
p.setPage(_p);
}
});
// loading indicator
var pload = jQuery('').addClass(cfg.pageLoadingClass).addClass(cfg.pageLoadingDoneClass);
// page field & form
var pfld = jQuery('').addClass(cfg.pageInputClass);
var pinfo = jQuery('')
.addClass(cfg.pageInfoClass)
.append(pfld);
var pform = jQuery('').append(pinfo).submit(function(){
var _p = parseInt(p.getPage());
if (_p) {
if (totp) {
if (_p <= totp) p.setPage(_p);
} else if (_p > 0) {
p.setPage(_p);
}
} else {
alert('Please Enter a Valid Page Number.');
}
return false;
});
// last page button & info (if we know total records)
var pb4;
if (cfg.totalRecords > 0) {
pinfo.html('Page ' + pinfo.html() + ' of ' + totp);
var pb4 = jQuery('»').addClass(cfg.pageEndClass).click(function(){
var _p = p.getPage(); _p++;
if (totp) {
if (_p < totp) p.setPage(totp);
}
});
} else {
pinfo.html('Page ' + pinfo.html());
}
p.append(pb1).append(pb2).append(pform).append(pb3).append(pb4).append(pload).append(pv);
}
// create a container div to for our main grid object
// append & extend grid {g} with header {h}, body {b}, paging {p}, resize handle {z}
var g = jQuery('').append(h).append(b).extend({
h : h,
b : b
});
if (cfg.paging) {
g.append(p).extend({ p : p });
}
if (cfg.resizableCols) {
g.append(z.hide()).extend({ z : z });
}
// create some other piece-parts, like
// ...a gap filler to fill gap over scrollport
var gap = jQuery('').width(cfg.scrollbarW).addClass(cfg.headerClass).height(cfg.headerHeight).css({
position: 'absolute',
zIndex: '0'
}).appendTo(g);
// ...a loading modal mask
var modalmask = jQuery('').html(cfg.loadingHtml).addClass(cfg.loadingClass).css({
position: 'absolute',
zIndex: '1000'
}).appendTo(g).hide();
// create methods on our grid object
g.extend({
load : function(params, cb) {
var data = jQuery.extend(cfg.extraParams, params);
/*
alert(this + ' ...is jQuery')
alert(this[0] + ' ...is the div, id="' + this.attr('id') + '"')
*/
// show loading canvas
modalmask.width(b.width()).show();
// save selected rows
g.saveSelectedRows();
jQuery.ajax({
type: cfg.type.toUpperCase(),
url: cfg.url,
data: data,
success: function(result){
if(result == "") {
alert('Error: Empty result.');
return;
}
// for JSON return type
if (cfg.dataType == 'json') {
var rows = eval( '(' + result + ')' );
alert('json = ' + rows);
}
// for HTML (Table) return type
if (cfg.dataType == 'html') {
var $tbl = jQuery(result);
var row = $tbl.find('tr:first');
if ( jQuery(row).find('td').length == cfg.colWidths.length ) {
// setting width on first row keeps it from "blinking"
jQuery(row).find('td').each(function(i){
// don't use width() - makes column headers jitter
// g.getHeader(i).width()
jQuery(this).width( g.getHeader(i).css('width') )
});
// now swap the tbody's
b.find('tbody').html($tbl.find('tbody').html());
g.initStylesAndWidths();
// remember the last loaded state for this grid?
g.saveState(data);
} else if (row.length < 1) {
// no rows returned
alert('Error: No Rows Returned.');
} else {
// inconsistent results... too many (or too few) columns returned
alert('Error: Total columns returned [' + $tbl.find('tbody tr:first td').length + '] do not match Ingrid ['+ cfg.colWidths.length +'].');
}
}
if (cb) cb();
},
error: function(){
alert('Error: Could not load "' + cfg.url + '". Please check the URL and try again. ');
},
complete: function(){
modalmask.hide();
}
});
return this;
},
// returns JSON
getState : function() {
/*
alert(this + ' ...is jQuery')
alert(this[0] + ' ...is the div, id="' + this.attr('id') + '"')
*/
var props = {
url : 'nothing'
}
return props;
},
saveState : function(data){
// how can we deserialize the 'data' object from JSON, to a string, like: "{page:3}"
// we could then save this JSON string into a cookie,
// and eval() it back out again when initStylesAndWidths() is called
/*
I think I need the JSON lib?
JSON.toString(json_object)
so, like
JSON.toString(props)
would be nice to combine JSON & jQuery's cookie plugin, call it something like "cache"
which would let you serialize JSON objects as strings, for storage in cookies, and eval() them back out from a cookie later
so you could call like:
jQuery.toCache(json_object, 'key')
json_object = eval( jQuery.fromCache('key') );
jQuery.clearCache('key')
...u could get creative and call it "save", "remember", "recall", "read", "store", "forget" or whatever
*/
if (jQuery.cookie) {
// save page #, column sort & dir
var g_id = this.attr('id');
var param_str = 'page=' + data.page + ',sort=' + data.sort + ',dir=' + data.dir;
jQuery.cookie(g_id, param_str, {expires: cfg.cookieExpiresDays, path: cfg.cookiePath});
}
/*
props.url = data;
alert( data.toString() );
alert( props.toString() );
*/
},
saveSelectedRows : function() {
if (jQuery.cookie) {
var row_ids = g.getSelectedRowIds();
if (row_ids.length > 0) {
jQuery.cookie( this.attr('id') + '_rows', row_ids.join(','), {expires: cfg.cookieExpiresDays, path: cfg.cookiePath});
}
}
},
// returns
els
getHeaders : function(cb) {
var ths = this.find('th');
if (cb) {
ths.each(function(){
cb(jQuery(this));
});
return this;
} else {
return ths;
}
},
// returns single
el
getHeader : function(i, cb) {
var th = this.find('th').slice(i, i+1);
if (cb) {
cb(jQuery(this));
return this;
} else {
return th;
}
},
// returns
els in column i
getColumn : function(i, cb) {
var tds = this.find("tbody td[" + cfg.columnIDAttr + "='" + i + "']");
if (cb) {
tds.each(function(){
cb(jQuery(this));
});
return this;
} else {
return tds;
}
},
// returns
els
getRows : function(cb) {
var trs = this.find("tbody tr");
if (cb) {
trs.each(function(){
cb(jQuery(this));
});
return this;
} else {
return trs;
}
},
// returns
els
getSelectedRows : function() {
return this.find("tbody tr[_selected='true']");
},
// returns an array of IDs (current view)
getSelectedRowIds : function() {
var rows = g.getSelectedRows();
var row_ids = [];
for (i=0; i 0) {
var cursor = (r == 0 ? 0 : r % cfg.rowClasses.length);
if (cfg.rowClasses[cursor] != '') {
// custom row class
jQuery(this).addClass(cfg.rowClasses[cursor]);
}
if (cfg.rowHoverClass != '') {
// hover class
jQuery(this).hover(
function() { if (jQuery(this).attr('_selected') != 'true') jQuery(this).removeClass(cfg.rowClasses[cursor]).addClass(cfg.rowHoverClass); },
function() { if (jQuery(this).attr('_selected') != 'true') jQuery(this).removeClass(cfg.rowHoverClass).addClass(cfg.rowClasses[cursor]); }
);
}
}
// selection behaviour
if (cfg.rowSelection == true) {
jQuery(this).click(function(){
if (jQuery(this).attr('_selected')) {
jQuery(this).attr('_selected') == 'true' ?
jQuery(this).attr('_selected', 'false').removeClass(cfg.rowSelectedClass) :
jQuery(this).attr('_selected', 'true').addClass(cfg.rowSelectedClass);
} else {
jQuery(this).attr('_selected', 'true').addClass(cfg.rowSelectedClass);
}
if (cfg.onRowSelect) {
cfg.onRowSelect(this, (jQuery(this).attr('_selected') == 'true' ? true : false) );
}
});
// previously selected rows
if (jQuery(this).attr('id') && str_ids.indexOf( '|' + jQuery(this).attr('id') + '|' ) != -1) {
jQuery(this).attr('_selected', 'true').addClass(cfg.rowSelectedClass);
}
}
// setup column IDs & classes on row's cells
jQuery(this).find('td').each(function(i){
// column IDs & width
// wrap the cell text in a div with overflow hidden, so cells aren't stretched wider by long text
var txt = jQuery(this).html();
jQuery(this).attr(cfg.columnIDAttr, i)
.width(colWidths[i])
.html( jQuery('').html(txt).css('overflow', 'hidden') );
// column colors
if (cfg.colClasses.length > 0) {
if (cfg.colClasses[i] != '') {
jQuery(this).addClass(cfg.colClasses[i]);
}
}
});
});
}
});
// don't break the chain
// return a modified & extended jQ table object.
// here,
// this ...is jQuery
// this[0] ...is a table
/*
alert(this + ' ...is jQuery')
alert(this[0] + ' ...is a table')
alert(this.length + ' = total tables matched to selector')
*/
return this.each(function(tblIter){
// fires for each table[tblIter].
// for each one,
// this ...is a table
/*
alert(this + ' ...is a table [' + tblIter + '] , id="' + jQuery(this).attr('id') + '"')
alert(g[0] + ' ...is the grid div html');
*/
// append to doc
var g_id = cfg.ingridIDPrefix + '_' + jQuery( this ).attr('id') + '_' + tblIter;
g.attr( 'id', g_id );
jQuery( this ).replaceWith( g[0] )
// init grid styles, etc
g.initStylesAndWidths();
// sync grid size to headers
g.resize();
// place the mask accordingly
modalmask.width( h.width() + cfg.scrollbarW ).height( b.height()).css({
top: b.offset().top,
left: b.offset().left
});
// load it up?
if (cfg.savedStateLoad && jQuery.cookie) {
var param_str = jQuery.cookie(g_id);
if (!param_str) {
g.load();
cfg.initialLoad = false;
} else {
// we have a saved state for this grid_id
var pairs = param_str.split(',');
var params = {};
var hash = [];
for (i=0; i 0) {
for (i=0; i, like setSort()?
// (re-init sortable cols)
var i2 = 0;
g.getHeaders(function(col){
col.find('div:first').each(function() {
if(cfg.isSortableCol(i2++))
g.getHeaders(function(th){
jQuery(this).addClass(cfg.sortNoneClass).removeClass(cfg.sortAscClass).removeClass(cfg.sortDescClass);
})
})
});
// all this prevents the column from being style-less (and blinking)
g.getHeader(parseInt(colid)).find('div:first').addClass(cfg.sortNoneClass).removeClass(cfg.sortAscClass).removeClass(cfg.sortDescClass)
.addClass( params.dir == cfg.sortAscParam ? cfg.sortAscClass : cfg.sortDescClass )
.removeClass(cfg.sortNoneClass);
}
if ( params.page || params.sort || params.dir ) {
g.load(params);
cfg.initialLoad = false;
}
}
}
if (cfg.initialLoad) {
g.load();
}
}).extend({
g : g
});
};