/** * 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 }); };