/**
 * 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:"Filter: 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
			if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-1) {
				filters={ 'filter':filters.options[filters.selectedIndex].value };
			}
			// Also allow for a regular input
			if (filters.nodeName=="INPUT" && filters.type=="text") {
				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(/^\/(.*)\/$/)) {
						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;
					}
				}
			}
		});
		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;
})();















/**
 * 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.
 * 
 */
// Global objects to keep track of DynamicOptionList objects created on the page
var dynamicOptionListCount=0;
var dynamicOptionListObjects = new Array();

// Init call to setup lists after page load. One call to this function sets up all lists.
function initDynamicOptionLists() {
	// init each DynamicOptionList object
	for (var i=0; i<dynamicOptionListObjects.length; i++) {
		var dol = dynamicOptionListObjects[i];

		// Find the form associated with this list
		if (dol.formName!=null) { 
			dol.form = document.forms[dol.formName];
		}
		else if (dol.formIndex!=null) {
			dol.form = document.forms[dol.formIndex];
		}
		else {
			// Form wasn't set manually, so go find it!
			// Search for the first form element name in the lists
			var name = dol.fieldNames[0][0];
			for (var f=0; f<document.forms.length; f++) {
				if (typeof(document.forms[f][name])!="undefined") {
					dol.form = document.forms[f];
					break;
				}
			}
			if (dol.form==null) {
				alert("ERROR: Couldn't find form element "+name+" in any form on the page! Init aborted"); return;
			}
		}

		// Form is found, now set the onchange attributes of each dependent select box
		for (var j=0; j<dol.fieldNames.length; j++) {
			// For each set of field names...
			for (var k=0; k<dol.fieldNames[j].length-1; k++) {
				// For each field in the set...
				var selObj = dol.form[dol.fieldNames[j][k]];
				if (typeof(selObj)=="undefined") { alert("Select box named "+dol.fieldNames[j][k]+" could not be found in the form. Init aborted"); return; }
				// Map the HTML options in the first select into the options we created
				if (k==0) {
					if (selObj.options!=null) {
						for (l=0; l<selObj.options.length; l++) {
							var sopt = selObj.options[l];
							var m = dol.findMatchingOptionInArray(dol.options,sopt.text,sopt.value,false);
							if (m!=null) {
								var reselectForNN6 = sopt.selected;
								var m2 = new Option(sopt.text, sopt.value, sopt.defaultSelected, sopt.selected);
								m2.selected = sopt.selected; // For some reason I need to do this to make NN4 happy
								m2.defaultSelected = sopt.defaultSelected;
								m2.DOLOption = m;
								selObj.options[l] = m2;
								selObj.options[l].selected = reselectForNN6; // Reselect this option for NN6 to be happy. Yuck.
							}
						}
					}
				}
				if (selObj.onchange==null) {
					// We only modify the onChange attribute if it's empty! Otherwise do it yourself in your source!
					selObj.onchange = new Function("dynamicOptionListObjects["+dol.index+"].change(this)");
				}
			}
		}
	}
	// Set the preselectd options on page load 
	resetDynamicOptionLists();
}

// This function populates lists with the preselected values. 
// It's pulled out into a separate function so it can be hooked into a 'reset' button on a form
// Optionally passed a form object which should be the only form reset
function resetDynamicOptionLists(theform) {
	// reset each DynamicOptionList object
	for (var i=0; i<dynamicOptionListObjects.length; i++) {
		var dol = dynamicOptionListObjects[i];
		if (typeof(theform)=="undefined" || theform==null || theform==dol.form) {
			for (var j=0; j<dol.fieldNames.length; j++) {
				dol.change(dol.form[dol.fieldNames[j][0]],true); // Second argument says to use preselected values rather than default values
			}
		}
	}
}

// An object to represent an Option() but just for data-holding
function DOLOption(text,value,defaultSelected,selected) {
	this.text = text;
	this.value = value;
	this.defaultSelected = defaultSelected;
	this.selected = selected;
	this.options = new Array(); // To hold sub-options
	return this;
}

