Source: checkboxTable.js

/**
 * @fileOverview This file defines the CheckboxTable class.
 * Copyright 2008-2015 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
 */
/**
 * @class The CheckboxTable object, a displayable table with a checkbox column and optional sorting (no paging)
 * @constructor
 */
function CheckboxTable(tag) {
	// constructor body
	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','CheckboxTable');
	this.thead = document.createElement('thead');
	this.elt.appendChild(this.thead);
	this.tbody = document.createElement('tbody');
	this.elt.appendChild(this.tbody);	
}
CheckboxTable.prototype.datarows = 0;	// number of data rows in table
CheckboxTable.prototype.searchIndex = 0;	// moves through table as "get next"
CheckboxTable.prototype.row = 0;	// stores number of currently selected row
/**
 * Assigns a CSS class to the table - the default class is CheckboxTable
 * @param {string} clinfo The CSS class to assign to the table's class attribute
 */
CheckboxTable.prototype.setClass = function(clinfo) { this.elt.setAttribute('class',clinfo); };
/**
 * Returns the currently selected row
 * @returns {number} The current row variable
 */
CheckboxTable.prototype.getRow = function() { return this.row; };
/**
 * Adds a header to the table. Uses the anonymous arguments array as input, so no declared args.
 * @param {string[]} (anonymous) The columns headers for the table
 */
CheckboxTable.prototype.addHeader = function() {
	var i,
		tr = document.createElement('tr'),
		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);
};	
/**
 * Adds a data row to the table. Uses the anonymous arguments array as input, so no declared args.
 * @example myTable.addRow('ken','707-555-1212','ken@calast.com');
 * @returns {integer} row index added.
 */
CheckboxTable.prototype.addRow = function() { // should be passed an array of strings
	var i,
		tr = document.createElement('tr'),
		td = document.createElement('td'),
		input = document.createElement('input');
		
	input.setAttribute('type','checkbox');
	input.setAttribute('name',this.tag+'Checkbox[]');
	input.setAttribute('value',this.datarows);	// value is the table index here
	this.datarows += 1;
	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);
	return this.datarows - 1;
};
/**
 * Adds a data row to the table. Uses the anonymous arguments array as input, so no declared args.
 * The first argument is not displayable, but is assigned to the checkbox as a value.
 * @example myTable.addRowWithValue(42,'ken','707-555-1212','ken@calast.com');
 * @returns {integer} row index added.
 */
CheckboxTable.prototype.addRowWithValue = function() { // should be passed an array of strings, 
	// the first of which is value
	var i, 
		tr = document.createElement('tr'),
		td = document.createElement('td'),
		input = document.createElement('input');
		
	input.setAttribute('type','checkbox');
	input.setAttribute('name',this.tag+'Checkbox[]');
	input.setAttribute('value',arguments[0]);
	this.datarows += 1;
	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);
	return this.datarows - 1;
};
/**
 * 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 {string} val The new value
 */
CheckboxTable.prototype.setDataValue = 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]) {
		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)
 */
CheckboxTable.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 div into a cell of the table for later use (e.g., as a graphic element or control)
 * @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 number
 * @param {string} width Optional width specifier
 */
CheckboxTable.prototype.addDiv = function(row,col,val,width) {
	var rows, rowOfInterest, dataItems, sname;
	
	rows = this.tbody.getElementsByTagName('tr');
	rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return;
	}
	dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		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>';
		}
	}
};
/**
 * Appends the table (and page block) to a parent node
 * @param {Object} parent The DOM element node to which this is added
 */
CheckboxTable.prototype.appendTo = function(form) { form.appendChild(this.elt); };
/**
 * Returns the number of rows that have been added to the table
 * @returns {number} The row count
 */
CheckboxTable.prototype.getRowCount = function() { return this.datarows; };
/**
 * Deletes all the data rows in a table by deleting children of tbody
 */
CheckboxTable.prototype.clearBody = function() {
	while (this.tbody.childNodes[0]) {
		this.tbody.removeChild(this.tbody.childNodes[0]);	// always remove first
	}
	this.datarows = 0;
};
/**
 * Sets all the rows in the table to checked
 */
CheckboxTable.prototype.selectAll = function() {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (rows.length === 0) {
		return;
	}
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName("input");
		buttons[0].checked = true;
	}
};
/**
 * Sets all the rows in the table to unchecked
 */
CheckboxTable.prototype.deselectAll = function() {
	var i, buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (rows.length === 0) {
		return;
	}
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName("input");
		buttons[0].checked = false;
	}
};
/**
 * Sets a specific table row to be checked or unchecked
 * @param {integer} row The row to be affected (0..n-1)
 * @param {boolean} turnon Set to checked if true, unchecked otherwise
 */
CheckboxTable.prototype.setChecked = function(row,turnon) {
	var buttons, rows = this.tbody.getElementsByTagName('tr');
	
	// sanity check the row number
	if (rows.length === 0 || row < 0 || row >= rows.length) {
		return;
	}
	buttons = rows[row].getElementsByTagName("input");
	buttons[0].checked = turnon;
};
/**
 * Tests a specific table row to see if it is checked or unchecked
 * @param {integer} row The row to be tested (0..n-1)
 * @returns {boolean} The state of the checkbox, or false if bad row number
 */
