/** * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com) *  * Dual licensed under the MIT and GPL licenses.  * This basically means you can use this code however you want for * free, but don't claim to have written it yourself! * Donations always accepted: http://www.JavascriptToolbox.com/donate/ *  * Please do not link to the .js files on javascripttoolbox.com from * your site. Copy the files locally to your server instead. *  *//** * Table.js * Functions for interactive Tables * * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com) * Dual licensed under the MIT and GPL licenses.  * * @version 0.981 * * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin. * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes * @history 0.958 2007-02-28 Added auto functionality based on class names * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality. * @history 0.950 2006-11-15 First BETA release. * * @todo Add more date format parsers * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column * @todo Correct for colspans in data rows (this may slow it down) * @todo Fix for IE losing form control values after sort? *//** * Sort Functions */var Sort = (function(){	var sort = {};	// Default alpha-numeric sort	// --------------------------	sort.alphanumeric = function(a,b) {		return (a==b)?0:(a<b)?-1:1;	};	sort['default'] = sort.alphanumeric; // IE chokes on sort.default	// This conversion is generalized to work for either a decimal separator of , or .	sort.numeric_converter = function(separator) {		return function(val) {			if (typeof(val)=="string") {				val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0;			}			return val || 0;		};	};	// Numeric Sort		// ------------	sort.numeric = function(a,b) {		return sort.numeric.convert(a)-sort.numeric.convert(b);	};	sort.numeric.convert = sort.numeric_converter(".");	// Numeric Sort	- comma decimal separator	// --------------------------------------	sort.numeric_comma = function(a,b) {		return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b);	};	sort.numeric_comma.convert = sort.numeric_converter(",");	// Case-insensitive Sort	// ---------------------	sort.ignorecase = function(a,b) {		return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));	};	sort.ignorecase.convert = function(val) {		if (val==null) { return ""; }		return (""+val).toLowerCase();	};	// Currency Sort	// -------------	sort.currency = sort.numeric; // Just treat it as numeric!	sort.currency_comma = sort.numeric_comma;	// Date sort	// ---------	sort.date = function(a,b) {		return sort.numeric(sort.date.convert(a),sort.date.convert(b));	};	// Convert 2-digit years to 4	sort.date.fixYear=function(yr) {		yr = +yr;		if (yr<50) { yr += 2000; }		else if (yr<100) { yr += 1900; }		return yr;	};	sort.date.formats = [		// YY[YY]-MM-DD		{ re:/(\d{2,4})-(\d{1,2})-(\d{1,2})/ , f:function(x){ return (new Date(sort.date.fixYear(x[1]),+x[2],+x[3])).getTime(); } }		// MM/DD/YY[YY] or MM-DD-YY[YY]		,{ re:/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/ , f:function(x){ return (new Date(sort.date.fixYear(x[3]),+x[1],+x[2])).getTime(); } }		// Any catch-all format that new Date() can handle. This is not reliable except for long formats, for example: 31 Jan 2000 01:23:45 GMT		,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } }	];	sort.date.convert = function(val) {		var m,v, f = sort.date.formats;		for (var i=0,L=f.length; i<L; i++) {			if (m=val.match(f[i].re)) {				v=f[i].f(m);				if (typeof(v)!="undefined") { return v; }			}		}		return 9999999999999; // So non-parsed dates will be last, not first	};	return sort;})();/** * The main Table namespace */var Table = (function(){	/**	 * Determine if a reference is defined	 */	function def(o) {return (typeof o!="undefined");};	/**	 * Determine if an object or class string contains a given class.	 */	function hasClass(o,name) {		return new RegExp("(^|\\s)"+name+"(\\s|$)").test(o.className);	};	/**	 * Add a class to an object	 */	function addClass(o,name) {		var c = o.className || "";		if (def(c) && !hasClass(o,name)) {			o.className += (c?" ":"") + name;		}	};	/**	 * Remove a class from an object	 */	function removeClass(o,name) {		var c = o.className || "";		o.className = c.replace(new RegExp("(^|\\s)"+name+"(\\s|$)"),"$1");	};	/**	 * For classes that match a given substring, return the rest	 */	function classValue(o,prefix) {		var c = o.className;		if (c.match(new RegExp("(^|\\s)"+prefix+"([^ ]+)"))) {			return RegExp.$2;		}		return null;	};	/**	 * Return true if an object is hidden.	 * This uses the "russian doll" technique to unwrap itself to the most efficient	 * function after the first pass. This avoids repeated feature detection that 	 * would always fall into the same block of code.	 */	 function isHidden(o) {		if (window.getComputedStyle) {			var cs = window.getComputedStyle;			return (isHidden = function(o) {				return 'none'==cs(o,null).getPropertyValue('display');			})(o);		}		else if (window.currentStyle) {			return(isHidden = function(o) {				return 'none'==o.currentStyle['display'];			})(o);		}		return (isHidden = function(o) {			return 'none'==o.style['display'];		})(o);	};	/**	 * Get a parent element by tag name, or the original element if it is of the tag type	 */	function getParent(o,a,b) {		if (o!=null && o.nodeName) {			if (o.nodeName==a || (b && o.nodeName==b)) {				return o;			}			while (o=o.parentNode) {				if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {					return o;				}			}		}		return null;	};	/**	 * Utility function to copy properties from one object to another	 */	function copy(o1,o2) {		for (var i=2;i<arguments.length; i++) {			var a = arguments[i];			if (def(o1[a])) {				o2[a] = o1[a];			}		}	}	// The table object itself	var table = {		//Class names used in the code		AutoStripeClassName:"table-autostripe",		StripeClassNamePrefix:"table-stripeclass:",		AutoSortClassName:"table-autosort",		AutoSortColumnPrefix:"table-autosort:",		AutoSortTitle:"Click to sort",		SortedAscendingClassName:"table-sorted-asc",		SortedDescendingClassName:"table-sorted-desc",		SortableClassName:"table-sortable",		SortableColumnPrefix:"table-sortable:",		NoSortClassName:"table-nosort",		AutoFilterClassName:"table-autofilter",		FilteredClassName:"table-filtered",		FilterableClassName:"table-filterable",		FilteredRowcountPrefix:"table-filtered-rowcount:",		RowcountPrefix:"table-rowcount:",		FilterAllLabel:"All",		AutoPageSizePrefix:"table-autopage:",		AutoPageJumpPrefix:"table-page:",		PageNumberPrefix:"table-page-number:",		PageCountPrefix:"table-page-count:"	};	/**	 * A place to store misc table information, rather than in the table objects themselves	 */	table.tabledata = {};	/**	 * Resolve a table given an element reference, and make sure it has a unique ID	 */	table.uniqueId=1;	table.resolve = function(o,args) {		if (o!=null && o.nodeName && o.nodeName!="TABLE") {			o = getParent(o,"TABLE");		}		if (o==null) { return null; }		if (!o.id) {			var id = null;			do { var id = "TABLE_"+(table.uniqueId++); } 				while (document.getElementById(id)!=null);			o.id = id;		}		this.tabledata[o.id] = this.tabledata[o.id] || {};		if (args) {			copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");		}		return o;	};	/**	 * Run a function against each cell in a table header or footer, usually 	 * to add or remove css classes based on sorting, filtering, etc.	 */	table.processTableCells = function(t, type, func, arg) {		t = this.resolve(t);		if (t==null) { return; }		if (type!="TFOOT") {			this.processCells(t.tHead, func, arg);		}		if (type!="THEAD") {			this.processCells(t.tFoot, func, arg);		}	};	/**	 * Internal method used to process an arbitrary collection of cells.	 * Referenced by processTableCells.	 * It's done this way to avoid getElementsByTagName() which would also return nested table cells.	 */	table.processCells = function(section,func,arg) {		if (section!=null) {			if (section.rows && section.rows.length && section.rows.length>0) { 				var rows = section.rows;				for (var j=0,L2=rows.length; j<L2; j++) { 					var row = rows[j];					if (row.cells && row.cells.length && row.cells.length>0) {						var cells = row.cells;						for (var k=0,L3=cells.length; k<L3; k++) {							var cellsK = cells[k];							func.call(this,cellsK,arg);						}					}				}			}		}	};	/**	 * Get the cellIndex value for a cell. This is only needed because of a Safari	 * bug that causes cellIndex to exist but always be 0.	 * Rather than feature-detecting each time it is called, the function will	 * re-write itself the first time it is called.	 */	table.getCellIndex = function(td) {		var tr = td.parentNode;		var cells = tr.cells;		if (cells && cells.length) {			if (cells.length>1 && cells[cells.length-1].cellIndex>0) {				// Define the new function, overwrite the one we're running now, and then run the new one				(this.getCellIndex = function(td) {					return td.cellIndex;				})(td);			}			// Safari will always go through this slower block every time. Oh well.			for (var i=0,L=cells.length; i<L; i++) {				if (tr.cells[i]==td) {					return i;				}			}		}		return 0;	};	/**	 * A map of node names and how to convert them into their "value" for sorting, filtering, etc.	 * These are put here so it is extensible.	 */	table.nodeValue = {		'INPUT':function(node) { 			if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {				return node.value;			}			return "";		},		'SELECT':function(node) {			if (node.selectedIndex>=0 && node.options) {				// Sort select elements by the visible text				return node.options[node.selectedIndex].text;			}			return "";		},		'IMG':function(node) {			return node.name || "";		}	};	/**	 * Get the text value of a cell. Only use innerText if explicitly told to, because 	 * otherwise we want to be able to handle sorting on inputs and other types	 */	table.getCellValue = function(td,useInnerText) {		if (useInnerText && def(td.innerText)) {			return td.innerText;		}		if (!td.childNodes) { 			return ""; 		}		var childNodes=td.childNodes;		var ret = "";		for (var i=0,L=childNodes.length; i<L; i++) {			var node = childNodes[i];			var type = node.nodeType;			// In order to get realistic sort results, we need to treat some elements in a special way.			// These behaviors are defined in the nodeValue() object, keyed by node name			if (type==1) {				var nname = node.nodeName;				if (this.nodeValue[nname]) {					ret += this.nodeValue[nname](node);				}				else {					ret += this.getCellValue(node);				}			}			else if (type==3) {				if (def(node.innerText)) {					ret += node.innerText;				}				else if (def(node.nodeValue)) {					ret += node.nodeValue;				}			}		}		return ret;	};	/**	 * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex	 * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2, 	 * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really	 * starts in the second column rather than the first.	 * See: http://www.javascripttoolbox.com/temp/table_cellindex.html	 */	table.tableHeaderIndexes = {};	table.getActualCellIndex = function(tableCellObj) {		if (!def(tableCellObj.cellIndex)) { return null; }		var tableObj = getParent(tableCellObj,"TABLE");		var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);		// If it has already been computed, return the answer from the lookup table		if (def(this.tableHeaderIndexes[tableObj.id])) {			return this.tableHeaderIndexes[tableObj.id][cellCoordinates];      		} 		var matrix = [];		this.tableHeaderIndexes[tableObj.id] = {};		var thead = getParent(tableCellObj,"THEAD");		var trs = thead.getElementsByTagName('TR');		// Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets		// populated with an "x" for each space that a cell takes up. If the first cell is colspan		// 2, it will fill in values [0] and [1] in the first array, so that the second cell will		// find the first empty cell in the first row (which will be [2]) and know that this is		// where it sits, rather than its internal .cellIndex value of [1].		for (var i=0; i<trs.length; i++) {			var cells = trs[i].cells;			for (var j=0; j<cells.length; j++) {				var c = cells[j];				var rowIndex = c.parentNode.rowIndex;				var cellId = rowIndex+"-"+this.getCellIndex(c);				var rowSpan = c.rowSpan || 1;				var colSpan = c.colSpan || 1;				var firstAvailCol;				if(!def(matrix[rowIndex])) { 					matrix[rowIndex] = []; 				}				var m = matrix[rowIndex];				// Find first available column in the first row				for (var k=0; k<m.length+1; k++) {					if (!def(m[k])) {						firstAvailCol = k;						break;					}				}				this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;				for (var k=rowIndex; k<rowIndex+rowSpan; k++) {					if(!def(matrix[k])) { 						matrix[k] = []; 					}					var matrixrow = matrix[k];					for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {						matrixrow[l] = "x";					}				}			}		}		// Store the map so future lookups are fast.		return this.tableHeaderIndexes[tableObj.id][cellCoordinates];	};	/**	 * Sort all rows in each TBODY (tbodies are sorted independent of each other)	 */	table.sort = function(o,args) {		var t, tdata, sortconvert=null;		// Allow for a simple passing of sort type as second parameter		if (typeof(args)=="function") {			args={sorttype:args};		}		args = args || {};		// If no col is specified, deduce it from the object sent in		if (!def(args.col)) { 			args.col = this.getActualCellIndex(o) || 0; 		}		// If no sort type is specified, default to the default sort		args.sorttype = args.sorttype || Sort['default'];		// Resolve the table		t = this.resolve(o,args);		tdata = this.tabledata[t.id];		// If we are sorting on the same column as last time, flip the sort direction		if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) {			tdata.desc = !tdata.lastdesc;		}		else {			tdata.desc = !!args.desc;		}		// Store the last sorted column so clicking again will reverse the sort order		tdata.lastcol=tdata.col;		tdata.lastdesc=!!tdata.desc;		// If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort		var sorttype = tdata.sorttype;		if (typeof(sorttype.convert)=="function") {			sortconvert=tdata.sorttype.convert;			sorttype=Sort.alphanumeric;		}		// Loop through all THEADs and remove sorted class names, then re-add them for the col		// that is being sorted		this.processTableCells(t,"THEAD",			function(cell) {				if (hasClass(cell,this.SortableClassName)) {					removeClass(cell,this.SortedAscendingClassName);					removeClass(cell,this.SortedDescendingClassName);					// If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted					if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) {						addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName);					}				}			}		);		// Sort each tbody independently		var bodies = t.tBodies;		if (bodies==null || bodies.length==0) { return; }		// Define a new sort function to be called to consider descending or not		var newSortFunc = (tdata.desc)?			function(a,b){return sorttype(b[0],a[0]);}			:function(a,b){return sorttype(a[0],b[0]);};		var useinnertext=!!tdata.useinnertext;		var col = tdata.col;		for (var i=0,L=bodies.length; i<L; i++) {			var tb = bodies[i], tbrows = tb.rows, rows = [];			// Allow tbodies to request that they not be sorted			if(!hasClass(tb,table.NoSortClassName)) {				// Create a separate array which will store the converted values and refs to the				// actual rows. This is the array that will be sorted.				var cRow, cRowIndex=0;				if (cRow=tbrows[cRowIndex]){					// Funky loop style because it's considerably faster in IE					do {						if (rowCells = cRow.cells) {							var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null;							if (sortconvert) cellValue = sortconvert(cellValue);							rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];						}					} while (cRow=tbrows[++cRowIndex])				}				// Do the actual sorting				rows.sort(newSortFunc);				// Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!				cRowIndex=0;				var displayedCount=0;				var f=[removeClass,addClass];				if (cRow=rows[cRowIndex]){					do { 						tb.appendChild(cRow[1]); 					} while (cRow=rows[++cRowIndex])				}			}		}		// If paging is enabled on the table, then we need to re-page because the order of rows has changed!		if (tdata.pagesize) {			this.page(t); // This will internally do the striping		}		else {			// Re-stripe if a class name was supplied			if (tdata.stripeclass) {				this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);			}		}	};	/**	* Apply a filter to rows in a table and hide those that do not match.	*/	table.filter = function(o,filters,args) {		var cell;		args = args || {};		var t = this.resolve(o,args);		var tdata = this.tabledata[t.id];		// If new filters were passed in, apply them to the table's list of filters		if (!filters) {			// If a null or blank value was sent in for 'filters' then that means reset the table to no filters			tdata.filters = null;		}		else {			// Allow for passing a select list in as the filter, since this is common design			/// added new class called custom_select - you can have a drop down that selects against a field using regex.			/// as long as the drop down matches any part of the field it will work.			///			if (filters.className=="custom_select" && filters.type=="select-one" && filters.selectedIndex>-1) {				filters={ 'filter':"/"+filters.options[filters.selectedIndex].value+"/"};			}			else if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-1) {				filters={ 'filter':filters.options[filters.selectedIndex].value };			}						// added support for a multiselect			//			// modification by Mike D (11/25/08)			//			if ((filters.className=="custom_select" || filters.nodeName=="SELECT") && filters.type=="select-multiple" && filters.selectedIndex>-1) {							var filter_results = new Array();				var filter_value = "";								for (var i=0; i<filters.options.length; i++) {					if (filters.options[i].selected) {						filter_results[filter_results.length] = filters.options[i].value;					}				}								if(filter_results.length > 0) {					 filter_value = '('+filter_results.join('|')+')'				}				filters={ 'filter':"/"+filter_value+"/"};							}									// Also allow for a regular input			if (filters.nodeName=="INPUT" && filters.type=="text") {				filters={ 'filter':"/"+filters.value+"/" };//				filters={ 'filter':"/^"+filters.value+"/" };			}			// Force filters to be an array			if (typeof(filters)=="object" && !filters.length) {				filters = [filters];			}			// Convert regular expression strings to RegExp objects and function strings to function objects			for (var i=0,L=filters.length; i<L; i++) {				var filter = filters[i];				if (typeof(filter.filter)=="string") {					// If a filter string is like "/expr/" then turn it into a Regex					if (filter.filter.match(/^\/(.*)\/$/)) {/// modded by rbv - added "i" switch to ignore case. 4/7/08						filter.filter = new RegExp(RegExp.$1,"i");//						filter.filter = new RegExp(RegExp.$1);						filter.filter.regex=true;					}					// If filter string is like "function (x) { ... }" then turn it into a function					else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {						filter.filter = Function(RegExp.$1,RegExp.$2);					}				}				// If some non-table object was passed in rather than a 'col' value, resolve it 				// and assign it's column index to the filter if it doesn't have one. This way, 				// passing in a cell reference or a select object etc instead of a table object 				// will automatically set the correct column to filter.				if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) {					filter.col = this.getCellIndex(cell);				}				// Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or ""				if ((!filter || !filter.filter) && tdata.filters) {					delete tdata.filters[filter.col];				}				else {					tdata.filters = tdata.filters || {};					tdata.filters[filter.col] = filter.filter;				}			}			// If no more filters are left, then make sure to empty out the filters object			for (var j in tdata.filters) { var keep = true; }			if (!keep) {				tdata.filters = null;			}		}				// Everything's been setup, so now scrape the table rows		return table.scrape(o);	};	/**	 * "Page" a table by showing only a subset of the rows	 */	table.page = function(t,page,args) {		args = args || {};		if (def(page)) { args.page = page; }		return table.scrape(t,args);	};	/**	 * Jump forward or back any number of pages	 */	table.pageJump = function(t,count,args) {		t = this.resolve(t,args);		return this.page(t,(table.tabledata[t.id].page||0)+count,args);	};	/**	 * Go to the next page of a paged table	 */		table.pageNext = function(t,args) {		return this.pageJump(t,1,args);	};	/**	 * Go to the previous page of a paged table	 */		table.pagePrevious = function(t,args) {		return this.pageJump(t,-1,args);	};	/**	* Scrape a table to either hide or show each row based on filters and paging	*/	table.scrape = function(o,args) {		var col,cell,filterList,filterReset=false,filter;		var page,pagesize,pagestart,pageend;		var unfilteredrows=[],unfilteredrowcount=0,totalrows=0;		var t,tdata,row,hideRow;		args = args || {};		// Resolve the table object		t = this.resolve(o,args);		tdata = this.tabledata[t.id];		// Setup for Paging		var page = tdata.page;		if (def(page)) {			// Don't let the page go before the beginning			if (page<0) { tdata.page=page=0; }			pagesize = tdata.pagesize || 25; // 25=arbitrary default			pagestart = page*pagesize+1;			pageend = pagestart + pagesize - 1;		}		// Scrape each row of each tbody		var bodies = t.tBodies;		if (bodies==null || bodies.length==0) { return; }		for (var i=0,L=bodies.length; i<L; i++) {			var tb = bodies[i];			for (var j=0,L2=tb.rows.length; j<L2; j++) {				row = tb.rows[j];				hideRow = false;				// Test if filters will hide the row				if (tdata.filters && row.cells) {					var cells = row.cells;					var cellsLength = cells.length;					// Test each filter					for (col in tdata.filters) {						if (!hideRow) {							filter = tdata.filters[col];							if (filter && col<cellsLength) {								var val = this.getCellValue(cells[col]);								if (filter.regex && val.search) {									hideRow=(val.search(filter)<0);								}								else if (typeof(filter)=="function") {									hideRow=!filter(val,cells[col]);								}								else {									hideRow = (val!=filter);								}							}						}					}				}				// Keep track of the total rows scanned and the total runs _not_ filtered out				totalrows++;				if (!hideRow) {					unfilteredrowcount++;					if (def(page)) {						// Temporarily keep an array of unfiltered rows in case the page we're on goes past						// the last page and we need to back up. Don't want to filter again!						unfilteredrows.push(row);						if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) {							hideRow = true;						}					}				}				row.style.display = hideRow?"none":"";			}		}		if (def(page)) {			// Check to see if filtering has put us past the requested page index. If it has, 			// then go back to the last page and show it.			if (pagestart>=unfilteredrowcount) {				pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize);				tdata.page = page = pagestart/pagesize;				for (var i=pagestart,L=unfilteredrows.length; i<L; i++) {					unfilteredrows[i].style.display="";				}			}		}		// Loop through all THEADs and add/remove filtered class names		this.processTableCells(t,"THEAD",			function(c) {				((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);			}		);		// Stripe the table if necessary		if (tdata.stripeclass) {			this.stripe(t);		}		// Calculate some values to be returned for info and updating purposes		var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;		if (def(page)) {			// Update the page number/total containers if they exist			if (tdata.container_number) {				tdata.container_number.innerHTML = page+1;			}			if (tdata.container_count) {				tdata.container_count.innerHTML = pagecount;			}		}		// Update the row count containers if they exist		if (tdata.container_filtered_count) {			tdata.container_filtered_count.innerHTML = unfilteredrowcount;		}		if (tdata.container_all_count) {			tdata.container_all_count.innerHTML = totalrows;		}		return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };	};	/**	 * Shade alternate rows, aka Stripe the table.	 */	table.stripe = function(t,className,args) { 		args = args || {};		args.stripeclass = className;		t = this.resolve(t,args);		var tdata = this.tabledata[t.id];		var bodies = t.tBodies;		if (bodies==null || bodies.length==0) { 			return; 		}		className = tdata.stripeclass;		// Cache a shorter, quicker reference to either the remove or add class methods		var f=[removeClass,addClass];		for (var i=0,L=bodies.length; i<L; i++) {			var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0;			if (cRow=tbrows[cRowIndex]){				// The ignorehiddenrows test is pulled out of the loop for a slight speed increase.				// Makes a bigger difference in FF than in IE.				// In this case, speed always wins over brevity!				if (tdata.ignoreHiddenRows) {					do {						f[displayedCount++%2](cRow,className);					} while (cRow=tbrows[++cRowIndex])				}				else {					do {						if (!isHidden(cRow)) {							f[displayedCount++%2](cRow,className);						}					} while (cRow=tbrows[++cRowIndex])				}			}		}	};	/**	 * Build up a list of unique values in a table column	 */	table.getUniqueColValues = function(t,col) {		var values={}, bodies = this.resolve(t).tBodies;		for (var i=0,L=bodies.length; i<L; i++) {			var tbody = bodies[i];			for (var r=0,L2=tbody.rows.length; r<L2; r++) {				values[this.getCellValue(tbody.rows[r].cells[col])] = true;			}		}		var valArray = [];		for (var val in values) {			valArray.push(val);		}		return valArray.sort();	};	/**	 * Scan the document on load and add sorting, filtering, paging etc ability automatically	 * based on existence of class names on the table and cells.	 */	table.auto = function(args) {		var cells = [], tables = document.getElementsByTagName("TABLE");		var val,tdata;		if (tables!=null) {			for (var i=0,L=tables.length; i<L; i++) {				var t = table.resolve(tables[i]);				tdata = table.tabledata[t.id];				if (val=classValue(t,table.StripeClassNamePrefix)) {					tdata.stripeclass=val;				}				// Do auto-filter if necessary				if (hasClass(t,table.AutoFilterClassName)) {					table.autofilter(t);				}				// Do auto-page if necessary				if (val = classValue(t,table.AutoPageSizePrefix)) {					table.autopage(t,{'pagesize':+val});				}				// Do auto-sort if necessary				if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) {					table.autosort(t,{'col':(val==null)?null:+val});				}				// Do auto-stripe if necessary				if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {					table.stripe(t);				}			}		}	};	/**	 * Add sorting functionality to a table header cell	 */	table.autosort = function(t,args) {		t = this.resolve(t,args);		var tdata = this.tabledata[t.id];		this.processTableCells(t, "THEAD", function(c) {			var type = classValue(c,table.SortableColumnPrefix);			if (type!=null) {				type = type || "default";				c.title =c.title || table.AutoSortTitle;				addClass(c,table.SortableClassName);				c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})");				// If we are going to auto sort on a column, we need to keep track of what kind of sort it will be				if (args.col!=null) {					if (args.col==table.getActualCellIndex(c)) {						tdata.sorttype=Sort['"+type+"'];					}				}			}		} );		if (args.col!=null) {			table.sort(t,args);		}	};	/**	 * Add paging functionality to a table 	 */	table.autopage = function(t,args) {		t = this.resolve(t,args);		var tdata = this.tabledata[t.id];		if (tdata.pagesize) {			this.processTableCells(t, "THEAD,TFOOT", function(c) {				var type = classValue(c,table.AutoPageJumpPrefix);				if (type=="next") { type = 1; }				else if (type=="previous") { type = -1; }				if (type!=null) {					c.onclick = Function("","Table.pageJump(this,"+type+")");				}			} );			if (val = classValue(t,table.PageNumberPrefix)) {				tdata.container_number = document.getElementById(val);			}			if (val = classValue(t,table.PageCountPrefix)) {				tdata.container_count = document.getElementById(val);			}			return table.page(t,0,args);		}	};	/**	 * A util function to cancel bubbling of clicks on filter dropdowns	 */	table.cancelBubble = function(e) {		e = e || window.event;		if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); } 		if (def(e.cancelBubble)) { e.cancelBubble = true; }	};	/**	 * Auto-filter a table	 */	table.autofilter = function(t,args) {		args = args || {};		t = this.resolve(t,args);		var tdata = this.tabledata[t.id],val;		table.processTableCells(t, "THEAD", function(cell) {			if (hasClass(cell,table.FilterableClassName)) {				var cellIndex = table.getCellIndex(cell);				var colValues = table.getUniqueColValues(t,cellIndex);				if (colValues.length>0) {					if (typeof(args.insert)=="function") {						func.insert(cell,colValues);					}					else {						var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>';						for (var i=0; i<colValues.length; i++) {							sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>';						}						sel += '</select>';//						cell.innerHTML += "<br>"+sel;						cell.innerHTML += ""+sel;					}				}			}		});		if (val = classValue(t,table.FilteredRowcountPrefix)) {			tdata.container_filtered_count = document.getElementById(val);		}		if (val = classValue(t,table.RowcountPrefix)) {			tdata.container_all_count = document.getElementById(val);		}	};	/**	 * Attach the auto event so it happens on load.	 * use jQuery's ready() function if available	 */	if (typeof(jQuery)!="undefined") {		jQuery(table.auto);	}	else if (window.addEventListener) {		window.addEventListener( "load", table.auto, false );	}	else if (window.attachEvent) {		window.attachEvent( "onload", table.auto );	}	return table;})();