// DynamicOptionList CONSTRUCTOR
function DynamicOptionList() {
	this.form = null;// The form this list belongs to
	this.options = new Array();// Holds the options of dependent lists
	this.longestString = new Array();// Longest string that is currently a potential option (for Netscape)
	this.numberOfOptions = new Array();// The total number of options that might be displayed, to build dummy options (for Netscape)
	this.currentNode = null;// The current node that has been selected with forValue() or forText()
	this.currentField = null;// The current field that is selected to be used for setValue()
	this.currentNodeDepth = 0;// How far down the tree the currentNode is
	this.fieldNames = new Array();// Lists of dependent fields which use this object
	this.formIndex = null;// The index of the form to associate with this list
	this.formName = null;// The name of the form to associate with this list
	this.fieldListIndexes = new Object();// Hold the field lists index where fields exist
	this.fieldIndexes = new Object();// Hold the index within the list where fields exist
	this.selectFirstOption = true;// Whether or not to select the first option by default if no options are default or preselected, otherwise set the selectedIndex = -1
	this.numberOfOptions = new Array();// Store the max number of options for a given option list
	this.longestString = new Array();// Store the longest possible string 
	this.values = new Object(); // Will hold the preselected values for fields, by field name
	
	// Method mappings
	this.forValue = DOL_forValue;
	this.forText = DOL_forText;
	this.forField = DOL_forField;
	this.forX = DOL_forX;
	this.addOptions = DOL_addOptions;
	this.addOptionsTextValue = DOL_addOptionsTextValue;
	this.setDefaultOptions = DOL_setDefaultOptions;
	this.setValues = DOL_setValues;
	this.setValue = DOL_setValues;
	this.setFormIndex = DOL_setFormIndex;
	this.setFormName = DOL_setFormName;
	this.printOptions = DOL_printOptions;
	this.addDependentFields = DOL_addDependentFields;
	this.change = DOL_change;
	this.child = DOL_child;
	this.selectChildOptions = DOL_selectChildOptions;
	this.populateChild = DOL_populateChild;
	this.change = DOL_change;
	this.addNewOptionToList = DOL_addNewOptionToList;
	this.findMatchingOptionInArray = DOL_findMatchingOptionInArray;

	// Optionally pass in the dependent field names
	if (arguments.length > 0) {
		// Process arguments and add dependency groups
		for (var i=0; i<arguments.length; i++) {
			this.fieldListIndexes[arguments[i].toString()] = this.fieldNames.length;
			this.fieldIndexes[arguments[i].toString()] = i;
		}
		this.fieldNames[this.fieldNames.length] = arguments;
	}
	
	// Add this object to the global array of dynamicoptionlist objects
	this.index = window.dynamicOptionListCount++;
	window["dynamicOptionListObjects"][this.index] = this;
}

// Given an array of Option objects, search for an existing option that matches value, text, or both
function DOL_findMatchingOptionInArray(a,text,value,exactMatchRequired) {
	if (a==null || typeof(a)=="undefined") { return null; }
	var value_match = null; // Whether or not a value has been matched
	var text_match = null; // Whether or not a text has been matched
	for (var i=0; i<a.length; i++) {
		var opt = a[i];
		// If both value and text match, return it right away
		if (opt.value==value && opt.text==text) { return opt; }
		if (!exactMatchRequired) {
			// If value matches, store it until we complete scanning the list
			if (value_match==null && value!=null && opt.value==value) {
				value_match = opt;
			}
			// If text matches, store it for later
			if (text_match==null && text!=null && opt.text==text) {
				text_match = opt;
			}
		}
	}
	return (value_match!=null)?value_match:text_match;
}

