Source: selectorTable.js

/**
 * @fileOverview This file defines the SelectorTable class.
 * Copyright 2008-2015 by Kenneth F. Greenberg; all rights reservedf
 * 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, alert */
// This function adds a listener to an element in a browser-agnostic way
// elem is an element; ev is a string like click or change; fn is the function to call
function addRadioButtonListener(elem,ev,fn) {
	if (elem.addEventListener ) {
	  elem.addEventListener(ev, fn, false); 
	} else if (elem.attachEvent) {
		  elem.attachEvent('on'+ev, fn);
	}
}
/**
 * @class The SelectorTable object, a displayable table with a radio button column and optional paging.
 * @constructor
 */
function SelectorTable(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.elt.setAttribute('class','SelectorTable');
	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";
}
SelectorTable.prototype.rows = 0;
SelectorTable.prototype.pageSize = 0;
SelectorTable.prototype.currentPage = 0;
SelectorTable.prototype.paging = false;
/**
 * Returns the number of rows that have been added to the table
 * @returns {number} The row count
 */
SelectorTable.prototype.getRowCount = function() { return this.rows; };
/**
 * Assigns a CSS class to the table
 * @param {string} clinfo The CSS class to assign to the table's class attribute
 */
SelectorTable.prototype.setClass = function(clinfo) { this.elt.setAttribute('class',clinfo); };
/**
 * Set the number of table rows per page
 * @param {number} sz The number of rows per page
 */
SelectorTable.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
 */
