Source: resultsTable.js

/**
 * @fileOverview This file defines the ResultsTable class.
 * Copyright 2008-2013 by Kenneth F. Greenberg; all rights reserved
 * This work is licensed under the Creative Commons Attribution-Share Alike License. To view a copy of this license, 
 * visit http://creativecommons.org/licenses/by-sa/3.0/; 
 * or, (b) send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA.
 * @author Kenneth F. Greenberg
 * @requires InputElement library
 */
/*global document, InputElementButton */
/**
 * @class The ResultsTable object, a displayable table with optional paging and sorting.
 * @constructor
 * @param {String} tag The ID string assigned to both id and name attributes of the table
 */
function ResultsTable(tag) {	// constructor body creates table and tbody
	this.tag = tag;		// save for methods
	this.elt = document.createElement('table');
	this.elt.setAttribute('id',tag);
	this.elt.setAttribute('name',tag);
	this.thead = document.createElement('thead');
	this.elt.appendChild(this.thead);
	this.tbody = document.createElement('tbody');
	this.elt.appendChild(this.tbody);
	this.pageBlock = document.createElement('p');
	this.pageBlock.setAttribute('id',tag+'pages');
	this.pageBlock.style.display = "none";
}
ResultsTable.prototype.rows = 0;
ResultsTable.prototype.pageSize = 0;
ResultsTable.prototype.currentPage = 0;
ResultsTable.prototype.paging = false;
/**
 * Assigns a CSS class to the table
 * @param {string} clinfo The CSS class to assign to the table's class attribute
 */
ResultsTable.prototype.setClass = function(clinfo) { this.elt.setAttribute('class',clinfo); };
/**
 * Set the CSS class for a column
 * @param {number} col The column of interest
 * @param {string} style The CSS class name
 */
ResultsTable.prototype.setColumnClass = function(col,style) {
	var r, rows = this.tbody.getElementsByTagName('tr');
	for (r=0; r<rows.length; r++) {
		var cols = rows[r].getElementsByTagName("td");
		if (col < cols.length) {
			cols[col].setAttribute('class',style);
		}
	}
};
/**
 * Insert a span into a column of the table for later use (e.g., as a graphic element)
 * @param {number} row The row of interest, zero being first tbody row
 * @param {number} col The column of interest
 * @param {string} val The name base of the span, to which we append the row to create a unique ID
 * @param {string} width Optional width specifier
 */
ResultsTable.prototype.addDiv = function(row,col,val,width) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return;
	}
	var dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		var sname = val+row;	// e.g., foo -> foo0, foo1, etc
		if (width) {
			dataItems[col].innerHTML = '<div id="'+sname+'" style="width:'+width+'"></div>';
		} else {
			dataItems[col].innerHTML = '<div id="'+sname+'"></div>';
		}
	}
};
/**
 * Set the number of table rows per page
 * @param {number} sz The number of rows per page
 */
ResultsTable.prototype.setPageSize = function(sz) { this.pageSize = sz; };
/**
 * Turn paging on for this object
 * @param {function} listener An event listener to be called when user clicks on prev and next buttons
 */
ResultsTable.prototype.enablePaging = function(listener) {
	if (this.pageSize === 0) {
		return;
	}
	if (this.rows <= this.pageSize) {	// pointless
		return;
	}
	if (!this.paging) {
		var b = new InputElementButton(this.tag+'Prev','Prev');
		if (listener) {
			b.onClick(listener);
		}
		b.appendTo(this.pageBlock);
		b = new InputElementButton(this.tag+'Next','Next');
		b.appendTo(this.pageBlock);
		if (listener) {
			b.onClick(listener);
		}
		// now walk the table and turn on the first page
		var tbl = this.elt;
		for (var i=1; i<=this.pageSize; i++) {
			tbl.rows[i].style.display = "";
		}
		// and turn off the rest
		for (; i<tbl.rows.length; i++) {
			tbl.rows[i].style.display = "none";
		}
		this.currentPage = 1;
	}
	this.paging = true;
	this.pageBlock.style.display = "block";
};
/**
 * Display the next page of the table; called from event listener.
 */