CheckboxTable.prototype.isChecked = function(row) {
	var buttons, rows = this.tbody.getElementsByTagName('tr');
	
	// sanity check the row number
	if (rows.length === 0 || row < 0 || row >= rows.length) {
		return false;
	}
	buttons = rows[row].getElementsByTagName("input");
	return buttons[0].checked;
};
/**
 * Get the value of the checkbox of the currently selected row. Requires use of addRowWithValue.
 * For checkbox tables, this method disallows multiple select. It returns null if zero or more
 * than one rows are selected.
 * @returns {mixed} The value of the checkbox, or null if the number of selected rows is not one.
 */
CheckboxTable.prototype.getSelectedValue = function() {
	var i, buttons, selected, id, rows;
	
	rows = this.tbody.getElementsByTagName('tr');
	if (rows.length === 0) {
		return null; 	// no rows in table
	}
	selected = 0;
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName("input");
		if (buttons[0].checked) {
			selected++;
			id = buttons[0].value;
		}
	}
	if ( selected !== 1) {
		return null;	// not legal
	}
	return id;
};
/**
 * Get the value of the checkbox of a selected row. Requires use of addRowWithValue.
 * @param {integer} row The row to be retrieved (0..n-1)
 * @returns {mixed} The value of the checkbox, or null if row does not exist.
 */
CheckboxTable.prototype.getValueForIndex = function(row) {
	var buttons, rows = this.tbody.getElementsByTagName('tr');
	
	if (rows.length === 0 || row >= rows.length) {
		return null;		// no such row
	}
	buttons = rows[row].getElementsByTagName('input');
	return buttons[0].value;
};
/**
 * Get the index of the currently selected row. Counts the header, so first row is 1, not 0.
 * For checkbox tables, this function disallows multiple select. It returns null if zero or more
 * than one rows are selected.
 * @returns {number} The row index, or null if no row is selected.
 */
CheckboxTable.prototype.getSelectedRow = function() {
	var i, buttons, selected, id, rows;
	
	rows = this.tbody.getElementsByTagName('tr');
	if (rows.length === 0) {
		return null; 	// no rows in table
	}
	selected = 0;
	for (i=0; i<rows.length; i++) {
		buttons = rows[i].getElementsByTagName("input");
		if (buttons[0].checked) {
			selected++;
			id = i;
		}
	}
	if ( selected !== 1) {
		return null;	// not legal
	}
	return id+1;	// compensate for the header
};
/**
 * Sets the search index of the table to zero, so it can be re-searched.
 */
CheckboxTable.prototype.resetIndex = function() { this.searchIndex = 0; };
/**
 * This method returns the index of the next checked item, or null if there isn't one. A static
 * index is maintained in the object, so we know which ones we have already seen. We start from
 * just after that point to find the next one. Since a checkboxTable object may contain inputElements 
 * as column values, we look at the inputs in each row, and use the first input as the checkbox.
 * @returns {number} The index of the next selected row, or null if there is no next selected row.
 */
CheckboxTable.prototype.getNextSelectedIndex = function() {
	var i, id, inputs, row,
		rows = this.tbody.getElementsByTagName('tr');
	
	if (this.datarows === 0) {
		return null;	// empty table
	}
	id = null;
	for (i=this.searchIndex;i<this.datarows;i++) {
		row = rows[i];
		inputs = row.getElementsByTagName("input");
		if (inputs[0].checked) {
			id = i;		// found it
		}
		this.row = this.searchIndex;	// so we can get more info later
		this.searchIndex++;	// advance ptr past this point for next call
		if (id !== null) {		// could be zero
			return id;
		}
	}
	return null;	// didn't find any more
};
/**
 * This method returns the value of the next checked item, or null if there isn't one
 * @returns {mixed} The value of the button in the next selected row
 */
CheckboxTable.prototype.getNextSelectedValue = function() {
	var i, id, inputs,
		rows = this.tbody.getElementsByTagName('tr');
	
	if (this.datarows === 0) {
		return null;	// empty table
	}
	id = null;
	for (i=this.searchIndex; i<this.datarows; i++) {
		inputs = rows[i].getElementsByTagName("input");
		if (inputs[0].checked) {
			id = inputs[0].value;
		}
		this.row = this.searchIndex;	// so we can get more info later
		this.searchIndex++;	// advance ptr past this point for next call
		if (id !== null) { return id; }	
	}
	return null;	// didn't find any more
};
/**
 * This method returns the number of checked rows
 * @returns {mixed} The count of checked rows
 */
CheckboxTable.prototype.getSelectedCount = function() {
	var i, ct, inputs,
		rows = this.tbody.getElementsByTagName('tr');

	if (this.datarows === 0) {
		return 0;	// empty table
	}
	ct = 0;
	for (i=0; i<this.datarows; i++) {
		inputs = rows[i].getElementsByTagName("input");
		if (inputs[0].checked) {
			ct++;
		}
	}
	return ct;
};
/**
 * Get the contents of one table header element for a specified row and column
 * @param {number} row The row of interest, zero being the first thead row
 * @param {number} col The column of interest
 * @returns {mixed} The 'th' value at that row and column or null if not found
 */
