/**
* @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;
}
};