// Util function used by forValue and forText
function DOL_forX(s,type) {
	if (this.currentNode==null) { this.currentNodeDepth=0; }
	var useNode = (this.currentNode==null)?this:this.currentNode;
	var o = this.findMatchingOptionInArray(useNode["options"],(type=="text")?s:null,(type=="value")?s:null,false);
	if (o==null) {
		o = new DOLOption(null,null,false,false);
		o[type] = s;
		useNode.options[useNode.options.length] = o;
	}
	this.currentNode = o;
	this.currentNodeDepth++;
	return this;
}

// Set the portion of the list structure that is to be used by a later operation like addOptions
function DOL_forValue(s) { return this.forX(s,"value"); }

// Set the portion of the list structure that is to be used by a later operation like addOptions
function DOL_forText(s) { return this.forX(s,"text"); }

// Set the field to be used for setValue() calls
function DOL_forField(f) { this.currentField = f; return this; }

// Create and add an option to a list, avoiding duplicates
function DOL_addNewOptionToList(a, text, value, defaultSelected) {
	var o = new DOLOption(text,value,defaultSelected,false);
	// Add the option to the array
	if (a==null) { a = new Array(); }
	for (var i=0; i<a.length; i++) {
		if (a[i].text==o.text && a[i].value==o.value) {
			if (o.selected) { 
				a[i].selected=true;
			}
			if (o.defaultSelected) {
				a[i].defaultSelected = true;
			}
			return a;
		}
	}
	a[a.length] = o;
}

// Add sub-options to the currently-selected node, with the same text and value for each option
function DOL_addOptions() {
	if (this.currentNode==null) { this.currentNode = this; }
	if (this.currentNode["options"] == null) { this.currentNode["options"] = new Array(); }
	for (var i=0; i<arguments.length; i++) {
		var text = arguments[i];
		this.addNewOptionToList(this.currentNode.options,text,text,false);
		if (typeof(this.numberOfOptions[this.currentNodeDepth])=="undefined") {
			this.numberOfOptions[this.currentNodeDepth]=0;
		}
		if (this.currentNode.options.length > this.numberOfOptions[this.currentNodeDepth]) {
			this.numberOfOptions[this.currentNodeDepth] = this.currentNode.options.length;
		}
		if (typeof(this.longestString[this.currentNodeDepth])=="undefined" || (text.length > this.longestString[this.currentNodeDepth].length)) {
			this.longestString[this.currentNodeDepth] = text;
		}
	}
	this.currentNode = null;
	this.currentNodeDepth = 0;
}

// Add sub-options to the currently-selected node, specifying separate text and values for each option
function DOL_addOptionsTextValue() {
	if (this.currentNode==null) { this.currentNode = this; }
	if (this.currentNode["options"] == null) { this.currentNode["options"] = new Array(); }
	for (var i=0; i<arguments.length; i++) {
		var text = arguments[i++];
		var value = arguments[i];
		this.addNewOptionToList(this.currentNode.options,text,value,false);
		if (typeof(this.numberOfOptions[this.currentNodeDepth])=="undefined") {
			this.numberOfOptions[this.currentNodeDepth]=0;
		}
		if (this.currentNode.options.length > this.numberOfOptions[this.currentNodeDepth]) {
			this.numberOfOptions[this.currentNodeDepth] = this.currentNode.options.length;
		}
		if (typeof(this.longestString[this.currentNodeDepth])=="undefined" || (text.length > this.longestString[this.currentNodeDepth].length)) {
			this.longestString[this.currentNodeDepth] = text;
		}
	}
	this.currentNode = null;
	this.currentNodeDepth = 0;
}

// Find the first dependent list of a select box
// If it's the last list in a chain, return null because there are no children
function DOL_child(obj) {
	var listIndex = this.fieldListIndexes[obj.name];
	var index = this.fieldIndexes[obj.name];
	if (index < (this.fieldNames[listIndex].length-1)) {
		return this.form[this.fieldNames[listIndex][index+1]];
	}
	return null;
}

