function Grid(clientId, aSourceDir, aKeys, colCount, reorder, sel, singleSel)
{
	var prefix = clientId || "";
	var sourceDir = aSourceDir || "";
	var keys = aKeys || [];
	var userColumnCount = colCount;
	var showReorderColumn = reorder;
	var showSelectionColumn = sel;
	var showSingleSelectionColumn = singleSel;
	var printWindow = null;
	
	var field = function(name)
	{
		return Dom.get(prefix + name);
	};
	
	var submitBtn = field("submit"); // indicates that postback source is this grid <type=submit>
	var cmdBox = field("cmd"); // command that is to be executed <type=hidden>
	var cmdArgBox = field("cmdarg"); // command argument <type=hidden>
	var searchDD = field("searchDD"); // search dropdown <select>
	var searchBox = field("searchBox"); // search keyword textbox <type=text>
	var gridTable = field("body"); // grid table <table>
	var titleDiv = field("titleDiv"); // title <div>
	var searchMode = field("searchmode"); // whether grid is in search mode or show all records mode <type=hidden>
	var sortCol = field("sortcol"); // sort column <type=hidden>
	var sortDir = field("sortdir"); // sort direction <type=hidden>
	var curPage = field("curpage"); // current page <type=hidden>
	var totPage = field("totpage"); // total page <type=hidden>
	var cbxList = null; // checkbox list
	var rbList = null; // radio button list

	if(showSingleSelectionColumn)
	{
		rbList = Dom.getlist(prefix + "singleKey");
		if(rbList == null) rbList = [];
		
		this.getSelectedKey = function()
		{
			for(var i=0; i<rbList.length; i++)
			{
				if(rbList[i].checked)
					return rbList[i].value;
			}
			return null;
		};
		
		this.disableRadioButtons = function(values)
		{
			for(var i=0; i=values.length; i++)
			{
				for(var j=0; j=rbList.length; j++)
				{
					if(rbList[j].value == values[i])
						rbList[j].disabled = true;
				}
			}
		};
	}
	else
	{
		this.getSelectedKey = function() { return null; };
		this.disableRadioButtons = function(values) { return false; };
	}
	
	if(showSelectionColumn)
	{
		cbxList = Dom.getlist(prefix + "selectedKeys");
		this.getSelectedKeys = function()
		{
			var v = [];
			for(var i=0; i<cbxList.length; i++)
			{
				if(cbxList[i].checked)
					v.push(cbxList[i].value);
			}
			if(v.length == 0)
				v = null;
			return v;
		};
		
		this.checkAll = function(b)
		{
			for(var i=0; i<cbxList.length; i++)
			{
				cbxList[i].checked = b;
			}
		};

		this.disableCheckboxes = function(values)
		{
			for(var i=0; i<values.length; i++)
			{
				for(var j=0; j<cbxList.length; j++)
				{
					if(cbxList[j].value == values[i])
					{
						cbxList[j].disabled = true;
						break;
					}
				}
			}
		};
	}
	else //(!showSelectionColumn && !showSingleSelectionColumn)
	{
		this.getSelectedKeys = function() { return null; };
		this.checkAll = function(b) { return false; };
		this.disableCheckboxes = function(values) { return false; };
	}
	
	var get_row = function(key)
	{
		var idx = keys.indexOf(key);
		if(idx < 0)
			return null;
		var rows = gridTable.tBodies[0].rows;
		if(idx > rows.length - 1)
			return null;
		return rows[i];
	};
	
	var fix_rows_class_names = function()
	{
		var rows = gridTable.tBodies[0].rows;
		var flag = false;
		for(var i=0; i<rows.length; i++)
		{
			rows[i].className = (flag) ? "gridrow" : "altgridrow";
			flag = !flag;
		}
	};
	
	var submit = function(cmd, arg)
	{
		cmdBox.value = (cmd) ? cmd : "";
		cmdArgBox.value = (arg) ? arg : "";
		submitBtn.click();
	};
	
	this.getNumberOfColumns = function()
	{
		return userColumnCount;
	};
	
	this.addRow = function(key, values)
	{
		if(!key || key == "" || !values || values.length == 0)
			return;
		var tbody = gridTable.tBodies[0];
		var class_name = (tbody.rows[tbody.rows.length - 1].className == "gridrow") ? "altgridrow" : "gridrow";
		var row = tbody.insertRow(tbody.rows.length);
		row.className = class_name;
		row.grid = this;
		row.onmouseover = function() { this.grid.high(this); };
		row.onmouseout = function() { this.grid.low(this); };
		for(var i=0; i<userColumnCount; i++)
		{
			var v = values[i];
			if(!v || v == "")
				v = "&nbsp;";
			var cell = row.insertCell(i);
			cell.innerHTML = v;
		}
		var current = userColumnCount;
		if(showReorderColumn)
		{
			var cell = row.insertCell(current);
			current = current + 1;
			var paths = ["/img/top.gif", "/img/up.gif", "/img/down.gif", "/img/bottom.gif"];
			var dir = ["top", "up", "down", "bottom"];
			for(var i=0; i<paths.length; i++)
			{
				var img = new Image();
				img.src = sourceDir + paths[i];
				img.className = "imagebtn";
				img.grid = this;
				img.where = dir[i];
				img.key = key;
				img.onclick = function() { this.grid.move(this.where, this.key); };
				cell.appendChild(img);
			}
		}
		if(showSelectionColumn)
		{
			var cell = row.insertCell(current);
			current = current + 1;
			var cb = document.createElement("input");
			cb.type = "checkbox";
			cb.name = prefix + "selectedKeys";
			cb.value = key;
			cell.appendChild(cb);
		}
		if(showSingleSelectionColumn)
		{
			var cell = row.insertCell(current);
			current = current + 1;
			var rb = document.createElement("input");
			rb.type = "radio";
			rb.name = prefix + "singleKey";
			rb.value = key;
			cell.appendChild(rb);
		}
		keys.push(key);
	};
	
	this.removeRow = function(key)
	{
		var idx = keys.indexOf(key);
		if(idx < 0)
			return;
		keys.splice(idx, 1);
		gridTable.tBodies[0].deleteRow(idx);
	};
	
	this.postback = function(cmd, arg)
	{
		cmdBox.value = (cmd) ? cmd : "";
		cmdArgBox.value = (arg) ? arg : "";
		submitBtn.value = "2";
		submitBtn.click();
	};
	
	this.searchHandler = null;
	
	this.search = function()
	{
		var v = searchBox.value.trim();
		if(v == "")
		{
			searchMode.value = "0";
			return false;
		}
		searchMode.value = "1";
		if(this.searchHandler != null && typeof(this.searchHandler) == "function")
		{
			var x = this.searchHandler(v);
			if(x == false)
				return false;
		}
		submit("search", v);
	};
	
	this.showAllHandler = null;
	
	this.showAll = function()
	{
		searchMode.value = "0";
		if(this.showAllHandler != null && typeof(this.showAllHandler) == "function")
		{
			var x = this.showAllHandler();
			if(x == false)
				return false;
		}
		submit("showall", "");
	};
	
	this.sortHandler = null;
	
	this.sort = function(sortExpr)
	{
		if(sortExpr == "")
			return false;
		if(sortCol.value == sortExpr)
			sortDir.value = (sortDir.value == "asc") ? "desc" : "asc";
		else
			sortCol.value = sortExpr;
		if(this.sortHandler != null && typeof(this.sortHandler) == "function")
		{
			var x = this.sortHandler(sortCol.value, sortDir.value);
			if(x == false)
				return false;
		}
		submit("sort", sortExpr);
	};
	
	this.moveHandler = null;
	
	this.move = function(dir, key)
	{
		if(this.moveHandler != null && typeof(this.moveHandler) == "function")
		{
			var x = this.moveHandler(dir, key);
			if(x == false)
				return false;
		}
		submit(dir, key);
	};
	
	this.newItemHandler = null;
	
	this.newItem = function()
	{
		if(this.newItemHandler != null && typeof(this.newItemHandler) == "function")
		{
			var x = this.newItemHandler();
			if(x == false)
				return false;
		}
		submit("new", "");
	};
	
	this.copyItemHandler = null;
	
	this.copyItem = function()
	{
		var list = this.getSelectedKeys();
		var singleKey = this.getSelectedKey();
		var x;
		if(singleKey) x = singleKey;
		else if(list != null && list.length > 0) x = list[0];
		if(!x)
		{
			alert("Please select an item to copy from.");
			return false;
		}
		if(singleKey == null && list.length > 1)
		{
			alert("It appears that you have selected more than one item.\nPlease select only one item and try again.");
			return false;
		}
		if(this.copyItemHandler != null && typeof(this.copyItemHandler) == "function")
		{
			var y = this.copyItemHandler(x);
			if(y == false)
				return false;
		}
		submit("copy", x);
	};
	
	this.editItemHandler = null;
	
	this.editItem = function()
	{
		var list = this.getSelectedKeys();
		var singleKey = this.getSelectedKey();
		var x;
		if(singleKey) x = singleKey;
		else if(list != null && list.length > 0) x = list[0];
		if(!x)
		{
			alert("Please select an item to edit.");
			return false;
		}
		if(singleKey == null && list.length > 1)
		{
			alert("It appears that you have selected more than one item.\nPlease select only one item try again.");
			return false;
		}
		if(this.editItemHandler != null && typeof(this.editItemHandler) == "function")
		{
			var y = this.editItemHandler(x);
			if(y == false)
				return false;
		}
		submit("edit", x);
	};
	
	this.editItemDirect = function(key)
	{
		submit("edit", key);
	};
	
	this.deleteItemHandler = null;
	
	this.deleteItem = function()
	{
		var list = this.getSelectedKeys();
		var singleKey = this.getSelectedKey();
		if(list || singleKey)
		{
			var ok = confirm("Are you sure you would like to delete selected item(s)?");
			if(ok)
			{
				if(this.deleteItemHandler != null && typeof(this.deleteItemHandler) == "function")
				{
					var y;
					if(singleKey != null) y = this.deleteItemHandler([singleKey]);
					else if(list != null) y = this.deleteItemHandler(list);
					if(y == false)
						return false;
				}
				var v;
				if(singleKey != null) v = singleKey;
				else if(list != null) v = list.join(",");
				submit("delete", v);
			}
		}
		else 
			alert("It appears that you have not selected any items.\nPlease select items that you would like to delete and try again.");
	};
	
	this.prevPageHandler = null;
	
	this.prevPage = function()
	{
		var c = parseInt(curPage.value);
		if(c <= 1)
			return false;
		c = c - 1;
		curPage.value = c.toString();
		if(this.prevPageHandler != null && typeof(this.prevPageHandler) == "function")
		{
			var x = this.prevPageHandler(c);
			if(x == false)
				return false;
		}
		submit("prev");
	};
	
	this.nextPageHandler = null;
	
	this.nextPage = function()
	{
		var c = parseInt(curPage.value);
		var t = parseInt(totPage.value);
		if(c >= t)
			return false;
		c = c + 1;
		curPage.value = c.toString();
		if(this.nextPageHandler != null && typeof(this.nextPageHandler) == "function")
		{
			var x = this.nextPageHandler(c);
			if(x == false)
				return false;
		}
		submit("next");
	};
	
	this.refresh = function(cmd)
	{
		submit(cmd);
	};
	
	this.print = function(e)
	{
		e = e || window.event;
		var src = e.srcElement || e.target;
		if(src) src.style.visibility = "hidden";
		
		if(printWindow && !printWindow.closed)
		{
			printWindow.close();
		}
		var url = sourceDir + "/printgrid.html";
		printWindow = window.open(url, prefix + "PrintWindow", "location=0,status=0,menubar=0,resizable=1,scrollbars=1");
		printWindow.gridTitle = titleDiv.innerHTML;
		printWindow.gridTable = gridTable;
		
		if(src) src.style.visibility = "";
	};
	
	this.rowMouseOverHandler;
	this.high = function(row, e)
	{
		row.oldClassName = row.className;
		row.className = "higridrow";
		e = e || self.event;
		if(this.rowMouseOverHandler != null)
			this.rowMouseOverHandler(row, e);
	};
	
	this.rowMouseOutHandler;
	this.low = function(row, e)
	{
		row.className = row.oldClassName;
		row.oldClassName = "";
		e = e || self.event;
		if(this.rowMouseOutHandler != null)
			this.rowMouseOutHandler(row, e);
	};
}