SelectorTable.prototype.enablePaging = function(listener) {
	var i;
	if (this.pageSize === 0) {
		return;
	}
	if (this.rows <= this.pageSize) {	// pointless
		return;
	}
	if (!this.paging) {
		var pages = Math.ceil(this.rows / this.pageSize);
		var a = document.createElement('a');
		a.setAttribute('id',this.tag+'Prev');
		addRadioButtonListener(a,'click',listener);
		var t = document.createTextNode('Prev');
		a.appendChild(t);
		this.pageBlock.appendChild(a);
		this.pageBlock.appendChild(document.createTextNode(' '));
		for (i=1; i<=pages; i++) {
			a = document.createElement('a');
			a.setAttribute('id',this.tag+i);
			addRadioButtonListener(a,'click',listener);
			t = document.createTextNode(i.toString());
			a.appendChild(t);
			this.pageBlock.appendChild(a);
			this.pageBlock.appendChild(document.createTextNode(' '));
		}
		a = document.createElement('a');
		a.setAttribute('id',this.tag+'Next');
		addRadioButtonListener(a,'click',listener);
		t = document.createTextNode('Next');
		a.appendChild(t);
		this.pageBlock.appendChild(a);
		// now walk the table and turn on the first page
		var tbl = this.elt;
		for (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";
};
/**
 * Does the actual display; called from showPage, nextPage, or prevPage
 */
SelectorTable.prototype.refreshPage = function() {
	var i, 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 (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 a specific page of the table; called from event listener.
 * @param {number} pagenum Which page to display
 */
SelectorTable.prototype.showPage = function(pagenum) {
	var pages = Math.ceil(this.rows / this.pageSize);
	var seekPage = parseInt(pagenum,10);
	if (seekPage >= 1 && seekPage <= pages) {
		this.currentPage = seekPage;
		this.refreshPage();
	} else {
		alert('Page '+pagenum+' is not valid');
	}
};
/**
 * Display the next page of the table; called from event listener.
 */
SelectorTable.prototype.nextPage = function() {
	var pages = Math.ceil(this.rows / this.pageSize);
	if ((this.currentPage + 1) > pages) { return; }
	this.currentPage++;
	this.refreshPage();
};
/**
 * Display the previous page of the table; called from event listener.
 */
SelectorTable.prototype.prevPage = function() {
	if (this.currentPage === 1) { return; }
	this.currentPage--;
	this.refreshPage();
};
/**
 * Add an (optional) event listener to every radio button - must be called AFTER rows have been added.
 * Originally added listener to every input, but works more reliably if the first input per row is used.
 * @param {function} listener The function to be called when the user clicks on a radio button
 */
SelectorTable.prototype.onClick = function(listener) {
	var i, inputs, rows = this.tbody.getElementsByTagName('tr');
	
	for (i=0; i<rows.length; i++) {
		inputs = rows[i].getElementsByTagName("input");
		addRadioButtonListener(inputs[0],'click',listener);
	}
};
/**
 * Adds a header to the table. Uses the anonymous argument array as input, so no declared args.
 */
SelectorTable.prototype.addHeader = function() { // should be passed an array of strings
	var i, tr = document.createElement('tr');
	var td = document.createElement('th');
	td.appendChild(document.createTextNode('Select'));
	tr.appendChild(td);
	for (i=0;i<arguments.length;i++) {
		td = document.createElement('th');
		td.appendChild(document.createTextNode(arguments[i]));
		tr.appendChild(td);			
	}
	this.thead.appendChild(tr);
};
/**
 * Delete all data rows from the table. Primarily used when redrawing a table that is already
 * part of a view, keeping its header intact. Deletes all children of the tbody node.
 */
SelectorTable.prototype.deleteData = function() {
	var parent = this.tbody;
	if (parent.hasChildNodes()) {
		while (parent.childNodes.length >= 1) {
			parent.removeChild(parent.firstChild);
		}
	}
	this.rows = 0;
}
/**
 * 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');
 * @returns {integer} row index added.
 */
SelectorTable.prototype.addRow = function() { // should be passed an array of strings
	var i, tr = document.createElement('tr');
	var td = document.createElement('td');
	// IE hack for radio group
	var input = document.createElement('span');
	input.innerHTML = '<input type="radio" name="'+this.tag+'Group">';
	td.appendChild(input);
	tr.appendChild(td);
	for (i=0;i<arguments.length;i++) {
		td = document.createElement('td');
		td.appendChild(document.createTextNode(arguments[i]));
		tr.appendChild(td);			
	}
	this.tbody.appendChild(tr);
	this.rows++;
	return this.rows - 1;
};
/**
 * Adds a data row to the table. Uses the anonymous argument array as input, so no declared args.
 * The first argument is not displayable, but is assigned to the radio button as a value.
 * @example myTable.addRowWithValue(42,'ken','707-555-1212','ken@calast.com');
 * @returns {integer} row index added.
 */
SelectorTable.prototype.addRowWithValue = function(v) {
	var tr = document.createElement('tr');
	var td = document.createElement('td');
	var i, input = document.createElement('span');
	input.innerHTML = '<input type="radio" name="'+this.tag+'Group" value="'+v+'">';
	td.appendChild(input);
	tr.appendChild(td);
	for (i=1;i<arguments.length;i++) {
		td = document.createElement('td');
		td.appendChild(document.createTextNode(arguments[i]));
		tr.appendChild(td);			
	}
	this.tbody.appendChild(tr);
	this.rows++;
	return this.rows - 1;
};
/**
 * Finds a row by button value, changes other columns. Uses the anonymous argument array as input, 
 * so no declared args except the value string identfying the button.
 * @param {String[]} (anonymous) First element is value to find, rest are replacement values
 * @example myTable.updateRowWithValue(42,'bob','707-555-1213','keng@calast.com');
 */
SelectorTable.prototype.updateRowWithValue = function(v) {
	var i, ary, row, selectedRow = null, buttons, rows = this.tbody.getElementsByTagName("tr");
	
	if (rows.length === 0) {
		return;
	}	// empty table, nothing to do
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName('input');
		if (buttons[0].value.toString() === v.toString()) {
			selectedRow = i;	// OK, a match
			break;
		}
	}
	if (selectedRow !== null) {
		row = rows[selectedRow];
		ary = row.getElementsByTagName('td');
		for (i=1; i<arguments.length; i++) {
			ary[i].innerHTML = arguments[i];
		}
	}
};
/**
 * Appends the table and page block to a parent node
 * @param {Object} parent The DOM element to which this is added
 */
SelectorTable.prototype.appendTo = function(parent) { 
	parent.appendChild(this.elt);
	parent.appendChild(this.pageBlock); 
};
/**
 * Get the index of the currently selected row, zero being first tbody row
 * @param {Boolean} autoselect Optional control of automatic selection of first row if only one present; default true
 * @returns {number} The row index, or null if no row is selected.
 */
SelectorTable.prototype.getSelected = function(autoselect) {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');

	if (autoselect === undefined) {
		autoselect = true;
	}	
	if (rows.length === 0) {
		return null;
	}
	if (autoselect && this.rows < 2) {
		return 0; 		// auto-select first row
	}
	for (i=0; i<this.rows; i++) {
		buttons = rows[i].getElementsByTagName("input");
		if (buttons[0].checked) {
			return i;
		}
	}
	return null;
};
/**
 * Select the specified row, if possible
 */
SelectorTable.prototype.setSelected = function(row) {
	var buttons, rows = this.tbody.getElementsByTagName('tr');
	if (row < rows.length) {
		buttons = rows[row].getElementsByTagName("input");
		buttons[0].checked = true;
	}
};
/**
 * Get the value of the button of the currently selected row. Requires use of addRowWithValue.
 * @param {Boolean} autoselect Optional control of automatic selection of first row if only one present; default true
 * @returns {mixed} The value of the selector button, or null if no row is selected.
 */
SelectorTable.prototype.getSelectedValue = function(autoselect) {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (autoselect === undefined) {
		autoselect = true;
	}	
	if (rows.length === 0) {
		return null;
	}
	if (autoselect && rows.length < 2) {
		buttons = rows[0].getElementsByTagName("input");	// always use first
		return buttons[0].value;
	}
	for (i=0; i<this.rows; i++) {
		buttons = rows[i].getElementsByTagName("input");
		if (buttons[0].checked) {
			return buttons[0].value;
		}
	}
	return null;
};
/**
 * Get the value of the button of the specified row. Requires use of addRowWithValue.
 * @param {number} row The data row of interest, NOT the one selected.
 * @returns {mixed} The value of the selector button, or null if nonexistent.
 */
SelectorTable.prototype.getRowValue = function(row) {
	var buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (row < rows.length) {
		buttons = rows[row].getElementsByTagName("input");
		return buttons[0].value;
	}
	return null;
};
/**
 * Get the contents of one header element for a specified row and column
 * @param {number} row The row of interest, zero being first thead row (usually only one)
 * @param {number} col The column of interest
 * @returns {mixed} The 'th' value at that row and column or null if not found
 */
SelectorTable.prototype.getHeaderValue = function(row,col) {
	var rows = this.thead.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	var dataItems = rowOfInterest.getElementsByTagName('th');
	if (dataItems[col]) {
		return dataItems[col].innerHTML;
	}
	return null;	// passed end of table, probably
};
/**
 * Get the contents of one data element for a specified row and column
 * @param {number} row The row of interest, zero being first tbody row
 * @param {number} col The column of interest
 * @returns {mixed} The 'td' value at that row and column or null if not found
 */
SelectorTable.prototype.getDataValue = function(row,col) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	var dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		return dataItems[col].innerHTML;
	}
	return null;	// passed end of table, probably
};
/**
 * 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
 */
SelectorTable.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;
	}
};
/**
 * Replace the contents of specified row and column with an input element, and give the
 * element the class inputElement in addition to any classes it already has.
 * @param {number} row The row of interest, zero being first tbody row
 * @param {number} col The column of interest
 * @param {string} val The new value (an input element)
 */