// Set the options which should be selected by default for a certain value in the parent
function DOL_setDefaultOptions() {
	if (this.currentNode==null) { this.currentNode = this; }
	for (var i=0; i<arguments.length; i++) {
		var o = this.findMatchingOptionInArray(this.currentNode.options,null,arguments[i],false);
		if (o!=null) {
			o.defaultSelected = true;
		}
	}
	this.currentNode = null;
}

// Set the options which should be selected when the page loads. This is different than the default value and ONLY applies when the page LOADS
function DOL_setValues() {
	if (this.currentField==null) { 
		alert("Can't call setValues() without using forField() first!");
		return;
	}
	if (typeof(this.values[this.currentField])=="undefined") {
		this.values[this.currentField] = new Object();
	}
	for (var i=0; i<arguments.length; i++) {
		this.values[this.currentField][arguments[i]] = true;
	}
	this.currentField = null;
}

// Manually set the form for the object using an index
function DOL_setFormIndex(i) {
	this.formIndex = i;
}

// Manually set the form for the object using a form name
function DOL_setFormName(n) {
	this.formName = n;
}

// Print blank <option> objects for Netscape4, since it refuses to grow or shrink select boxes for new options
function DOL_printOptions(name) {
	// Only need to write out "dummy" options for Netscape4
    if ((navigator.appName == 'Netscape') && (parseInt(navigator.appVersion) <= 4)){
		var index = this.fieldIndexes[name];
		var ret = "";
		if (typeof(this.numberOfOptions[index])!="undefined") {
			for (var i=0; i<this.numberOfOptions[index]; i++) { 
				ret += "<OPTION>";
			}
		}
		ret += "<OPTION>";
		if (typeof(this.longestString[index])!="undefined") {
			for (var i=0; i<this.longestString[index].length; i++) {
				ret += "_";
			}
		}
		document.writeln(ret);
	}
}

// Add a list of field names which use this option-mapping object.
// A single mapping object may be used by multiple sets of fields
function DOL_addDependentFields() {
	for (var i=0; i<arguments.length; i++) {
		this.fieldListIndexes[arguments[i].toString()] = this.fieldNames.length;
		this.fieldIndexes[arguments[i].toString()] = i;
	}
	this.fieldNames[this.fieldNames.length] = arguments;
}

// Called when a parent select box is changed. It populates its direct child, then calls change on the child object to continue the population.
function DOL_change(obj, usePreselected) {
	if (usePreselected==null || typeof(usePreselected)=="undefined") { usePreselected = false; }
	var changedListIndex = this.fieldListIndexes[obj.name];
	var changedIndex = this.fieldIndexes[obj.name];
	var child = this.child(obj);
	if (child == null) { return; } // No child, no need to continue
	if (obj.type == "select-one") {
		// Treat single-select differently so we don't have to scan the entire select list, which could potentially speed things up
		if (child.options!=null) {
			child.options.length=0; // Erase all the options from the child so we can re-populate
		}
		if (obj.options!=null && obj.options.length>0 && obj.selectedIndex>=0) {
			var o = obj.options[obj.selectedIndex];
			this.populateChild(o.DOLOption,child,usePreselected);
			this.selectChildOptions(child,usePreselected);
		}
	}
	else if (obj.type == "select-multiple") {
		// For each selected value in the parent, find the options to fill in for this list
		// Loop through the child list and keep track of options that are currently selected
		var currentlySelectedOptions = new Array();
		if (!usePreselected) {
			for (var i=0; i<child.options.length; i++) {
				var co = child.options[i];
				if (co.selected) {
					this.addNewOptionToList(currentlySelectedOptions, co.text, co.value, co.defaultSelected);
				}
			}
		}
		child.options.length=0;
		if (obj.options!=null) {
			var obj_o = obj.options;
			// For each selected option in the parent...
			for (var i=0; i<obj_o.length; i++) {
				if (obj_o[i].selected) {
					// if option is selected, add its children to the list
 					this.populateChild(obj_o[i].DOLOption,child,usePreselected);
				}
			}
			// Now go through and re-select any options which were selected before
			var atLeastOneSelected = false;
			if (!usePreselected) {
				for (var i=0; i<child.options.length; i++) {
					var m = this.findMatchingOptionInArray(currentlySelectedOptions,child.options[i].text,child.options[i].value,true);
					if (m!=null) {
						child.options[i].selected = true;
						atLeastOneSelected = true;
					}
				}
			}
			if (!atLeastOneSelected) {	
				this.selectChildOptions(child,usePreselected);
			}
		}
	}
	// Change all the way down the chain
	this.change(child,usePreselected);
}
function DOL_populateChild(dolOption,childSelectObj,usePreselected) {
	// If this opton has sub-options, populate the child list with them
	if (dolOption!=null && dolOption.options!=null) {
		for (var j=0; j<dolOption.options.length; j++) {
			var srcOpt = dolOption.options[j];
			if (childSelectObj.options==null) { childSelectObj.options = new Array(); }
			// Put option into select list
			var duplicate = false;
			var preSelectedExists = false;
			for (var k=0; k<childSelectObj.options.length; k++) {
				var csi = childSelectObj.options[k];
				if (csi.text==srcOpt.text && csi.value==srcOpt.value) {
					duplicate = true;
					break;
				}
			}
			if (!duplicate) {
				var newopt = new Option(srcOpt.text, srcOpt.value, false, false);
				newopt.selected = false; // Again, we have to do these two statements for NN4 to work
				newopt.defaultSelected = false;
				newopt.DOLOption = srcOpt;
				childSelectObj.options[childSelectObj.options.length] = newopt;
			}
		}
	}
}

