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