<commit_before>
sb.include('String.prototype.stripHTML');
sb.include('Array.prototype.sum');

/**
@Name: sb.widget.magicTable
@Version: 0.1 12-08-2008 12-08-2008
@Author: <NAME> sort functions taken from http://www.tagarga.com/blok/post/2
@Description:  Makes or adds interactivity to an HTML table.  All events are attached to the table and delegated from their to keep overhead very low
@param o Object
o.table String/Node Reference to an HTML table e.g. #myTable, or myTable
o.table Object Define table to be created {headers : [], rows : []}
o.classes Object class names to use for properties
o.classes.unsortable String The classname for columns you want to set as unsortable, the default is 'unsortable'
o.classes.sortable String The classname added to all sortable columns <th>s by sb.magicTable, default is 'sortable'
o.classes.sortedBy String The classname added to the <th> being sorted on, default is 'sortedBy'

@Example:
var myTable = new sb.widget.magicTable({
	table : '#jimmy',
	//these are the defaults, if you want them you can leave the classes property out
	sortable : 1,
	onCellClick : function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	},
	onRowClick : function(tr){
		tr.style.backgroundColor = 'red';
		//alert(tr.innerHTML);
	}
});

//or
var myTable = new sb.widget.magicTable({
	table : {
		headers : ['name', 'age', 'phone'],
		rows : [
			['Paul', 31, '228-7445'],
			['Matthew', 36, '228-7613'],
			['Terry', 29, '228-5731']
		]
	},
	sortable : 1,
	onCellClick : function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	},
	onRowClick : function(tr){
		tr.style.backgroundColor = 'red';
		//alert(tr.innerHTML);
	}
});
*/
sb.widget.magicTable = function(o){

	if(o.table && o.table.headers && o.table.rows){
		this.create(o.table.headers, o.table.rows);
	}

	if(typeof o.table == 'string' || o.table.appendTo){
		this.table = $(o.table);
		
		this.head = $(this.table.tHead);
		
		this.body = $(this.table.tBodies[0]);
	}
	
	this.sortable = o.sortable;
	if(this.sortable && o.classes){
		
		this.classes = {
			unsortable : (o.classes.unsortable) ? o.classes.unsortable : 'sb_unsortable',
			sortable : (o.classes.sortable) ? o.classes.sortable : 'sb_sortable',
			sorted_by : (o.classes.sorted_by) ? o.classes.sorted_by : 'sb_sorted_by',
			force_sort : (o.classes.force_sort) ? o.classes.force_sort : 'sb_force_sort'
		};

		this.setSortStyles();
	}

	if(typeof o.onCellClick == 'function'){
		this.onCellClick = o.onCellClick;
	}

	if(typeof o.onRowClick == 'function'){
		this.onRowClick = o.onRowClick;
	}

	if(typeof o.onColClick == 'function'){
		this.onColClick = o.onColClick;
	}

	if(typeof o.onHeaderClick == 'function'){
		this.onHeaderClick = o.onHeaderClick;
	}

	if(typeof o.onCellMouseOver == 'function'){
		this.onCellMouseOver = o.onCellMouseOver;
	}

	if(typeof o.onCellMouseOut == 'function'){
		this.onCellMouseOut = o.onCellMouseOut;
	}
	
	this.addEvents();

};