// Once a child select is populated, go back over it to select options which should be selected
function DOL_selectChildOptions(obj,usePreselected) {
	// Look to see if any options are preselected=true. If so, then set then selected if usePreselected=true, otherwise set defaults
	var values = this.values[obj.name];
	var preselectedExists = false;
	if (usePreselected && values!=null && typeof(values)!="undefined") {
		for (var i=0; i<obj.options.length; i++) {
			var v = obj.options[i].value;
			if (v!=null && values[v]!=null && typeof(values[v])!="undefined") {
				preselectedExists = true;
				break;
			}
		}
	}
	// Go back over all the options to do the selection
	var atLeastOneSelected = false;
	for (var i=0; i<obj.options.length; i++) {
		var o = obj.options[i];
		if (preselectedExists && o.value!=null && values[o.value]!=null && typeof(values[o.value])!="undefined") {
			o.selected = true;
			atLeastOneSelected = true;
		}
		else if (!preselectedExists && o.DOLOption!=null && o.DOLOption.defaultSelected) {
			o.selected = true;
			atLeastOneSelected = true;
		}
		else {
			o.selected = false;
		}
	}
	// If nothing else was selected, select the first one by default
	if (this.selectFirstOption && !atLeastOneSelected && obj.options.length>0) {
		obj.options[0].selected = true;
	}
	else if (!atLeastOneSelected &&  obj.type=="select-one") {
		obj.selectedIndex = -1;
	}
}















// ===================================================================
// Author: Matt Kruse <matt@mattkruse.com>
// WWW: http://www.mattkruse.com/
//
// NOTICE: You may use this code for any purpose, commercial or
// private, without any further permission from the author. You may
// remove this notice from your final code if you wish, however it is
// appreciated by the author if at least my web site address is kept.
//
// You may *NOT* re-distribute this code in any way except through its
// use. That means, you can include it in your product, or your web
// site, or any other form where the code is actually being used. You
// may not put the plain javascript up on your site for download or
// include it in your javascript libraries for download. 
// If you wish to share this code with others, please just point them
// to the URL instead.
// Please DO NOT link directly to my .js files from your site. Copy
// the files to your server and use them there. Thank you.
// ===================================================================

