/*
	holonet.sortgrid.js
	Author: Luke Coe <luke@holonet.com.au>
	Simple sortable table with hover highlight and single select row support.
*/

// For browsers that don't reflect the DOM constants (like IE).
if (document.ELEMENT_NODE == null) {
	document.ELEMENT_NODE = 1;
  	document.TEXT_NODE = 3;
}

SortGrid = Class.create();
SortGrid.prototype = {
	initialize: function(gridWrap,bodyWrap,gridHead,gridBody,context) {
		this.IMG_DESC = context + '/web/theme/grid/desc.png';
		this.IMG_ASC  = context + '/web/theme/grid/asc.png';
	
		this.multiple 		= true;
		this.selectedRows   = new Array();
		this.hoverRow 		= null;
		this.lastSorted		= null;
		this.onsort       	= null;		//event
		this.head			= gridHead;
		
		//assign event handlers
		var o = this;
		this.head.onclick	= function(e) { o._click(e); }
		
		this.bindTable(bodyWrap,gridBody);
	},
	
	bindTable : function(bodyWrap,gridBody) {
		this.table = gridBody;
		this.data = bodyWrap;
		
		//vars to adjust table to cope with scrollbars
		var scrolling = gridBody.offsetHeight > bodyWrap.offsetHeight;
		var scrollbarSize = scrolling ? bodyWrap.offsetWidth - gridBody.offsetWidth : 0;
		
		//adjust header
		if (window.ActiveXObject && scrolling)
			this.table.style.width = this.table.clientWidth - scrollbarSize + 'px';
		if (this.table.clientWidth != 0) 
			this.head.style.width = this.table.clientWidth +'px';
		this.head.rows[0].cells[this.head.rows[0].cells.length-1].style.borderRight = '0px';
		
		//assign event handlers
		var o = this;
		
		//this.table.onclick		 = function(e) { o._click(e); }
		//this.table.onmouseover	 = function(e) { o._mouseOver(e); }
		//this.table.onmouseout	 = function(e) { o._mouseOut(e); }
		//this.table.onselectstart = function(e) { return false; }
		
		if (this.lastSorted != null) 
			grid.sort(this.lastSorted);
		else this.updateSelections();
	},
	
	_mouseOver : function(e) {
		var el = (e) ? e.target : window.event.srcElement;
		this.hoverRow = this.table.rows[el.parentNode.rowIndex];
		if (this.hoverRow && this.hoverRow.className.search('selected') == -1) {
	      	//remove 'sorted' css to support highlight
	      	if (this.lastSorted != null) {
	      		var cellEl = this.hoverRow.cells[this.lastSorted];
	      		cellEl.className = cellEl.className.replace(new RegExp('sorted','gi'), '');
	      		cellEl.className = this.normalizeString(cellEl.className);
	      	}
			this.hoverRow.className = this.hoverRow.className.replace(new RegExp('odd','gi'), '');
			this.hoverRow.className = this.hoverRow.className.replace(new RegExp('even','gi'), '');
	      	this.hoverRow.className += ' highlight';
	      	this.hoverRow.className = this.normalizeString(this.hoverRow.className);
		}
		return 0;
	},
	
	_mouseOut : function(e) {
		var el = (e) ? e.target : window.event.srcElement;
		this.hoverRow = this.table.rows[el.parentNode.rowIndex];
		if (this.hoverRow && this.hoverRow.className.search('selected') == -1) {
			//restore 'sorted' css after highlight
			if (this.lastSorted != null) {
		  		var cellEl = this.hoverRow.cells[this.lastSorted];
				cellEl.className += ' sorted';
		    	cellEl.className = this.normalizeString(cellEl.className);
		    }
			this.hoverRow.className = this.hoverRow.className.replace(new RegExp('highlight','gi'), '');
	      	this.hoverRow.className += (el.parentNode.rowIndex % 2 != 0) ? ' odd':' even';
	      	this.hoverRow.className = this.normalizeString(this.hoverRow.className);
		}
		return 0;
	},

	_click : function(e) {
		var el = (e) ? e.target : window.event.srcElement;
		if (el.tagName == 'IMG' || el.tagName == 'DIV') 
			el = el.parentNode;
		if (el.tagName == 'TH' || el.parentNode.tagName == 'TH') 
			this.sort(el.cellIndex,false);
		if (el.tagName == 'TD')
			this.selectRow(el.parentNode, (e) ? e.ctrlKey : window.event.ctrlKey);
	},
	
	selectRow : function(row, ctrlKey) {
		var select = true;
		// Normal click
		if ((!ctrlKey) || (!this.multiple)) {
			// Deselect previously selected rows
			while (this.selectedRows.length) {
				this.styleRow(this.selectedRows[0],false);
				this.selectedRows.splice(0, 1);
			}
		} else { // Control + Click
		for (var i=0; i<this.selectedRows.length; i++) {
				if (this.selectedRows[i] == row) {
					// Deselect clicked row
					this.styleRow(row,false);
					this.selectedRows.splice(i, 1);
					select = false; break;
				}
			}
		}
		// Add selected row
		if (select) {
			this.selectedRows.push(row);
		   	this.styleRow(row,true);
	   	}
		return 0;
	},
	
	getText : function(el) {
	  	// Find and concatenate the values of all text nodes contained within the element.
	  	var s = '';
	  	for (var i = 0; i < el.childNodes.length; i++) {
	    	if (el.childNodes[i].nodeType == document.TEXT_NODE)
	      		s += el.childNodes[i].nodeValue;
	    	else if (el.childNodes[i].nodeType == document.ELEMENT_NODE && el.childNodes[i].tagName == 'BR')
	      		s += ' ';
	    	else s += this.getText(el.childNodes[i]);	// Use recursion to get text within sub-elements.
		}
		return this.normalizeString(s);
	},
	
	normalizeString : function(s) {
		s = s.replace(new RegExp('\\s\\s+', 'g'), ' ');  	// Collapse any multiple whites space.
	  	s = s.replace(new RegExp('^\\s*|\\s*$', 'g'), '');	// Remove leading or trailing white space.
	  	return s;
	},
	
	updateSelections : function(){
		this.selectedRows = new Array();
		for (var i=0; i<this.table.rows.length; i++){
			var select = (this.table.rows[i].className.search('selected') != -1);
			if (select) 
				this.selectedRows.push(this.table.rows[i]);
			this.styleRow(this.table.rows[i],select);
		}
	},
	
	updateArrows : function(index,desc) {
		if (this.lastSorted != null){
			var el = this.head.rows[0].cells[this.lastSorted];
			var img = el.getElementsByTagName('img')[0];
			if (this.lastSorted != index) { 
				el.removeChild(img);
				this.addArrow(index,desc);
			} else img.src = (desc) ? this.IMG_DESC : this.IMG_ASC;
		} else this.addArrow(index,desc);
	},
	
	addArrow : function(index, desc) {
		var el = this.head.rows[0].cells[index];
		var img = el.getElementsByTagName('img')[0];
		img = document.createElement('IMG');
		img.src = (desc) ? this.IMG_DESC : this.IMG_ASC;
		img.style.display = 'inline';
		el.appendChild(img);
	},
	
	styleRow : function(row, select) {
		if (select) {
			row.className = row.className.replace(new RegExp('highlight','gi'), '');
			row.className += ' selected';
			row.className = this.normalizeString(row.className);
			
			if (this.lastSorted != null) {
				var cellEl = row.cells[this.lastSorted];
				cellEl.className = cellEl.className.replace(new RegExp('sorted', 'gi'), '');
			}
		} else {
			row.className = row.className.replace(new RegExp('selected','gi'), '');
			row.className = row.className.replace(new RegExp('odd','gi'), '');
			row.className = row.className.replace(new RegExp('even','gi'), '');
			row.className += (row.rowIndex & 1)?' odd':' even';
			row.className = this.normalizeString(row.className);
			
			if (this.lastSorted != null) {
				var cellEl = row.cells[this.lastSorted];
				cellEl.className += ' sorted';
				cellEl.className = this.normalizeString(cellEl.className);
			}
		}
	},
	
	styleColumn : function(index,sorted) {
		for (var i=0; i<this.table.rows.length; i++) {
			var row = this.table.rows[i];
			if (!this.isSelected(row)) {
				var cellEl = row.cells[this.lastSorted];
				if (sorted) {
					cellEl.className += ' sorted';
					cellEl.className = this.normalizeString(cellEl.className);
				} else cellEl.className = cellEl.className.replace(new RegExp('sorted', 'gi'), '');
			}
		}
	},
	
	isSelected : function(row){
		for (var r in this.selectedRows){
			if (r == row) return true;
		}
		return false;
	},
	
	
	sort : function(col, rev) {
		var tableBody = this.table.tBodies[0];
	
	  	// The first time this function is called for a given table, set up an
	  	// array of reverse sort flags.
	  	if (tableBody.reverseSort == null)
	    	tableBody.reverseSort = new Array();
	
	  	// If this column has not been sorted before, set the initial sort direction.
	  	if (tableBody.reverseSort[col] == null)
	    	tableBody.reverseSort[col] = rev;
	
	  	// If this column was the last one sorted, reverse its sort direction and update arrows
	  	if (col == this.lastSorted) {
	    	tableBody.reverseSort[col] = !tableBody.reverseSort[col];
	    	this.updateArrows(col,tableBody.reverseSort[col]);
	    } else {
	    	this.updateArrows(col,tableBody.reverseSort[col]);
			if (this.lastSorted != null)
				this.styleColumn(this.lastSorted,false);
			this.lastSorted = col;
			this.styleColumn(this.lastSorted,true);
		}
	
	  	var oldDsply = tableBody.style.display;
	  	tableBody.style.display = "none";
	
	  	// Sort the rows based on the content of the specified column using a selection sort.
	  	for (var i=0; i<tableBody.rows.length-1; i++) {
	    	// Assume the current row has the minimum value.
	    	var minIdx = i;
	    	var minVal = this.getText(tableBody.rows[i].cells[col]);
	
	    	// Search the rows that follow the current one for a smaller value.
	    	for (var j=i+1; j<tableBody.rows.length; j++) {
	      		var testVal = this.getText(tableBody.rows[j].cells[col]);
	      		var cmp = this.compareValues(minVal, testVal);
	      		// Negate the comparison result if the reverse sort flag is set.
	      		if (tableBody.reverseSort[col])
	        		cmp = -cmp;
	      		// If this row has a smaller value than the current minimum, remember its
	      		// position and update the current minimum value.
	      		if (cmp > 0) {
	        		minIdx = j;
	        		minVal = testVal;
	      		}
	    	}
	
	    	// insert smallest before the current row.
	    	if (minIdx > i) {
	      		var tmpEl = tableBody.removeChild(tableBody.rows[minIdx]);
	      		tableBody.insertBefore(tmpEl, tableBody.rows[i]);
	    	}
	  	}
	
		//update selections and row css
		this.updateSelections();
		tableBody.style.display = oldDsply;
		
	  	return false;
	},
	
	//currently only supports numerics and strings
	//TODO update to allow plugable compare functions
	compareValues : function(v1,v2) {
	  	// If the values are numeric, convert them to floats.
	  	var f1 = parseFloat(v1);
	  	var f2 = parseFloat(v2);
	  	
	  	if (!isNaN(f1) && !isNaN(f2)) {
	  		v1 = f1;
	  		v2 = f2;
	  	} else {
	  		v1 = v1.toUpperCase(); 
	  		v2 = v2.toUpperCase();
	  	}
	  	
	  	// Compare the two values.
	  	if (v1 == v2) return 0;
	  	if (v1 > v2) return 1;
	  	return -1;
	}
};