Grid.__doc__ = function()
{
	var buf = new StrBuf("<b><u>JavaScript Grid API</u></b><br><br>");
	buf.append("<b>getSelectedKeys()</b> - returns a list of selected keys (checked checkboxes)<br><br>");
	buf.append("<b>getNumberOfColumns()</b> - returns the number of user columns in the grid.<br><br>");
	buf.append("<b>addRow(key, values)</b> - adds a row to the table.<br>");
	buf.append("key - row key.<br>");
	buf.append("values - array of string values (could be HTML). Each string will get its own table cell. Bound by the number of user columns in the grid.<br><br>");
	buf.append("<b>removeRow(key)</b> - removes table row with given key.<br><br>");
	buf.append("<b>postback(cmd, cmdArg)</b> - causes a postback.<br>");
	buf.append("cmd - command name.<br>");
	buf.append("cmdArg - command argument.<br><br>");
	buf.append("<b>search()</b> - causes a search postback.<br><br>");
	buf.append("<b>showAll()</b> - causes a show all records postback.<br><br>");
	buf.append("<b>sort(sortExpr)</b> - causes a sort postback.<br>");
	buf.append("sortExpr - sort expression.<br><br>");
	buf.append("<b>move(dir, key)</b> - causes a move postback.<br>");
	buf.append("dir - move direction [top, up, down, bottom].<br>");
	buf.append("key - row key.<br><br>");
	buf.append("<b>newItem()</b> - causes a new item postback.<br><br>");
	buf.append("<b>copyItem()</b> - causes a copy item postback. Selection column should be visible and one value should be selected.<br><br>");
	buf.append("<b>editItem()</b> - causes an edit item postback. Selection column should be visible and one value should be selected.<br><br>");
	buf.append("<b>editItemDirect(key)</b> - causes an edit item postback.<br>");
	buf.append("key - row key.<br><br>");
	buf.append("<b>deleteItem()</b> - causes a delete item postback.<br><br>");
	buf.append("<b>print()</b> - opens grid in printable format in a new window.<br><br>");
	buf.append("<b>disableCheckboxes(values)</b> - disables checkboxes with specified keys. Only available when selection column is visible.<br>");
	buf.append("values - array of item (row) keys.<br><br>");
	buf.append("<b>getSelectedKeys()</b> - returns array of selected item keys. Only available when selection column is visible.<br>");
	buf.append("<b>checkAll(b)</b> - checks all checkboxes in the grid. Only available when selection column is visible.<br>");
	buf.append("b - true or false<br><br>");
	//buf.append("<b>getSelectedKey()</b> - returns the selected key in single selection column. Only available if single selection column is visible.<br><br>");
	buf.append("<b>disableRadioButtons(values)</b> - disable specified radio buttons. Only available when single selection column is visible.<br>");
	buf.append("values - array of item (row) keys.<br><br>");
	document.write(buf.toString());
};