ResultsTable.prototype.nextPage = function() {
	var pages = Math.ceil(this.rows / this.pageSize);
	if ((this.currentPage + 1) > pages) { return; }
	this.currentPage++;
	var tbl = this.elt;
	var firstRow = ((this.currentPage - 1) * this.pageSize) + 1;
	var lastRow = firstRow + this.pageSize - 1;
	if (lastRow > this.rows) {
		lastRow = this.rows;
	}
	for (var i=1; i<tbl.rows.length; i++) {
		if (i>=firstRow && i<=lastRow) {
			tbl.rows[i].style.display = "";
		} else {
			tbl.rows[i].style.display = "none";
		}			
	}
};
/**
 * Display the previous page of the table; called from event listener.
 */
ResultsTable.prototype.prevPage = function() {
	if (this.currentPage == 1) { return; }
	this.currentPage--;
//	var pages = Math.ceil(this.rows / this.pageSize);
	var tbl = this.elt;
	var firstRow = ((this.currentPage - 1) * this.pageSize) + 1;
	var lastRow = firstRow + this.pageSize - 1;
	if (lastRow > this.rows) {
		lastRow = this.rows;
	}
	for (var i=1; i<tbl.rows.length; i++) {
		if (i>=firstRow && i<=lastRow) {
			tbl.rows[i].style.display = "";
		} else {
			tbl.rows[i].style.display = "none";
		}			
	}
};
/**
 * Return the currently displayed page number.
 * @returns {number} The current page number
 */
ResultsTable.prototype.getCurrentPage = function() { return this.currentPage; };
/**
 * Adds a row to the table, either a header or a data row. Used internally.
 * @private
 * @param {string} nodetype Either 'th' or 'td'
 * @param {array} list The list of strings to be added to form the row.
 */
ResultsTable.prototype.addTableRow = function(nodetype,list) {
	var tr = document.createElement('tr');
	var td;
	for (var i=0;i<list.length;i++) {
		td = document.createElement(nodetype);	// either th or td
		td.appendChild(document.createTextNode(list[i]));
		tr.appendChild(td);			
	}
	if (nodetype == 'th') {
		this.thead.appendChild(tr);
	} else {
		this.tbody.appendChild(tr);
	}
};
/**
 * Adds a header to the table. Uses the anonymous argument array as input, so no declared args.
 */
ResultsTable.prototype.addHeader = function() { // should be passed a list of strings as args
	this.addTableRow('th',arguments);
};
/**
 * Adds a header to the table. Uses an explicit array
 * @param {array} sa Array of strings to be used as header items.
 */
ResultsTable.prototype.addHeaderStrings = function(sa) { // should be passed an array of strings
	this.addTableRow('th',sa);
};
/**
 * Adds a data row to the table. Uses the anonymous argument array as input, so no declared args.
 * @example myTable.addRow('ken','707-555-1212','ken@calast.com');
 */
ResultsTable.prototype.addRow = function() { // should be passed an array of strings
	this.addTableRow('td',arguments);
	this.rows++;
};
/**
 * Adds a data row to the table. Used when you want to pass an existing array of strings.
 * @param {array} sa Array of strings to be used as data items.
 * @example myTable.addRow(myStringArray);
 */
ResultsTable.prototype.addRowStrings = function(sa) { // should be passed an array of strings
	this.addTableRow('td',sa);
	this.rows++;
};
/**
 * appends the table and page block to a parent node
 * @param {Object} parent The DOM element to which this is added
 */
ResultsTable.prototype.appendTo = function(parent) { 
	parent.appendChild(this.elt);
	parent.appendChild(this.pageBlock); 
};
/**
 * Get the number of rows in the table
 * @returns {number} The row count
 */
ResultsTable.prototype.getRows = function() { return this.rows; };
/**
 * Get the contents of one row
 * @param {number} row The row of interest, zero being first tbody row
 * @returns {array} The 'td' values in that row, or null if row does not exist
 */
ResultsTable.prototype.getRowData = function(row) {	// returns an array of values
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	var dataItems = rowOfInterest.getElementsByTagName('td');
	var retValue = [];
	for (var i=0; i<dataItems.length; i++) {
		retValue.push(dataItems[i].innerHTML);
	}
	return retValue;
};
/**
 * Get the contents of one table element
 * @param {number} row The row of interest, zero being first tbody row
 * @param {number} col The column of interest
 * @returns {string} The 'td' value at that row and column, or null if row does not exist
 */