SelectorTable.prototype.setInputElement = function(row,col,val) {
	var rows, rowOfInterest, dataItems;
	
	rows = this.tbody.getElementsByTagName('tr');
	rowOfInterest = rows[row];
	// make sure the row exists
	if (!rowOfInterest) {
		return;
	}
	dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		val.appendTo(dataItems[col]);
		dataItems[col].className += " inputElement";
	}
};
/**
 * 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
 * @param {string} width Optional width specifier
 */
SelectorTable.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>';
		}
	}
};
/**
 * Get the contents of one table element from the currently selected row
 * @param {number} col The column of interest
 * @returns {array} The 'td' value at that row and column, null if nonexistent
 */
SelectorTable.prototype.getSelectedColumn = function(col) {
	var i, columns, buttons, rows = this.tbody.getElementsByTagName('tr'), row = null;
	
	if (rows.length === 0) {
		return null;
	}
	if (this.rows < 2) {
		row = 0;
	} else {
		for (i=0; i<rows.length; i++) {
			buttons = rows[i].getElementsByTagName("input");
			if (buttons[0].checked) {
				row = i;
				break;
			}
		}
	}
	if (row === null) {	// none selected
		return null;
	}
	row = rows[row];
	columns = row.getElementsByTagName('td');
	if (col >= columns.length) {
		return null;
	}
	return columns[col].innerHTML;
};
/**
 * Set the background colors for the table object
 * @param {mixed} normal The color to use for the non-selected rows
 * @param {mixed} selected The color to use for the selected row
 */