sb.widget.magicTable.prototype = {

	create : function(headers, rows){
	
		this.table = new sb.element({
			tag : 'table'
		});
	
		this.head = new sb.element({
			tag : 'thead'
		});
	
		this.body = new sb.element({
			tag : 'tbody'
		});
		
		this.table.appendChild(this.head);
		this.table.appendChild(this.body);
		this.addHeaders(headers);
		this.addRows(rows);
	},
	
	addCells : function(data, parent, tag){
		var self = this;
		if(data[0].forEach){
			data.forEach(function(row){
				self.addCells(row, parent, tag);
			});
		} else {
		
			var tr = parent.appendChild(new sb.element({
				tag : 'tr'
			}));
			
			data.forEach(function(cell){
				tr.appendChild(new sb.element({tag : tag, innerHTML : cell}));
			});
		}
	},
	
	/**
	@Name: sb.widget.table.prototype.addHeaders
	@Description: Adds rows to table headers
	@Example:
	myTable.addHeaders(['Blythe', '50','Wed, November 24, 2004','04/12/03','3.9.05', '$6,89']);
	*/
	addHeaders : function(data){
		this.addCells(data, this.head, 'th');
	},
	
	/**
	@Name: sb.widget.table.prototype.addRows
	@Description: Adds rows to a table instance
	@Example:
	myTable.addRows(['Blythe', '50','Wed, November 24, 2004','04/12/03','3.9.05', '$6,89']);
	*/
	addRows : function(data){
		this.addCells(data, this.body, 'td');
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.removeRows
	@Description: Removes rows from a magicTable instance, rows start at 0
	@Example:
	//removes row 1
	mymagicTable.removeRows(1);
	//all rows in the array
	mymagicTable.removeRows([0,1,5,7]);
	//remove all rows in range
	mymagicTable.removeRows('1-3');
	*/
	
	removeRows : function(rows){
		if(typeof rows == 'string'){
			
			var matches = rows.match(/(\d)+-(\d+)/);
			if(matches){
				rows = [];
				for(var x=matches[1];x<=matches[2];x++){
					rows.push(parseInt(x, 10));
				}
			}
		}
		
		this.table.$('tbody tr').forEach(function(v,k){
			
			if(rows.inArray(k) ){
				v.remove();
			}
		});
	},

	/**
	@Name: sb.widget.magicTable.loadRows
	@Description: Loads more rows from the server.  It expects the loading page to be in json format and to be an array of arrays which represent the values in the rows.  Null values should be represented by null.
	@Param: o object
	o.url String The url to load the data from
	@Example:
	myTable.loadRows({
		url : '/some/data'
	});
	*/
	loadRows : function(o){
		var self = this;
		o.format = 'json';
		o.onResponse = o.onResponse || function(r){
			self.addRows(r);
			if(typeof o.onLoaded == 'function'){
				o.onLoaded(r);
			}
		};
		var aj = new sb.ajax(o).fetch();
	},

	/**
	@Name: sb.widget.magicTable.sortBy()
	@Description: Loads more rows from the server.  It expects the loading page to be in json format and to be an array of arrays which represent the values in the rows.  Null values should be represented by null.
	@Param: header string Either the text value of the header to sort by, or a DOM reference to the header itself.  Always lowercase.
	@Param: reverse boolean Sort DESC
	@Example:
	myTable.sortBy('age');
	//or reversse
	myTable.sortBy('age', true);
	*/
	sortBy : function(header, reverse){
		var rows = [];
		var col = 0;
		var sortRule = '';
		var self = this;
	
		//UPDATE TO use cellIndex
		this.table.$('thead th').forEach(function(v,k){
			
			if((typeof header == 'string' && header == v.innerHTML.stripHTML().toLowerCase()) || header == v){
				
				col = k;
				var customSort = v.className.match(new RegExp(self.classes.force_sort+"_(\\w+)"));
				if(customSort){
					sortRule = customSort[1];
					
				}
		
				if(!sb.widget.magicTable.compare[sortRule]){
					sortRule = self.guessSortRule(k);
				}
				
				v.addClassName(self.classes.sorted_by);

			} else {
				v.removeClassName(self.classes.sorted_by);
			}
		});

		var trs = this.table.$('tbody tr');
		trs.forEach(function(tr,k,a){
			
			rows.push({
				text: tr.cells[col].innerHTML.stripHTML(), 
				td: tr.cells[col],
				tr: tr
			});
		});

		var compare = sb.widget.magicTable.compare[sortRule];
		rows.sort(function(a, b) {
			return compare(a.text + '', b.text + '', a.td, b.td);
		});

		if(reverse){
			rows.reverse();
		}

		var tbody = this.table.$('tbody').nodes[0];
		rows.forEach(function(row,k,a){
			
			tbody.appendChild(row.tr);
		});

		rows = null;
	},

	/**
	@Name: sb.widget.magicTable.prototype.onCellClick
	@Description: fires when a cell is clicked.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was clicked.
	@Example:
	myTable.onCellClick = function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	};	
	*/
	onCellClick : function(td){},

	/**
	@Name: sb.widget.magicTable.prototype.onRowClick
	@Description: fires when a row is clicked.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was clicked.
	@Example:
	myTable.onRowClick = function(tr){
		tr.style.backgroundColor = 'red';
	};
	*/
	onRowClick : function(tr){},
	
	/**
	@Name: sb.widget.magicTable.prototype.onColClick
	@Description: fires when a cell in a column is clicked.  Because there is no correspionding column node in HTML it returns an sb.nodeList of all the tds in the column.  The "this" is the magicTable instance itself.
	@Param: column Object An object representing the column clicked and some additional data
	column.title String The title of the column
	column.values Array An array of the values in the column
	column.th Element The TH of the column clicked
	column.tds sb.nodeList containing the TDs of the column clicked
	column.prevColumn A reference to the last Column clicked if there was one.  This allows to reset any changes (e.g. style changes) from the last column.
	@Example:
	myTable.onColClick = function(column){
		//change all the columns to yellow
		column.tds.styles({
			backgroundColor : 'yellow'
		});
		
		if(column.prevColumn){
			column.prevColumn.tds.styles({
				backgroundColor : ''
			});
		}
		
		column.values = column.values.map(function(v){
			return parseInt(v, 10);
		});
		alert(column.values.sum());
	};
	*/
	onColClick : function(column){},

	/**
	@Name: sb.widget.magicTable.prototype.onHeaderClick
	@Description: fires when a header TH is clicked.  The "this" is the magicTable instance itself.
	@Param: th Element The th that was clicked.
	@Example:
	myTable.onHeaderClick = function(th){
		th.style.backgroundColor = 'red';
	};
	*/
	onHeaderClick : function(th){
		th.style.backgroundColor = 'blue';
	},

	/**
	@Name: sb.widget.magicTable.prototype.onCellMouseOver
	@Description: fires when a table cell is moused over.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was mousedover.
	@Example:
	myTable.onCellMouseOver = function(td){
		td.style.backgroundColor = 'pink';
	};
	*/
	onCellMouseOver : function(td){},

	/**
	@Name: sb.widget.magicTable.prototype.onCellMouseOut
	@Description: fires when a table cell is moused out.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was mousedout.
	@Example:
	myTable.onCellMouseOut = function(td){
		td.style.backgroundColor = '';
	};
	*/
	onCellMouseOut : function(td){},
	
	/**
	@Name: sb.widget.magicTable.prototype.setSortStyles
	@Description: Used internally
	*/
	setSortStyles : function(){
		
		var self = this;
		
		this.table.$('th').forEach(function(v,k){
			
			v.onselectstart = function() {
		        return false;
		    };
		    v.unselectable = "on";
		    v.style.MozUserSelect = "none";

		    if(v.title == ''){
			    if(v.hasClassName(self.classes.unsortable)){
					v.title = 'Column not sortable';
					
			    } else {

					v.addClassName(self.classes.sortable);
			    	v.title = 'Click to sort, shift-click to reverse sort';
			    }
		    }
		});
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.addEvents
	@Description: Used internally
	*/
	
	addEvents : function(){
		var self = this;
		this.table.events({
			mousemove : function(e){
			
				target = sb.events.target(e);
				
				if(target.nodeName == 'TD'){
					if(self.prevover != target){
						if(self.prevover){
							self.onCellMouseOut(self.prevover);
						}
						self.onCellMouseOver(target);
					}

					self.prevover = target;
				}
				
			},
			click : function(e){
				var target = sb.events.target(e);
				
				if(target.nodeName == 'TD'){
					
					if(typeof self.onCellClick == 'function'){
						self.onCellClick(target);
					}
					self.onRowClick(target.parentNode);
				
					if(typeof self.onColClick == 'function'){
						
						var header = self.head.$('th').nodes[target.cellIndex];
						
						var column = {
							th : header,
							title : header.innerHTML.stripHTML(),
							values : [],
							tds : new sb.nodeList(),
							prevColumn : self._prevColumn
						};
						
						self.body.$('tr').forEach(function(tr){
							var td = tr.$('td').nodes[target.cellIndex];
							column.tds.add(td);
							column.values.push(td.innerHTML.stripHTML());
						});

						self.onColClick(column);
						self._prevColumn = column;
					}
					
				} else if(target.nodeName == 'TH'){
					self.onHeaderClick(target);
				}
			},
			
			mousedown : function(e){
				
				if(self.sortable && target.nodeName == 'TH'  && !target.hasClassName(self.classes.unsortable)){
					
					self.sortBy(target, e.shiftKey);
				}
			}
		});
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.guessSortRule
	@Description: Used internally
	*/
	guessSortRule: function(col) {
		var rows = this.table.tBodies[0].rows;
		for(var i = 0; i < rows.length; i++) {
			var text = rows[i].cells[col].innerHTML.stripHTML();
			if(text.length) return this.guessFormat(text);
		}
		return 'nocase';
	},

	/**
	@Name: sb.widget.magicTable.prototype.guessFormat
	@Description: Used internally
	*/
	guessFormat: function(text) {
		if(!isNaN(Number(text)))
			return 'numeric';
		if(text.match(/^\d{2}[\/-]\d{2}[\/-]\d{2,4}$/))
			return 'usdate';
		if(text.match(/^\d\d?\.\d\d?\.\d{2,4}$/))
			return 'eudate';
		if(!isNaN(Date.parse(text)))
			return 'date';
		if(!isNaN(sb.widget.magicTable.compare.currencyValue(text)))
			return 'currency';
		if(text.match(/^[a-z_]+\d+(\.\w+)$/))
			return 'natural';
		return 'nocase';
	}
		
};

/**
@Name: sb.widget.magicTable.prototype.compare
@Description: The sort methods avaiable to sb.widget.magicTable.  You can add your own too!
*/
sb.widget.magicTable.compare = {

	/**
	@Name: sb.widget.magicTable.prototype.alpha
	@Description: Sort alphabetically
	*/
	alpha : function(a, b) {
		return a > b ? 1 : a < b ? -1 : 0;
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.nocase
	@Description: Sort alphabetically, case insensitively
	*/
	nocase : function(a, b) {
		return sb.widget.magicTable.compare.alpha(a.toLowerCase(), b.toLowerCase());
	},
<commit_msg>updated Docs<commit_after>
sb.include('String.prototype.stripHTML');
sb.include('Array.prototype.sum');

/**
@Name: sb.widget.magicTable
@Version: 0.1 12-08-2008 12-08-2008
@Author: <NAME> sort functions taken from http://www.tagarga.com/blok/post/2
@Description:  Makes or adds interactivity to an HTML table.  All events are attached to the table and delegated from their to keep overhead very low
@param o Object
o.table String/Node Reference to an HTML table e.g. #myTable, or myTable
o.table Object Define table to be created {headers : [], rows : []}
o.classes Object class names to use for properties
o.classes.unsortable String The classname for columns you want to set as unsortable, the default is 'unsortable'
o.classes.sortable String The classname added to all sortable columns <th>s by sb.magicTable, default is 'sortable'
o.classes.sortedBy String The classname added to the <th> being sorted on, default is 'sortedBy'

@Example:
var myTable = new sb.widget.magicTable({
	table : '#jimmy',
	//these are the defaults, if you want them you can leave the classes property out
	sortable : 1,
	onCellClick : function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	},
	onRowClick : function(tr){
		tr.style.backgroundColor = 'red';
		//alert(tr.innerHTML);
	}
});
myTable.sortBy('colName');

//or
var myTable = new sb.widget.magicTable({
	table : {
		headers : ['name', 'age', 'phone'],
		rows : [
			['Paul', 31, '228-7445'],
			['Matthew', 36, '228-7613'],
			['Terry', 29, '228-5731']
		]
	},
	sortable : 1,
	onCellClick : function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	},
	onRowClick : function(tr){
		tr.style.backgroundColor = 'red';
		//alert(tr.innerHTML);
	}
});
myTable.table.appendTo('body');

*/
sb.widget.magicTable = function(o){

	if(o.table && o.table.headers && o.table.rows){
		this.create(o.table.headers, o.table.rows);
	}

	if(typeof o.table == 'string' || o.table.appendTo){
		this.table = $(o.table);
		
		this.head = $(this.table.tHead);
		
		this.body = $(this.table.tBodies[0]);
	}
	
	this.sortable = o.sortable;
	if(this.sortable && o.classes){
		
		this.classes = {
			unsortable : (o.classes.unsortable) ? o.classes.unsortable : 'sb_unsortable',
			sortable : (o.classes.sortable) ? o.classes.sortable : 'sb_sortable',
			sorted_by : (o.classes.sorted_by) ? o.classes.sorted_by : 'sb_sorted_by',
			force_sort : (o.classes.force_sort) ? o.classes.force_sort : 'sb_force_sort'
		};

		this.setSortStyles();
	}

	if(typeof o.onCellClick == 'function'){
		this.onCellClick = o.onCellClick;
	}

	if(typeof o.onRowClick == 'function'){
		this.onRowClick = o.onRowClick;
	}

	if(typeof o.onColClick == 'function'){
		this.onColClick = o.onColClick;
	}

	if(typeof o.onHeaderClick == 'function'){
		this.onHeaderClick = o.onHeaderClick;
	}

	if(typeof o.onCellMouseOver == 'function'){
		this.onCellMouseOver = o.onCellMouseOver;
	}

	if(typeof o.onCellMouseOut == 'function'){
		this.onCellMouseOut = o.onCellMouseOut;
	}
	
	this.addEvents();

};

sb.widget.magicTable.prototype = {

	/**
	@Name: sb.widget.table.prototype.addHeaders
	@Description: Adds rows to table headers
	@Example:
	myTable.addHeaders(['Blythe', '50','Wed, November 24, 2004','04/12/03','3.9.05', '$6,89']);
	*/
	addHeaders : function(data){
		this.addCells(data, this.head, 'th');
	},
	
	/**
	@Name: sb.widget.table.prototype.addRows
	@Description: Adds rows to a table instance
	@Example:
	myTable.addRows(['Blythe', '50','Wed, November 24, 2004','04/12/03','3.9.05', '$6,89']);
	*/
	addRows : function(data){
		this.addCells(data, this.body, 'td');
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.removeRows
	@Description: Removes rows from a magicTable instance, rows start at 0
	@Example:
	//removes row 1
	mymagicTable.removeRows(1);
	//all rows in the array
	mymagicTable.removeRows([0,1,5,7]);
	//remove all rows in range
	mymagicTable.removeRows('1-3');
	*/
	
	removeRows : function(rows){
		if(typeof rows == 'string'){
			
			var matches = rows.match(/(\d)+-(\d+)/);
			if(matches){
				rows = [];
				for(var x=matches[1];x<=matches[2];x++){
					rows.push(parseInt(x, 10));
				}
			}
		}
		
		this.table.$('tbody tr').forEach(function(v,k){
			
			if(rows.inArray(k) ){
				v.remove();
			}
		});
	},

	/**
	@Name: sb.widget.magicTable.loadRows
	@Description: Loads more rows from the server.  It expects the loading page to be in json format and to be an array of arrays which represent the values in the rows.  Null values should be represented by null.
	@Param: o object
	o.url String The url to load the data from
	@Example:
	myTable.loadRows({
		url : '/some/data'
	});
	*/
	loadRows : function(o){
		var self = this;
		o.format = 'json';
		o.onResponse = o.onResponse || function(r){
			self.addRows(r);
			if(typeof o.onLoaded == 'function'){
				o.onLoaded(r);
			}
		};
		var aj = new sb.ajax(o).fetch();
	},

	/**
	@Name: sb.widget.magicTable.sortBy()
	@Description: Loads more rows from the server.  It expects the loading page to be in json format and to be an array of arrays which represent the values in the rows.  Null values should be represented by null.
	@Param: header string Either the text value of the header to sort by, or a DOM reference to the header itself.  Always lowercase.
	@Param: reverse boolean Sort DESC
	@Example:
	myTable.sortBy('age');
	//or reversse
	myTable.sortBy('age', true);
	*/
	sortBy : function(header, reverse){
		var rows = [];
		var col = 0;
		var sortRule = '';
		var self = this;
	
		//UPDATE TO use cellIndex
		this.table.$('thead th').forEach(function(v,k){
			
			if((typeof header == 'string' && header == v.innerHTML.stripHTML().toLowerCase()) || header == v){
				
				col = k;
				var customSort = v.className.match(new RegExp(self.classes.force_sort+"_(\\w+)"));
				if(customSort){
					sortRule = customSort[1];
					
				}
		
				if(!sb.widget.magicTable.compare[sortRule]){
					sortRule = self.guessSortRule(k);
				}
				
				v.addClassName(self.classes.sorted_by);

			} else {
				v.removeClassName(self.classes.sorted_by);
			}
		});

		var trs = this.table.$('tbody tr');
		trs.forEach(function(tr,k,a){
			
			rows.push({
				text: tr.cells[col].innerHTML.stripHTML(), 
				td: tr.cells[col],
				tr: tr
			});
		});

		var compare = sb.widget.magicTable.compare[sortRule];
		rows.sort(function(a, b) {
			return compare(a.text + '', b.text + '', a.td, b.td);
		});

		if(reverse){
			rows.reverse();
		}

		var tbody = this.table.$('tbody').nodes[0];
		rows.forEach(function(row,k,a){
			
			tbody.appendChild(row.tr);
		});

		rows = null;
	},

	/**
	@Name: sb.widget.magicTable.prototype.onCellClick
	@Description: fires when a cell is clicked.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was clicked.
	@Example:
	myTable.onCellClick = function(td){
		td.style.backgroundColor = 'pink';
		if(td.innerHTML == 'delete'){
			$(td.parentNode).remove();
		}
	};	
	*/
	onCellClick : function(td){},

	/**
	@Name: sb.widget.magicTable.prototype.onRowClick
	@Description: fires when a row is clicked.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was clicked.
	@Example:
	myTable.onRowClick = function(tr){
		tr.style.backgroundColor = 'red';
	};
	*/
	onRowClick : function(tr){},
	
	/**
	@Name: sb.widget.magicTable.prototype.onColClick
	@Description: fires when a cell in a column is clicked.  Because there is no correspionding column node in HTML it returns an sb.nodeList of all the tds in the column.  The "this" is the magicTable instance itself.
	@Param: column Object An object representing the column clicked and some additional data
	column.title String The title of the column
	column.values Array An array of the values in the column
	column.th Element The TH of the column clicked
	column.tds sb.nodeList containing the TDs of the column clicked
	column.prevColumn A reference to the last Column clicked if there was one.  This allows to reset any changes (e.g. style changes) from the last column.
	@Example:
	myTable.onColClick = function(column){
		//change all the columns to yellow
		column.tds.styles({
			backgroundColor : 'yellow'
		});
		
		if(column.prevColumn){
			column.prevColumn.tds.styles({
				backgroundColor : ''
			});
		}
		
		column.values = column.values.map(function(v){
			return parseInt(v, 10);
		});
		alert(column.values.sum());
	};
	*/
	onColClick : function(column){},

	/**
	@Name: sb.widget.magicTable.prototype.onHeaderClick
	@Description: fires when a header TH is clicked.  The "this" is the magicTable instance itself.
	@Param: th Element The th that was clicked.
	@Example:
	myTable.onHeaderClick = function(th){
		th.style.backgroundColor = 'red';
	};
	*/
	onHeaderClick : function(th){
		th.style.backgroundColor = 'blue';
	},

	/**
	@Name: sb.widget.magicTable.prototype.onCellMouseOver
	@Description: fires when a table cell is moused over.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was mousedover.
	@Example:
	myTable.onCellMouseOver = function(td){
		td.style.backgroundColor = 'pink';
	};
	*/
	onCellMouseOver : function(td){},

	/**
	@Name: sb.widget.magicTable.prototype.onCellMouseOut
	@Description: fires when a table cell is moused out.  The "this" is the magicTable instance itself.
	@Param: td Element The td that was mousedout.
	@Example:
	myTable.onCellMouseOut = function(td){
		td.style.backgroundColor = '';
	};
	*/
	onCellMouseOut : function(td){},
	
	/**
	@Name: sb.widget.magicTable.prototype.setSortStyles
	@Description: Used internally
	*/
	setSortStyles : function(){
		
		var self = this;
		
		this.table.$('th').forEach(function(v,k){
			
			v.onselectstart = function() {
		        return false;
		    };
		    v.unselectable = "on";
		    v.style.MozUserSelect = "none";

		    if(v.title == ''){
			    if(v.hasClassName(self.classes.unsortable)){
					v.title = 'Column not sortable';
					
			    } else {

					v.addClassName(self.classes.sortable);
			    	v.title = 'Click to sort, shift-click to reverse sort';
			    }
		    }
		});
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.addEvents
	@Description: Used internally
	*/
	
	addEvents : function(){
		var self = this;
		this.table.events({
			mousemove : function(e){
			
				target = sb.events.target(e);
				
				if(target.nodeName == 'TD'){
					if(self.prevover != target){
						if(self.prevover){
							self.onCellMouseOut(self.prevover);
						}
						self.onCellMouseOver(target);
					}

					self.prevover = target;
				}
				
			},
			click : function(e){
				var target = sb.events.target(e);
				
				if(target.nodeName == 'TD'){
					
					if(typeof self.onCellClick == 'function'){
						self.onCellClick(target);
					}
					self.onRowClick(target.parentNode);
				
					if(typeof self.onColClick == 'function'){
						
						var header = self.head.$('th').nodes[target.cellIndex];
						
						var column = {
							th : header,
							title : header.innerHTML.stripHTML(),
							values : [],
							tds : new sb.nodeList(),
							prevColumn : self._prevColumn
						};
						
						self.body.$('tr').forEach(function(tr){
							var td = tr.$('td').nodes[target.cellIndex];
							column.tds.add(td);
							column.values.push(td.innerHTML.stripHTML());
						});

						self.onColClick(column);
						self._prevColumn = column;
					}
					
				} else if(target.nodeName == 'TH'){
					self.onHeaderClick(target);
				}
			},
			
			mousedown : function(e){
				
				if(self.sortable && target.nodeName == 'TH'  && !target.hasClassName(self.classes.unsortable)){
					
					self.sortBy(target, e.shiftKey);
				}
			}
		});
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.guessSortRule
	@Description: Used internally
	*/
	guessSortRule: function(col) {
		var rows = this.table.tBodies[0].rows;
		for(var i = 0; i < rows.length; i++) {
			var text = rows[i].cells[col].innerHTML.stripHTML();
			if(text.length) return this.guessFormat(text);
		}
		return 'nocase';
	},

	/**
	@Name: sb.widget.magicTable.prototype.guessFormat
	@Description: Used internally
	*/
	guessFormat: function(text) {
		if(!isNaN(Number(text)))
			return 'numeric';
		if(text.match(/^\d{2}[\/-]\d{2}[\/-]\d{2,4}$/))
			return 'usdate';
		if(text.match(/^\d\d?\.\d\d?\.\d{2,4}$/))
			return 'eudate';
		if(!isNaN(Date.parse(text)))
			return 'date';
		if(!isNaN(sb.widget.magicTable.compare.currencyValue(text)))
			return 'currency';
		if(text.match(/^[a-z_]+\d+(\.\w+)$/))
			return 'natural';
		return 'nocase';
	},
	
	/**
	@Name: sb.widget.table.prototype.create
	@Description: Used Internally
	*/
	create : function(headers, rows){
	
		this.table = new sb.element({
			tag : 'table'
		});
	
		this.head = new sb.element({
			tag : 'thead'
		});
	
		this.body = new sb.element({
			tag : 'tbody'
		});
		
		this.table.appendChild(this.head);
		this.table.appendChild(this.body);
		this.addHeaders(headers);
		this.addRows(rows);
	},
	
	/**
	@Name: sb.widget.table.prototype.addCells
	@Description: Used Internally
	*/
	addCells : function(data, parent, tag){
		var self = this;
		if(data[0].forEach){
			data.forEach(function(row){
				self.addCells(row, parent, tag);
			});
		} else {
		
			var tr = parent.appendChild(new sb.element({
				tag : 'tr'
			}));
			
			data.forEach(function(cell){
				tr.appendChild(new sb.element({tag : tag, innerHTML : cell}));
			});
		}
	}
		
};

/**
@Name: sb.widget.magicTable.prototype.compare
@Description: The sort methods avaiable to sb.widget.magicTable.  You can add your own too!
*/
sb.widget.magicTable.compare = {

	/**
	@Name: sb.widget.magicTable.prototype.alpha
	@Description: Sort alphabetically
	*/
	alpha : function(a, b) {
		return a > b ? 1 : a < b ? -1 : 0;
	},
	
	/**
	@Name: sb.widget.magicTable.prototype.nocase
	@Description: Sort alphabetically, case insensitively
	*/
	nocase : function(a, b) {
		return sb.widget.magicTable.compare.alpha(a.toLowerCase(), b.toLowerCase());
	},

{'fraction_non_alphanumeric': 0.15680420158998526, 'fraction_numerical': 0.008705349113277456, 'mean_word_length': 3.3114143920595533, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4361891', 'n_tokens_mistral': 11157, 'n_tokens_neox': 9561, 'n_words': 2251}