ResultsTable.prototype.getDataValue = function(row,col) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	var dataItems = rowOfInterest.getElementsByTagName('td');
	return dataItems[col].innerHTML;
};
/**
 * Set the contents of one data element by specified row and column
 * @param {number} row The row of interest, zero being first tbody row
 * @param {number} col The column of interest
 * @param {number} val The new value
 */
ResultsTable.prototype.setDataValue = function(row,col,val) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return;
	}
	var dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		dataItems[col].innerHTML = val;
	}
};
/**
 * Make this table sortable - must be called after table has its contents
 * Although most of the code for this method is original, many of the design ideas were inspired 
 * by examples at http://www.webtoolkit.info/, and much credit is due to the author(s) of that site.
 */
ResultsTable.prototype.makeSortable = function() {
	var thisObject = this;
	// Check to see if there are body rows to be sorted
	if (!(this.tbody && this.tbody.rows && this.tbody.rows.length > 0)) { return; }
	// There must be a non-empty thead for this to work, else give up.
	if (!(this.thead && this.thead.rows && this.thead.rows.length > 0)) { return; }
	var sortRow = this.thead.rows[0];
	/**
	 * This function retrieves the text in a DOM-independent way
	 * @inner
	 * @param {Object} el The DOM element whose text content is to be retrieved
	 * @return {String} Text contents of the table cell
	 */
	this.getInnerText = function (el) {
		if (typeof(el.textContent) != 'undefined') { return el.textContent; }
		if (typeof(el.innerText) != 'undefined') { return el.innerText; }
		if (typeof(el.innerHTML) == 'string') { return el.innerHTML.replace(/<[^<>]+>/g,''); }
	};
	/**
	 * This function does a sort compare of two numeric fields
	 * @inner
	 * @param {Object} a First DOM element
	 * @param {Object} b Second DOM element
	 * @return {Number} 0 if equal, 1 if a > b, -1 if a < b
	 */
	this.sortNumeric = function(a,b) {
		var aa = parseFloat(thisObject.getInnerText(a.cells[thisObject.sortColumnIndex]));
		if (isNaN(aa)) { aa = 0; }
		var bb = parseFloat(thisObject.getInnerText(b.cells[thisObject.sortColumnIndex]));
		if (isNaN(bb)) { bb = 0; }
		return aa-bb;
	};
	/**
	 * This function does a sort compare of two alpha fields in a case-independent way
	 * @inner
	 * @param {Object} a First DOM element
	 * @param {Object} b Second DOM element
	 * @return {Number} 0 if equal, 1 if a > b, -1 if a < b
	 */
	this.sortAlpha = function(a,b) {
		var aa = thisObject.getInnerText(a.cells[thisObject.sortColumnIndex]).toLowerCase();
		var bb = thisObject.getInnerText(b.cells[thisObject.sortColumnIndex]).toLowerCase();
		if (aa == bb) { return 0; }
		if (aa < bb) { return -1; }
		return 1;
	};
	/**
	 * This function does the actual sort
	 * @inner
	 * @param {Object} cell The DOM element that actually got clicked on
	 */
	this.sort = function(cell) {
		var column = cell.cellIndex;
		var cellContents = this.getInnerText(this.tbody.rows[1].cells[column]);
		var sortfn = this.sortAlpha;

		if (cellContents.replace(/^\s+|\s+$/g,"").match(/^-?[\d\.]+$/)) {
			sortfn = this.sortNumeric;
		}
		this.sortColumnIndex = column;

		var newRows = [];
		for (var j = 0; j < this.tbody.rows.length; j++) {
			newRows[j] = this.tbody.rows[j];
		}
		newRows.sort(sortfn);
		if (cell.getAttribute("sortdir") == 'down') {
			newRows.reverse();
			cell.setAttribute('sortdir','up');
		} else {
			cell.setAttribute('sortdir','down');
		}

		for (var i=0; i < newRows.length; i++) {
			this.tbody.appendChild(newRows[i]);
		}
	};
	/**
	 * This is the click handler - does a sort when clicked.
	 * @inner
	 */
	var sortClickHandler = function () {
		this.sTable.sort(this);
		return false;
	};
	// Install a click handler in each cell of the first header row.
	for (var i=0; i<sortRow.cells.length; i++) {
		sortRow.cells[i].sTable = this;
		sortRow.cells[i].onclick = sortClickHandler;
	}
};