SelectorTable.prototype.setColors = function(normal,selected) {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (rows.length === 0) {
		return;
	}
	if (this.rows < 2) {
		return;
	}
	for (i=0; i<this.rows; i++) {
		buttons = rows[i].getElementsByTagName('input');
		if (buttons[0].checked) {
			rows[i].style.backgroundColor = selected;
		} else {
			rows[i].style.backgroundColor = normal;
		}
	}
};
/**
 * Set the text color for one table element
 * @param {number} row The row of interest, zero is first tbody row
 * @param {number} col The column of interest
 * @param {mixed} selected The color to use for the selected element
 * @returns true if the row and column were found, false otherwise
 */
SelectorTable.prototype.setTextColor = function(row,col,color) {
	var rows = this.tbody.getElementsByTagName('tr');
	if (row > rows.length) { return false; }	// no such row in table
	// Use the selected row to determine if the col is in range
	var cols = rows[row].getElementsByTagName('td');
	if (col > (cols.length - 1)) { return false; }
	var elt = cols[col];
	elt.style.color = color;
	return true;
};
/**
 * Set the class for a column
 * @param {number} col The column of interest
 * @param {string} style The CSS class name
 */
SelectorTable.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);
		}
	}
};
/**
 * Set the class for a cell
 * @param {number} row The row of interest
 * @param {number} col The column of interest
 * @param {string} style The CSS class name
 */
SelectorTable.prototype.setCellClass = function(row,col,style) {
	if (row < this.rows) {
		var rows = this.tbody.getElementsByTagName('tr');
		var cols = rows[row].getElementsByTagName("td");
		if (col < cols.length) {
			cols[col].setAttribute('class',style);
		}
	}
};
/**
 * Hide a specific row of the table
 */
SelectorTable.prototype.hideRow = function(row) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return;
	}
	rowOfInterest.style.display = "none";
};
/**
 * Return display state of specified row
 */
SelectorTable.prototype.rowIsDisplayed = function(row) {
	var rows = this.tbody.getElementsByTagName('tr');
	var rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return false;
	}
	return rowOfInterest.style.display !== "none";
};
/**
 * Change the display attribute for all rows except the selected one to hidden.
 */
SelectorTable.prototype.hideUnselected = function() {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');

	if (rows.length === 0) {
		return; 
	}
	if (this.rows < 2) {
		return;
	}
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName("input");
		if (!buttons[0].checked) {
			rows[i].style.display = "none";
		}
	}
};
/**
 * 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.
 */
SelectorTable.prototype.makeSortable = function() {
	var i, 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 i, j, 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 (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 (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 (i=0; i<sortRow.cells.length; i++) {
		sortRow.cells[i].sTable = this;
		sortRow.cells[i].onclick = sortClickHandler;
	}
};