// HISTORY
// ------------------------------------------------------------------
// February 29, 2004: Fixed bug caused by LAST update, when control
//    checkbox is the first on checked. I hate when that happens.
// January 28, 2004: Fixed bug that occurred when checkbox was CHECKED
//    by default.
// December 16, 2002: Created
/* 
Original coding done by David Rogers - http://www.MrDaveR.com/

DESCRIPTION: This library lets you quickly and easily make multiple checkboxes
behave as a group by limiting the total number of boxes that can be checked
and/or have a master control checkbox. 

COMPATABILITY: Should work on all Javascript-compliant browsers.

USAGE:
// Create a new CheckBoxGroup object
var myOptions = new CheckBoxGroup();

// Tell the object which checkboxes exist in your group. You may make multiple
// calls to this function, and/or pass multiple arguments. You may specify
// field names exactly, or use a wildcard at the beginning or end of the 
// name.
myOptions.addToGroup("cb1","cb2","optionset*");
myOptions.addToGroup("*checkbox");

// Optionally set a "control" box which will effect all other boxes.
myOptions.setControlBox("masterCheckboxName");

// Specify how your control box will interact with the group. Options are
// either "some" or "all" ("all" is default).
// all: Checking the box will select all the other boxes. Unchecking it will
//      uncheck all the group checkboxes. Selecting all the checkboxes in
//      the group will automatically check the control box.
// some: Checking any checkbox in the group will check the control box. The
//       control box may not be unchecked if any option in the group is 
//       still checked. If no boxes in the group are checked, you may still
//       check the control box.
myOptions.setMasterBehavior("some");

// Optionally set the maximum number of boxes in the group which are allowed
// to be checked. You may pass a second argument which is an alert message to
// be displayed to the user if they exceed this limit.
myOptions.setMaxAllowed(3);
myOptions.setMaxAllowed(3,"You may only select 3 choices!");

// IMPORTANT: After defining the group and behavior, you must insert an action
// into EVERY checkbox's onClick event-handler. Just specify the name of the
// group object that the checkbox belongs to, and pass it 'this' as the argument.
<INPUT TYPE="checkbox" NAME="cb1" onClick="myOptions.check(this)">

// That's it! Your checkboxes will now behave as a group!

*/ 
function CheckBoxGroup() {
	this.controlBox=null;
	this.controlBoxChecked=null;
	this.maxAllowed=null;
	this.maxAllowedMessage=null;
	this.masterBehavior=null;
	this.formRef=null;
	this.checkboxWildcardNames=new Array();
	this.checkboxNames=new Array();
	this.totalBoxes=0;
	this.totalSelected=0;
	// Public methods
	this.setControlBox=CBG_setControlBox;
	this.setMaxAllowed=CBG_setMaxAllowed;
	this.setMasterBehavior=CBG_setMasterBehavior;	// all, some
	this.addToGroup=CBG_addToGroup;
	// Private methods
	this.expandWildcards=CBG_expandWildcards;
	this.addWildcardCheckboxes=CBG_addWildcardCheckboxes;
	this.addArrayCheckboxes=CBG_addArrayCheckboxes;
	this.addSingleCheckbox=CBG_addSingleCheckbox;
	this.check=CBG_check;
	}

// Set the master control checkbox name
function CBG_setControlBox(name) { this.controlBox=name; }

// Set the maximum number of checked boxes in the set, and optionally
// the message to popup when the max is reached.
function CBG_setMaxAllowed(num,msg) {
	this.maxAllowed=num;
	if (msg!=null&&msg!="") { this.maxAllowedMessage=msg; }
	}

// Set the behavior for the checkbox group master checkbox
//	All: all boxes must be checked for the master to be checked
//	Some: one or more of the boxes can be checked for the master to be checked
function CBG_setMasterBehavior(b) { this.masterBehavior = b.toLowerCase(); }

// Add checkbox wildcards to the checkboxes array
function CBG_addToGroup() {
	if (arguments.length>0) {
		for (var i=0;i<arguments.length;i++) {
			this.checkboxWildcardNames[this.checkboxWildcardNames.length]=arguments[i];
			}
		}
	}