CheckboxTable.prototype.getHeaderValue = function(row,col) {
	var rows = this.thead.getElementsByTagName('tr'), dataItems,
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return null;
	}
	dataItems = rowOfInterest.getElementsByTagName('th');
	if (dataItems[col]) {
		return dataItems[col].innerHTML;
	}
	return null;	// passed end of table, probably
};
/**
 * Get the class of one table element for a specified row and column
 * @param {number} row The row of interest, zero being the first tbody row
 * @param {number} col The column of interest
 * @returns {mixed} The 'td' class at that row and column or null if not found
 */
CheckboxTable.prototype.getDataClass = function(row,col) {
	var rows, rowOfInterest, dataItems;
	
	rows = this.tbody.getElementsByTagName('tr');
	rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		return dataItems[col].getAttribute("class");
	}
	return null;	// passed end of table, probably
};
/**
 * Get the contents of one table element for a specified row and column
 * @param {number} row The row of interest, zero being the 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
 */
CheckboxTable.prototype.getDataValue = function(row,col) {
	var rows, rowOfInterest, dataItems;
	
	rows = this.tbody.getElementsByTagName('tr');
	rowOfInterest = rows[row];
	if (!rowOfInterest) {
		return null;
	}
	dataItems = rowOfInterest.getElementsByTagName('td');
	if (dataItems[col]) {
		return dataItems[col].innerHTML;
	}
	return null;	// passed end of table, probably
};
/**
 * Collapse a table to show only the selected rows. Does nothing if less than two rows.
 */
CheckboxTable.prototype.hideUnselected = function() {
	var i, buttons,
		rows = this.tbody.getElementsByTagName('tr');
		
	if (rows.length === 0) {
		return;
	}
	if (this.datarows < 2) {
		return; 
	}
	for (i=0; i<this.datarows; i++) {
		buttons = rows[i].getElementsByTagName('input');
		if (!buttons[0].checked) {
			rows[i].style.display = "none";
		}
	}
};
/**
 * Set the background color for one table row, as specified by its index.
 * @param {number} row The row of interest, zero being the first tbody row
 * @param {string} color The new background color for that row
 */
CheckboxTable.prototype.setRowColor = function(row,color) {
	var rows = this.tbody.getElementsByTagName('tr'),
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return;
	}
	rowOfInterest.style.backgroundColor = color;
};
/**
 * Retrieves the background color for one table row, as specified by its index.
 * @param {number} row The row of interest, zero being the first tbody row
 * @returns {string} color The background color for that row, or null if it does not exist
 */
CheckboxTable.prototype.getRowColor = function(row) {
	var rows = this.tbody.getElementsByTagName('tr'),
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return null;
	}
	return rowOfInterest.style.backgroundColor;
};
/**
 * Apply a class to one table row, as specified by its index.
 * @param {number} row The row of interest, zero being the first tbody row
 * @param {string} clinfo The new class for that row
 */
CheckboxTable.prototype.setRowClass = function(row,clinfo) {
	var rows = this.tbody.getElementsByTagName('tr'),
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return;
	}
	rowOfInterest.setAttribute('class',clinfo);
};
/**
 * Set the text style for one table row, as specified by its index, to bold.
 * @param {number} row The row of interest, zero being the first tbody row
 */
CheckboxTable.prototype.setBold = function(row) {
	var rows = this.tbody.getElementsByTagName('tr'),
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return;
	}
	rowOfInterest.style.fontWeight = 'bold';
};
/**
 * Retrieves the font weight for one table row, as specified by its index.
 * @param {number} row The row of interest, zero being the first tbody row
 * @returns {string} weight The font weight for that row, or null if it does not exist
 */
CheckboxTable.prototype.getFontWeight = function(row) {
	var rows = this.tbody.getElementsByTagName('tr'),
		rowOfInterest = rows[row];
		
	if (!rowOfInterest) {
		return null;
	}
	return rowOfInterest.style.fontWeight;
};
/**
 * 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.
 */
CheckboxTable.prototype.makeSortable = function() {
	var i, thisObject = this, sortRow;
	
	// 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; }
	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, bb;
		
		aa = parseFloat(thisObject.getInnerText(a.cells[thisObject.sortColumnIndex]));
		if (isNaN(aa)) { aa = 0; }
		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, bb;
		
		aa = thisObject.getInnerText(a.cells[thisObject.sortColumnIndex]).toLowerCase();
		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 ix, j, newRows,
			column = cell.cellIndex,
			cellContents = this.getInnerText(this.tbody.rows[1].cells[column]),
			sortfn = this.sortAlpha;

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

		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 (ix=0; ix < newRows.length; ix++) {
			this.tbody.appendChild(newRows[ix]);
		}
	};
	/**
	 * 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;
	}
};