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