// Expand the wildcard checkbox names given in the addToGroup method
function CBG_expandWildcards() {
	if (this.formRef==null) {alert("ERROR: No form element has been passed.  Cannot extract form name!"); return false; }
	for (var i=0; i<this.checkboxWildcardNames.length;i++) {
		var n = this.checkboxWildcardNames[i];
		var el = this.formRef[n];
		if (n.indexOf("*")!=-1) { this.addWildcardCheckboxes(n); }
		else if(CBG_nameIsArray(el)) { this.addArrayCheckboxes(n); }
		else { this.addSingleCheckbox(el); }
		}
	}


// Add checkboxes to the group which match a pattern
function CBG_addWildcardCheckboxes(name) {
	var i=name.indexOf("*");
	if ((i==0) || (i==name.length-1)) {
		searchString= (i)?name.substring(0,name.length-1):name.substring(1,name.length);
		for (var j=0;j<this.formRef.length;j++) {
			currentElement = this.formRef.elements[j];
			currentElementName=currentElement.name;
			var partialName = (i)?currentElementName.substring(0,searchString.length) : currentElementName.substring(currentElementName.length-searchString.length,currentElementName.length);
			if (partialName==searchString) {
				if(CBG_nameIsArray(currentElement)) this.addArrayCheckboxes(currentElement);
				else this.addSingleCheckbox(currentElement);
				}
			}
		}
	}

// Add checkboxes to the group which all have the same name
function CBG_addArrayCheckboxes(name) {
	if((CBG_nameIsArray(this.formRef[name])) && (this.formRef[name].length>0)) {
		for (var i=0; i<this.formRef[name].length; i++) { this.addSingleCheckbox(this.formRef[name][i]); }
		}
	}

function CBG_addSingleCheckbox(obj) {
	if (obj != this.formRef[this.controlBox]) {
		this.checkboxNames[this.checkboxNames.length]=obj;
		this.totalBoxes++;
		if (obj.checked) {
			this.totalSelected++;
			}
		}
	}

// Runs whenever a checkbox in the group is clicked
function CBG_check(obj) {
	var checked=obj.checked;
	if (this.formRef==null) {
		this.formRef=obj.form;
		this.expandWildcards();
		if (this.controlBox==null || obj.name!=this.controlBox) {
			this.totalSelected += (checked)?-1:1;
			}
		}
	if (this.controlBox!=null&&obj.name==this.controlBox) {
		if (this.masterBehavior=="all") {
			for (i=0;i<this.checkboxNames.length;i++) { this.checkboxNames[i].checked=checked; }
			this.totalSelected=(checked)?this.checkboxNames.length:0;
			}
		else {
			if (!checked) {
				obj.checked = (this.totalSelected>0)?true:false;
				obj.blur();
				}
			}
		}
	else {
		if (this.masterBehavior=="all") {
			if (!checked) {
				this.formRef[this.controlBox].checked=false;
				this.totalSelected--;
				}
			else { this.totalSelected++; }
			if (this.controlBox!=null) {
				this.formRef[this.controlBox].checked=(this.totalSelected==this.totalBoxes)?true:false;
				}
			}
		else {
			if (!obj.checked) { this.totalSelected--; }	
			else { this.totalSelected++; }
			if (this.controlBox!=null) {
				this.formRef[this.controlBox].checked=(this.totalSelected>0)?true:false;
				}
			if (this.maxAllowed!=null) {
				if (this.totalSelected>this.maxAllowed) {
					obj.checked=false;
					this.totalSelected--;
					if (this.maxAllowedMessage!=null) { alert(this.maxAllowedMessage); }
					return false;
					}
				}
			}
		}
	}

function CBG_nameIsArray(obj) {
	return ((typeof obj.type!="string")&&(obj.length>0)&&(obj[0]!=null)&&(obj[0].type=="checkbox"));
	}
