John Cappiello - Dojo.common-0.4.1

Documentation | Source
dojo.provide("dojo.widget.FilteringTable");

dojo.require("dojo.date.format");
dojo.require("dojo.collections.Store");
dojo.require("dojo.html.*");
dojo.require("dojo.html.util");
dojo.require("dojo.html.style");
dojo.require("dojo.html.selection");
dojo.require("dojo.event.*");
dojo.require("dojo.widget.*");
dojo.require("dojo.widget.HtmlWidget");

dojo.widget.defineWidget(
	"dojo.widget.FilteringTable", 
	dojo.widget.HtmlWidget, 
	function(){
		// summary: A basic tabular data widget that supports sorting and filtering mechanisms.
		// description:
		//		FilteringTable is a 2D data view that supports multiple column sorting and filtering
		//		functionality.  It can get its data in one of two ways: via HTML (i.e. degradable
		//		data), or from an external JSON source through widget.store.setData.  Records in
		//		a FilteringTable can be selected as if it were a select list.
		// store: dojo.collections.Store
		//		The underlying Store for all data represented by the widget.
		// valueField: String
		// 		The name of the field used as a unique key for each row, defaults to "Id".
		// multiple: boolean
		//		Allow multiple selections.
		// maxSelect: Integer
		//		Maximum number of rows that can be selected at once.  0 == no limit.
		// maxSortable: Integer
		//		Maximum number of columns allowed for sorting at one time.
		// minRows: Integer
		//		The minimum number of rows to show.  Default is 0.
		// defaultDateFormat: String
		//		The default format for a date column, as used by dojo.date.format.
		// alternateRows: Boolean
		//		Use alternate row CSS classes to show zebra striping.
		// headClass: String
		//		CSS Class name for the head of the table.
		// tbodyClass: String
		//		CSS Class name for the body of the table.
		// headerClass: String
		//		CSS Class name for headers that are not sorted.
		// headerUpClass: String
		//		CSS Class name for headers that are for ascending sorted columns. Default is "selectedUp".
		// headerDownClass: String
		//		CSS Class name for headers that are for descending sorted columns. Default is "selectedDown".
		// rowClass: String
		//		CSS Class name for body rows.
		// rowAlternateClass: String
		//		CSS Class name for alternate rows.  Default is "alt".
		// rowSelectedClass: String
		//		CSS Class name for selected rows.  Default is "selected".
		// columnSelectedClass: String
		//		CSS Class name for any columns being sorted on.  Unimplemented.
		this.store=new dojo.collections.Store();

		//declare per instance changeable widget properties
		this.valueField="Id";
		this.multiple=false;
		this.maxSelect=0;
		this.maxSortable=1;  // how many columns can be sorted at once.
		this.minRows=0;
		this.defaultDateFormat = "%D";
		this.isInitialized=false;
		this.alternateRows=false;

		this.columns=[];
		this.sortInformation=[{
			index:0,
			direction:0
		}];

		// CSS definitions
		this.headClass="";
		this.tbodyClass="";
		this.headerClass="";
		this.headerUpClass="selectedUp";
		this.headerDownClass="selectedDown";
		this.rowClass="";
		this.rowAlternateClass="alt";
		this.rowSelectedClass="selected";
		this.columnSelected="sorted-column";
	},
{
	//	dojo widget properties
	isContainer: false,
	templatePath: null,
	templateCssPath: null,

	//	methods.
	getTypeFromString: function(/* string */s){
		//	summary
		//	Gets a function based on the passed string.
		var parts = s.split("."), i = 0, obj = dj_global; 
		do{ 
			obj = obj[parts[i++]]; 
		} while (i < parts.length && obj); 
		return (obj != dj_global) ? obj : null;	//	function
	},

	//	custom data access.
	getByRow: function(/*HTMLTableRow*/row){
		//	summary
		//	Returns the data object based on the passed row.
		return this.store.getByKey(dojo.html.getAttribute(row, "value"));	//	object
	},
	getDataByRow: function(/*HTMLTableRow*/row){
		//	summary
		//	Returns the source data object based on the passed row.
		return this.store.getDataByKey(dojo.html.getAttribute(row, "value")); // object
	},

	getRow: function(/* Object */ obj){
		//	summary
		//	Finds the row in the table based on the passed data object.
		var rows = this.domNode.tBodies[0].rows;
		for(var i=0; i<rows.length; i++){
			if(this.store.getDataByKey(dojo.html.getAttribute(rows[i], "value")) == obj){
				return rows[i];	//	HTMLTableRow
			}
		}
		return null;	//	HTMLTableRow
	},
	getColumnIndex: function(/* string */fieldPath){
		//	summary
		//	Returns index of the column that represents the passed field path.
		for(var i=0; i<this.columns.length; i++){
			if(this.columns[i].getField() == fieldPath){
				return i;	//	integer
			}
		}
		return -1;	//	integer
	},

	getSelectedData: function(){
		//	summary
		//	returns all objects that are selected.
		var data=this.store.get();
		var a=[];
		for(var i=0; i<data.length; i++){
			if(data[i].isSelected){
				a.push(data[i].src);
			}
		}
		if(this.multiple){
			return a;		//	array
		} else {
			return a[0];	//	object
		}
	},
	
	isSelected: function(/* object */obj){
		//	summary
		//	Returns whether the passed object is currently selected.
		var data = this.store.get();
		for(var i=0; i<data.length; i++){
			if(data[i].src == obj){
				return true;	//	boolean
			}
		}
		return false;	//	boolean
	},
	isValueSelected: function(/* string */val){
		//	summary
		//	Returns the object represented by key "val" is selected.
		var v = this.store.getByKey(val);
		if(v){
			return v.isSelected;	//	boolean
		}
		return false;	//	boolean
	},
	isIndexSelected: function(/* number */idx){
		//	summary
		//	Returns the object represented by integer "idx" is selected.
		var v = this.store.getByIndex(idx);
		if(v){
			return v.isSelected;	//	boolean
		}
		return false;	//	boolean
	},
	isRowSelected: function(/* HTMLTableRow */row){
		//	summary
		//	Returns if the passed row is selected.
		var v = this.getByRow(row);
		if(v){
			return v.isSelected;	//	boolean
		}
		return false;	//	boolean
	},

	reset: function(){
		//	summary
		//	Resets the widget to its initial internal state.
		this.store.clearData();
		this.columns = [];
		this.sortInformation = [ {index:0, direction:0} ];
		this.resetSelections();
		this.isInitialized = false;
		this.onReset();
	},
	resetSelections: function(){
		//	summary
		//	Unselects all data objects.
		this.store.forEach(function(element){
			element.isSelected = false;
		});
	},
	onReset:function(){ 
		//	summary
		//	Stub for onReset event.
	},

	//	selection and toggle functions
	select: function(/*object*/ obj){
		//	summary
		//	selects the passed object.
		var data = this.store.get();
		for(var i=0; i<data.length; i++){
			if(data[i].src == obj){
				data[i].isSelected = true;
				break;
			}
		}
		this.onDataSelect(obj);
	},
	selectByValue: function(/*string*/ val){
		//	summary
		//	selects the object represented by key "val".
		this.select(this.store.getDataByKey(val));
	},
	selectByIndex: function(/*number*/ idx){
		//	summary
		//	selects the object represented at index "idx".
		this.select(this.store.getDataByIndex(idx));
	},
	selectByRow: function(/*HTMLTableRow*/ row){
		//	summary
		//	selects the object represented by HTMLTableRow row.
		this.select(this.getDataByRow(row));
	},
	selectAll: function(){
		//	summary
		//	selects all objects.
		this.store.forEach(function(element){
			element.isSelected = true;
		});
	},
	onDataSelect: function(/* object */obj){ 
		//	summary
		//	Stub for onDataSelect event.
	},

	toggleSelection: function(/*object*/obj){
		//	summary
		//	Flips the selection state of passed obj.
		var data = this.store.get();
		for(var i=0; i<data.length; i++){
			if(data[i].src == obj){
				data[i].isSelected = !data[i].isSelected;
				break;
			}
		}
		this.onDataToggle(obj);
	},
	toggleSelectionByValue: function(/*string*/val){
		//	summary
		//	Flips the selection state of object represented by val.
		this.toggleSelection(this.store.getDataByKey(val));
	},
	toggleSelectionByIndex: function(/*number*/idx){
		//	summary
		//	Flips the selection state of object at index idx.
		this.toggleSelection(this.store.getDataByIndex(idx));
	},
	toggleSelectionByRow: function(/*HTMLTableRow*/row){
		//	summary
		//	Flips the selection state of object represented by row.
		this.toggleSelection(this.getDataByRow(row));
	},
	toggleAll: function(){
		//	summary
		//	Flips the selection state of all objects.
		this.store.forEach(function(element){
			element.isSelected = !element.isSelected;
		});
	},
	onDataToggle: function(/* object */obj){ 
		//	summary
		//	Stub for onDataToggle event.
	},

	//	parsing functions, from HTML to metadata/SimpleStore
	_meta:{
		field:null,
		format:null,
		filterer:null,
		noSort:false,
		sortType:"String",
		dataType:String,
		sortFunction:null,
		filterFunction:null,
		label:null,
		align:"left",
		valign:"middle",
		getField:function(){ 
			return this.field || this.label; 
		},
		getType:function(){ 
			return this.dataType; 
		}
	},
	createMetaData: function(/* object */obj){
		//	summary
		//	Take a JSON-type structure and make it into a ducktyped metadata object.
		for(var p in this._meta){
			//	rudimentary mixin
			if(!obj[p]){
				obj[p] = this._meta[p];
			}
		}
		if(!obj.label){
			obj.label=obj.field;
		}
		if(!obj.filterFunction){
			obj.filterFunction=this._defaultFilter;
		}
		return obj;	//	object
	},
	parseMetadata: function(/* HTMLTableHead */head){
		//	summary
		//	Parses the passed HTMLTableHead element to create meta data.
		this.columns=[];
		this.sortInformation=[];
		var row = head.getElementsByTagName("tr")[0];
		var cells = row.getElementsByTagName("td");
		if (cells.length == 0){
			cells = row.getElementsByTagName("th");
		}
		for(var i=0; i<cells.length; i++){
			var o = this.createMetaData({ });
			
			//	presentation attributes
			if(dojo.html.hasAttribute(cells[i], "align")){
				o.align = dojo.html.getAttribute(cells[i],"align");
			}
			if(dojo.html.hasAttribute(cells[i], "valign")){
				o.valign = dojo.html.getAttribute(cells[i],"valign");
			}
			if(dojo.html.hasAttribute(cells[i], "nosort")){
				o.noSort = (dojo.html.getAttribute(cells[i],"nosort")=="true");
			}
			if(dojo.html.hasAttribute(cells[i], "sortusing")){
				var trans = dojo.html.getAttribute(cells[i],"sortusing");
				var f = this.getTypeFromString(trans);
				if (f != null && f != window && typeof(f)=="function"){
					o.sortFunction=f;
				}
			}
			o.label = dojo.html.renderedTextContent(cells[i]);
			if(dojo.html.hasAttribute(cells[i], "field")){
				o.field=dojo.html.getAttribute(cells[i],"field");
			} else if(o.label.length > 0){
				o.field=o.label;
			} else {
				o.field = "field" + i;
			}
			if(dojo.html.hasAttribute(cells[i], "format")){
				o.format=dojo.html.getAttribute(cells[i],"format");
			}
			if(dojo.html.hasAttribute(cells[i], "dataType")){
				var sortType = dojo.html.getAttribute(cells[i],"dataType");
				if(sortType.toLowerCase()=="html" || sortType.toLowerCase()=="markup"){
					o.sortType = "__markup__";	//	always convert to "__markup__"
				}else{
					var type = this.getTypeFromString(sortType);
					if(type){
						o.sortType = sortType;
						o.dataType = type;
					}
				}
			}

			//	TODO: set up filtering mechanisms here.
			if(dojo.html.hasAttribute(cells[i], "filterusing")){
				var trans = dojo.html.getAttribute(cells[i],"filterusing");
				var f = this.getTypeFromString(trans);
				if (f != null && f != window && typeof(f)=="function"){
					o.filterFunction=f;
				}
			}
			
			this.columns.push(o);

			//	check to see if there's a default sort, and set the properties necessary
			if(dojo.html.hasAttribute(cells[i], "sort")){
				var info = {
					index:i,
					direction:0
				};
				var dir = dojo.html.getAttribute(cells[i], "sort");
				if(!isNaN(parseInt(dir))){
					dir = parseInt(dir);
					info.direction = (dir != 0) ? 1 : 0;
				}else{
					info.direction = (dir.toLowerCase() == "desc") ? 1 : 0;
				}
				this.sortInformation.push(info);
			}
		}
		if(this.sortInformation.length == 0){
			this.sortInformation.push({
				index:0,
				direction:0
			});
		} else if (this.sortInformation.length > this.maxSortable){
			this.sortInformation.length = this.maxSortable;
		}
	},
	parseData: function(/* HTMLTableBody */body){
		//	summary
		//	Parse HTML data into native JSON structure for the store.
		if(body.rows.length == 0 && this.columns.length == 0){
			return;	//	there's no data, ignore me.
		}

		//	create a data constructor based on what we've got for the fields.
		var self=this;
		this["__selected__"] = [];
		var arr = this.store.getFromHtml(this.columns, body, function(obj, row){
			if(typeof(obj[self.valueField])=="undefined" || obj[self.valueField]==null){
				obj[self.valueField] = dojo.html.getAttribute(row, "value");
			}
			if(dojo.html.getAttribute(row, "selected")=="true"){
				self["__selected__"].push(obj);
			}
		});
		
		this.store.setData(arr, true);
		this.render();
		
		for(var i=0; i<this["__selected__"].length; i++){
			this.select(this["__selected__"][i]);
		}
		this.renderSelections();

		delete this["__selected__"];

		//	say that we are already initialized so that we don't kill anything
		this.isInitialized=true;
	},

	//	standard events
	onSelect: function(/* HTMLEvent */e){
		//	summary
		//	Handles the onclick event of any element.
		var row = dojo.html.getParentByType(e.target,"tr");
		if(dojo.html.hasAttribute(row,"emptyRow")){
			return;
		}
		var body = dojo.html.getParentByType(row,"tbody");
		if(this.multiple){
			if(e.shiftKey){
				var startRow;
				var rows=body.rows;
				for(var i=0;i<rows.length;i++){
					if(rows[i]==row){
						break;
					}
					if(this.isRowSelected(rows[i])){
						startRow=rows[i];
					}
				}
				if(!startRow){
					startRow = row;
					for(; i<rows.length; i++){
						if(this.isRowSelected(rows[i])){
							row = rows[i];
							break;
						}
					}
				}
				this.resetSelections();
				if(startRow == row){
					this.toggleSelectionByRow(row);
				} else {
					var doSelect = false;
					for(var i=0; i<rows.length; i++){
						if(rows[i] == startRow){
							doSelect=true;
						}
						if(doSelect){
							this.selectByRow(rows[i]);
						}
						if(rows[i] == row){
							doSelect = false;
						}
					}
				}
			} else {
				this.toggleSelectionByRow(row);
			}
		} else {
			this.resetSelections();
			this.toggleSelectionByRow(row);
		}
		this.renderSelections();
	},
	onSort: function(/* HTMLEvent */e){
		//	summary
		//	Sort the table based on the column selected.
		var oldIndex=this.sortIndex;
		var oldDirection=this.sortDirection;
		
		var source=e.target;
		var row=dojo.html.getParentByType(source,"tr");
		var cellTag="td";
		if(row.getElementsByTagName(cellTag).length==0){
			cellTag="th";
		}

		var headers=row.getElementsByTagName(cellTag);
		var header=dojo.html.getParentByType(source,cellTag);
		
		for(var i=0; i<headers.length; i++){
			dojo.html.setClass(headers[i], this.headerClass);
			if(headers[i]==header){
				if(this.sortInformation[0].index != i){
					this.sortInformation.unshift({ 
						index:i, 
						direction:0
					});
				} else {
					this.sortInformation[0] = {
						index:i,
						direction:(~this.sortInformation[0].direction)&1
					};
				}
			}
		}

		this.sortInformation.length = Math.min(this.sortInformation.length, this.maxSortable);
		for(var i=0; i<this.sortInformation.length; i++){
			var idx=this.sortInformation[i].index;
			var dir=(~this.sortInformation[i].direction)&1;
			dojo.html.setClass(headers[idx], dir==0?this.headerDownClass:this.headerUpClass);
		}
		this.render();
	},
	onFilter: function(){
		//	summary
		//	show or hide rows based on the parameters of the passed filter.
	},

	//	Filtering methods
	_defaultFilter: function(/* Object */obj){
		//	summary
		//	Always return true as the result of the default filter.
		return true;
	},
	setFilter: function(/* string */field, /* function */fn){
		//	summary
		//	set a filtering function on the passed field.
		for(var i=0; i<this.columns.length; i++){
			if(this.columns[i].getField() == field){
				this.columns[i].filterFunction=fn;
				break;
			}
		}
		this.applyFilters();
	},
	setFilterByIndex: function(/* number */idx, /* function */fn){
		//	summary
		//	set a filtering function on the passed column index.
		this.columns[idx].filterFunction=fn;
		this.applyFilters();
	},
	clearFilter: function(/* string */field){
		//	summary
		//	clear a filtering function on the passed field.
		for(var i=0; i<this.columns.length; i++){
			if(this.columns[i].getField() == field){
				this.columns[i].filterFunction=this._defaultFilter;
				break;
			}
		}
		this.applyFilters();
	}, 
	clearFilterByIndex: function(/* number */idx){
		//	summary
		//	clear a filtering function on the passed column index.
		this.columns[idx].filterFunction=this._defaultFilter;
		this.applyFilters();
	}, 
	clearFilters: function(){
		//	summary
		//	clears all filters.
		for(var i=0; i<this.columns.length; i++){
			this.columns[i].filterFunction=this._defaultFilter;
		}
		//	we'll do the clear manually, it will be faster.
		var rows=this.domNode.tBodies[0].rows;
		for(var i=0; i<rows.length; i++){
			rows[i].style.display="";
			if(this.alternateRows){
				dojo.html[((i % 2 == 1)?"addClass":"removeClass")](rows[i], this.rowAlternateClass);
			}
		}
		this.onFilter();
	},
	applyFilters: function(){
		//	summary
		//	apply all filters to the table.
		var alt=0;
		var rows=this.domNode.tBodies[0].rows;
		for(var i=0; i<rows.length; i++){
			var b=true;
			var row=rows[i];
			for(var j=0; j<this.columns.length; j++){
				var value = this.store.getField(this.getDataByRow(row), this.columns[j].getField());
				if(this.columns[j].getType() == Date && value != null && !value.getYear){
					value = new Date(value);
				}
				if(!this.columns[j].filterFunction(value)){
					b=false;
					break;
				}
			}
			row.style.display=(b?"":"none");
			if(b && this.alternateRows){
				dojo.html[((alt++ % 2 == 1)?"addClass":"removeClass")](row, this.rowAlternateClass);
			}
		}
		this.onFilter();
	},

	//	sorting functionality
	createSorter: function(/* array */info){
		//	summary
		//	creates a custom function to be used for sorting.
		var self=this;
		var sortFunctions=[];	//	our function stack.
	
		function createSortFunction(fieldIndex, dir){
			var meta=self.columns[fieldIndex];
			var field=meta.getField();
			return function(rowA, rowB){
				if(dojo.html.hasAttribute(rowA,"emptyRow")){ return 1; }
				if(dojo.html.hasAttribute(rowB,"emptyRow")){ return -1; }

				//	TODO: check for markup and compare by rendered text.
				var a = self.store.getField(self.getDataByRow(rowA), field);
				var b = self.store.getField(self.getDataByRow(rowB), field);
				var ret = 0;
				if(a > b) ret = 1;
				if(a < b) ret = -1;
				return dir * ret;
			}
		}

		var current=0;
		var max = Math.min(info.length, this.maxSortable, this.columns.length);
		while(current < max){
			var direction = (info[current].direction == 0) ? 1 : -1;
			sortFunctions.push(
				createSortFunction(info[current].index, direction)
			);
			current++;
		}

		return function(rowA, rowB){
			var idx=0;
			while(idx < sortFunctions.length){
				var ret = sortFunctions[idx++](rowA, rowB);
				if(ret != 0) return ret;
			}
			//	if we got here then we must be equal.
			return 0; 	
		};	//	function
	},

	//	rendering
	createRow: function(/* object */obj){
		//	summary
		//	Create an HTML row based on the passed object
		var row=document.createElement("tr");
		dojo.html.disableSelection(row);
		if(obj.key != null){
			row.setAttribute("value", obj.key);
		}
		for(var j=0; j<this.columns.length; j++){
			var cell=document.createElement("td");
			cell.setAttribute("align", this.columns[j].align);
			cell.setAttribute("valign", this.columns[j].valign);
			dojo.html.disableSelection(cell);
			var val = this.store.getField(obj.src, this.columns[j].getField());
			if(typeof(val)=="undefined"){
				val="";
			}
			this.fillCell(cell, this.columns[j], val);
			row.appendChild(cell);
		}
		return row;	//	HTMLTableRow
	},
	fillCell: function(/* HTMLTableCell */cell, /* object */meta, /* object */val){
		//	summary
		//	Fill the passed cell with value, based on the passed meta object.
		if(meta.sortType=="__markup__"){
			cell.innerHTML=val;
		} else {
			if(meta.getType()==Date) {
				val=new Date(val);
				if(!isNaN(val)){
					var format = this.defaultDateFormat;
					if(meta.format){
						format = meta.format;
					}
					cell.innerHTML = dojo.date.strftime(val, format);
				} else {
					cell.innerHTML = val;
				}
			} else if ("Number number int Integer float Float".indexOf(meta.getType())>-1){
				//	TODO: number formatting
				if(val.length == 0){
					val="0";
				}
				var n = parseFloat(val, 10) + "";
				//	TODO: numeric formatting + rounding :)
				if(n.indexOf(".")>-1){
					n = dojo.math.round(parseFloat(val,10),2);
				}
				cell.innerHTML = n;
			}else{
				cell.innerHTML = val;
			}
		}
	},
	prefill: function(){
		//	summary
		//	if there's no data in the table, then prefill it with this.minRows.
		this.isInitialized = false;
		var body = this.domNode.tBodies[0];
		while (body.childNodes.length > 0){
			body.removeChild(body.childNodes[0]);
		}
		
		if(this.minRows>0){
			for(var i=0; i < this.minRows; i++){
				var row = document.createElement("tr");
				if(this.alternateRows){
					dojo.html[((i % 2 == 1)?"addClass":"removeClass")](row, this.rowAlternateClass);
				}
				row.setAttribute("emptyRow","true");
				for(var j=0; j<this.columns.length; j++){
					var cell = document.createElement("td");
					cell.innerHTML = "&nbsp;";
					row.appendChild(cell);
				}
				body.appendChild(row);
			}
		}
	},
	init: function(){
		//	summary
		//	initializes the table of data
		this.isInitialized=false;

		//	if there is no thead, create it now.
		var head=this.domNode.getElementsByTagName("thead")[0];
		if(head.getElementsByTagName("tr").length == 0){
			//	render the column code.
			var row=document.createElement("tr");
			for(var i=0; i<this.columns.length; i++){
				var cell=document.createElement("td");
				cell.setAttribute("align", this.columns[i].align);
				cell.setAttribute("valign", this.columns[i].valign);
				dojo.html.disableSelection(cell);
				cell.innerHTML=this.columns[i].label;
				row.appendChild(cell);

				//	attach the events.
				if(!this.columns[i].noSort){
					dojo.event.connect(cell, "onclick", this, "onSort");
				}
			}
			dojo.html.prependChild(row, head);
		}
		
		if(this.store.get().length == 0){
			return false;
		}

		var idx=this.domNode.tBodies[0].rows.length;
		if(!idx || idx==0 || this.domNode.tBodies[0].rows[0].getAttribute("emptyRow")=="true"){
			idx = 0;
			var body = this.domNode.tBodies[0];
			while(body.childNodes.length>0){
				body.removeChild(body.childNodes[0]);
			}

			var data = this.store.get();
			for(var i=0; i<data.length; i++){
				var row = this.createRow(data[i]);
				body.appendChild(row);
				idx++;
			}
		}

		//	add empty rows
		if(this.minRows > 0 && idx < this.minRows){
			idx = this.minRows - idx;
			for(var i=0; i<idx; i++){
				row=document.createElement("tr");
				row.setAttribute("emptyRow","true");
				for(var j=0; j<this.columns.length; j++){
					cell=document.createElement("td");
					cell.innerHTML="&nbsp;";
					row.appendChild(cell);
				}
				body.appendChild(row);
			}
		}

		//	last but not least, show any columns that have sorting already on them.
		var row=this.domNode.getElementsByTagName("thead")[0].rows[0];
		var cellTag="td";
		if(row.getElementsByTagName(cellTag).length==0) cellTag="th";
		var headers=row.getElementsByTagName(cellTag);
		for(var i=0; i<headers.length; i++){
			dojo.html.setClass(headers[i], this.headerClass);
		}
		for(var i=0; i<this.sortInformation.length; i++){
			var idx=this.sortInformation[i].index;
			var dir=(~this.sortInformation[i].direction)&1;
			dojo.html.setClass(headers[idx], dir==0?this.headerDownClass:this.headerUpClass);
		}

		this.isInitialized=true;
		return this.isInitialized;
	},
	render: function(){
		//	summary
		//	Renders the actual table data.

	/*	The method that should be called once underlying changes
	 *	are made, including sorting, filtering, data changes.
	 *	Rendering the selections themselves are a different method,
	 *	which render() will call as the last step.
	 ****************************************************************/
		if(!this.isInitialized){
			var b = this.init();
			if(!b){
				this.prefill();
				return;
			}
		}

		//	do the sort
		var rows=[];
		var body=this.domNode.tBodies[0];
		var emptyRowIdx=-1;
		for(var i=0; i<body.rows.length; i++){
			rows.push(body.rows[i]);
		}

		//	build the sorting function, and do the sorting.
		var sortFunction = this.createSorter(this.sortInformation);
		if(sortFunction){
			rows.sort(sortFunction);
		}

		//	append the rows without killing them, this should help with the HTML problems.
		for(var i=0; i<rows.length; i++){
			if(this.alternateRows){
				dojo.html[((i%2==1)?"addClass":"removeClass")](rows[i], this.rowAlternateClass);
			}
			dojo.html[(this.isRowSelected(body.rows[i])?"addClass":"removeClass")](body.rows[i], this.rowSelectedClass);
			body.appendChild(rows[i]);
		}
	},
	renderSelections: function(){
		//	summary
		//	Render all selected objects using CSS.
		var body=this.domNode.tBodies[0];
		for(var i=0; i<body.rows.length; i++){
			dojo.html[(this.isRowSelected(body.rows[i])?"addClass":"removeClass")](body.rows[i], this.rowSelectedClass);
		}
	},

	//	widget lifetime handlers
	initialize: function(){ 
		//	summary
		//	Initializes the widget.
		var self=this;
		//	connect up binding listeners here.
		dojo.event.connect(this.store, "onSetData", function(){
			self.store.forEach(function(element){
				element.isSelected = false;
			});
			self.isInitialized=false;
			var body = self.domNode.tBodies[0];
			if(body){
				while(body.childNodes.length>0){
					body.removeChild(body.childNodes[0]);
				}
			}
			self.render();
		});
		dojo.event.connect(this.store, "onClearData", function(){
			self.isInitialized = false;
			self.render();
		});
		dojo.event.connect(this.store, "onAddData", function(addedObject){
			var row=self.createRow(addedObject);
			self.domNode.tBodies[0].appendChild(row);
			self.render();
		});
		dojo.event.connect(this.store, "onAddDataRange", function(arr){
			for(var i=0; i<arr.length; i++){
				arr[i].isSelected=false;
				var row=self.createRow(arr[i]);
				self.domNode.tBodies[0].appendChild(row);
			};
			self.render();
		});
		dojo.event.connect(this.store, "onRemoveData", function(removedObject){
			var rows = self.domNode.tBodies[0].rows;
			for(var i=0; i<rows.length; i++){
				if(self.getDataByRow(rows[i]) == removedObject.src){
					rows[i].parentNode.removeChild(rows[i]);
					break;
				}
			}
			self.render();
		});
		dojo.event.connect(this.store, "onUpdateField", function(obj, fieldPath, val){
			var row = self.getRow(obj);
			var idx = self.getColumnIndex(fieldPath);
			if(row && row.cells[idx] && self.columns[idx]){
				self.fillCell(row.cells[idx], self.columns[idx], val);
			}
		});
	},
	postCreate: function(){
		//	summary
		//	finish widget initialization.
		this.store.keyField = this.valueField;

		if(this.domNode){
			//	start by making sure domNode is a table element;
			if(this.domNode.nodeName.toLowerCase() != "table"){
			}

			//	see if there is columns set up already
			if(this.domNode.getElementsByTagName("thead")[0]){
				var head=this.domNode.getElementsByTagName("thead")[0];
				if(this.headClass.length > 0){
					head.className = this.headClass;
				}
				dojo.html.disableSelection(this.domNode);
				this.parseMetadata(head);

				var header="td";
				if(head.getElementsByTagName(header).length==0){
					header="th";
				}
				var headers = head.getElementsByTagName(header);
				for(var i=0; i<headers.length; i++){
					if(!this.columns[i].noSort){
						dojo.event.connect(headers[i], "onclick", this, "onSort");
					}
				}
			} else {
				this.domNode.appendChild(document.createElement("thead"));
			}

			// if the table doesn't have a tbody already, add one and grab a reference to it
			if (this.domNode.tBodies.length < 1) {
				var body = document.createElement("tbody");
				this.domNode.appendChild(body);
			} else {
				var body = this.domNode.tBodies[0];
			}

			if (this.tbodyClass.length > 0){
				body.className = this.tbodyClass;
			}
			dojo.event.connect(body, "onclick", this, "onSelect");
			this.parseData(body);
		}
	}
});