/****************************************
TableSorter component - version 1.15.
*****************************************/
// Constructor.
// [optional] - specifies a particular table on a page. This can be either a value related to
// a specific
tag or simply the numerical order of the table
// on the page (i.e. 1, 2, ..., n).
function TableSorter() {
// These DOM constants aren't defined by some browsers (namely, IE), so define them here.
if (document.ELEMENT_NODE == null) {
document.ELEMENT_NODE = 1;
document.TEXT_NODE = 3;
}
// Private member variables.
var _ASC_ = '5'; // Pseudo-constant
var _DESC_ = '6'; // Pseudo-constant
var m_tid = ((arguments.length > 0) ? arguments[0] : 1);
var m_default = false;
var m_lastCol = null;
var m_lastSeq = 'A';
var m_initialized = false;
var m_tBodies = null;
var m_spanElement = null;
// Private Methods.
// Mutators.
var setDefault = function() { m_default = true; }
var resetDefault = function() { m_default = false; }
var setLastCol = function(col) { m_lastCol = col; }
var setLastSeq = function() { m_lastSeq = (isAscending() ? 'D' : 'A'); }
var resetLastSeq = function() { m_lastSeq = 'A'; }
var setDefaultSeq = function(seq) { m_lastSeq = seq; }
var setInitialized = function() { m_initialized = true; }
var setTBodiesRef = function(ref) { m_tBodies = ref; }
var setSpanElement = function(element) { m_spanElement = element; }
// Accessors.
var getTableBinding = function() { return m_tid; }
var isDefault = function() { return m_default; }
var getLastCol = function() { return m_lastCol; }
var isAscending = function() { return ((m_lastSeq == 'A') ? true : false); }
var isInitialized = function() { return m_initialized; }
var getTBodiesRef = function() { return m_tBodies; }
var getSpanElement = function() {
if (!m_spanElement) { createIndicatorSpan(); }
return m_spanElement;
}
// JavaScript doesn't understand commas in numbers. Remove all of the commas before comparison.
var removeCommas = function(arg) { return arg.replace(/\,/g, ""); }
// Remove any spaces from the number string. This will usually be in the form of leading spaces.
var removeSpaces = function(arg) { return arg.replace(/\s/g, ""); }
// Processes negative numbers marked with "( )" by temporarily "removing" the parentheses from
// the sort value. It then make s the number negative using a standard "-" sign. In the case of
// currency, the minus sign is inserted between the currency symbol and the value.
var replaceParens = function(val, type) {
if (val.charAt(0) == "(") {
val = val.replace(/[\(\)]/g, "");
if (type == 'currency') { val = (val.charAt(0) + "-" + val.substr(1)); }
else { val = "-" + val; }
}
return val;
}
// Recurses upward until it finds the tag. If it does not find a tag before
// it finds the tag (meaning no table is defined or is ill-defined), it exits.
// If it finds a proper table defintion, it returns a reference to the tag containing
// the clicked column..
// Note: XHTML specifies that ALL tables properly specify the , (optional), and
// structure. The methods in this object will not work unless the table is "well-formed".
var getTableRef = function(colRef) {
var tableRef = colRef.parentNode;
while (tableRef.nodeName.toLowerCase() != "table") {
tableRef = tableRef.parentNode;
if (tableRef.nodeName.toLowerCase() == "body") {
tableRef = null;
break;
}
}
return tableRef;
}
// Uses the bound table ID to get a reference to the actual table, either by ID or sequence.
var getTableRefByID = function(tid) {
var tableRef = document.getElementById(tid);
if (!tableRef) {
var tables = document.getElementsByTagName('table');
if (tables) { tableRef = ((tid <= tables.length) ? tables[tid - 1] : null); }
}
return tableRef;
}
// Returns a reference to a collection of tags for the table clicked.
var getTableBodies = function(colRef) {
var tableRef = getTableRef(colRef);
return ((tableRef && tableRef.tBodies) ? tableRef.tBodies : null);
}
// This node gets created once and a reference is stored. After that, it is just moved
// to the appropriate parent and re-attached.
var createIndicatorSpan = function() {
var newElement = document.createElement('span');
var newTextElement = document.createTextNode("");
newElement.setAttribute('className', 'indicator');
newElement.appendChild(newTextElement);
setSpanElement(newElement);
}
// Sets an indicator on the sorted column to show the direction of the sort.
var setIndicator = function(colRef) {
var lastCol = getLastCol();
if (!lastCol) { colRef.appendChild(getSpanElement()); }
else if (colRef != lastCol) {
colRef.appendChild(lastCol.removeChild(lastCol.lastChild));
}
colRef.lastChild.firstChild.nodeValue = ((isAscending()) ? _ASC_ : _DESC_);
}
// Used to get and retain a reference to the sortable rows area of a table so that it doesn't
// need to be resolved everytime it's needed - the tBody reference is used extensively. This
// method is called only once at the first sort request.
var init = function(col) {
var retVal = true;
if (!isInitialized()) {
retVal = false;
var tBodies = getTableBodies(col);
if (tBodies) {
if (tBodies[0].rows.length > 1) {
setTBodiesRef(tBodies[0]);
setInitialized();
retVal = true;
}
}
}
return retVal;
}
// Retrieves the information from the table to sort.
var readRows = function(col, type) {
var rowSet = getTBodiesRef().rows;
var numRows = rowSet.length;
var rowData = new Array(numRows);
for (var i = 0; i < numRows; i++) {
var row = rowSet[i];
var val = getValueByType(row, col, type);
if (val != null) {
rowData[i] = { value: val, row: row };
}
else {
rowData = null;
break;
}
}
return rowData;
}
// Loop through the sorted row data and rewrite it into the table.
var writeRows = function(data) {
var tBody = getTBodiesRef();
var len = data.length;
for (var i = 0; i < len; i++) { tBody.appendChild(data[i].row); }
}
// This method "compiles" the text information from a given node (data cell) for comparison.
// It is generic and will "find" the proper information regardless of whether or not the table
// structure undergoes changes. It calls itself recursively until the data is found.
var getRawCellData = function(node) {
var tag = (((arguments.length > 1) && arguments[1]) ? arguments[1].toLowerCase() : null);
var children = node.childNodes;
var len = children.length;
var value = "";
for (var i = 0; i < len; i++) {
switch (children[i].nodeType) {
case document.ELEMENT_NODE :
var child = children[i].nodeName.toLowerCase();
if (tag && (tag == 'src') && (child == 'img')) { value = children[i].src; }
else if (tag && (tag == 'href') && (child == 'a')) { value = children[i].href; }
else if (child == "br") { value += " "; }
else { value += getRawCellData(children[i], tag); }
break;
case document.TEXT_NODE :
value += children[i].nodeValue;
break;
default :
break;
}
}
return value;
}
// Traffic cop method for readability. Directs the processing to the proper method.
var getValueByType = function(row, col, type) {
var val = null;
if ((type == 'number') || (type == 'currency') || (type == 'date')) {
val = getNumberValue(row, col, type);
}
else if ((type == 'textcase') || (type == 'textignorecase') ||
(type == 'icon') || (type =='link') || (type == 'url')) {
val = getTextValue(row, col, type);
}
else { alert("Invalid data type: '" + type.toUpperCase() + "' is not supported."); }
return val;
}
// Processes numerical data. This method handles integer and decimal entries with both
// positive and negative values (using "-" or "( )" to show negativity).
// Also handles scientific notation and values containing commas.
var getNumberValue = function(row, col, type) {
var node = row.cells[col.cellIndex];
var value = getRawCellData(node);
value = replaceParens(value, type);
switch (type) {
// Ignores the currency designation and sorts in numerical order.
case 'currency' :
value = value.substr(1);
break;
// Ignores the format and sorts in chronological order. Supports any formats
// supported by java.text.SimpleDateFormat. Also supports "-" as separator.
case 'date' :
value = value.replace(/\-/g, "/");
value = (Date.parse(value)).toString();
break;
default :
break;
}
value = removeSpaces(removeCommas(value));
// This handles the case of "empty" data cells (Example: | ).
var re = /[\+\-]?(\d*\.?)?(\d+)(e[\+\-]?\d+)?/i;
value = ((re.test(value)) ? Number(value) : Number.POSITIVE_INFINITY);
return value;
}
// Compares alpha and alphanumeric data in ASCII order by default. Uppercase letters have
// a lower ASCII value and will therefore appear prior to any lowercase in the sort order.
// (e.g. 'Z', 'g', 'a', 'F' will be sorted ascending as 'F', 'Z', 'a', 'g').
//
// Compares alpha and alphanumeric data with no regard for case. ('A' === 'a') for all
// other text types. (e.g. 'Z', 'g', 'a', 'F' are sorted ascending as 'a', 'F', 'g', 'Z').
var getTextValue = function(row, col, type) {
var node = row.cells[col.cellIndex];
var value = null;
switch (type) {
// Handles reading actual URL values in a hyperlink. Link data is handled by default
// and needs no special processing.
case 'url' :
value = getRawCellData(node, 'href');
break;
// Used to goup similar icons together. Uses only the image name itself, not
// the entire file path. This allows images to be stored anywhere in the system
// and still sort appropriately.
case 'icon' :
value = getRawCellData(node, 'src');
var pos = value.lastIndexOf('/');
value = (pos ? value.substr(pos + 1) : value);
break;
// All other text types are handled here (incl. link text).
default :
value = getRawCellData(node)
break;
}
if (type != 'textcase') { value = value.toLowerCase(); }
return value;
}
// This is a generic compare function used by the Array.sort() method.
// It handles all currently supported data types.
var getComparator = function() {
return function compare(row1, row2) {
var a = row1.value;
var b = row2.value;
// If comparing two "empty" numeric cells to each other, just assert they are equal.
if ((a == Number.POSITIVE_INFINITY) && (b == Number.POSITIVE_INFINITY)) { return 0; }
else if (isAscending()) { return ((a < b) ? -1 : ((a > b) ? 1 : 0)); }
else { return ((b < a) ? -1 : ((b > a) ? 1 : 0)); }
}
}
// Public Methods
// This is a public interface to the TableSorter for setting intial defaults for a table
// during load. This allows a table to be displayed intially, pre-sorted on a given column.
// It has two required parameters and one optional parameter.
//
// defCol - the column number for the actual column in the table
// colType - specifies the data type to sort. (see sortColumn() for details).
// [optional] - specifies the default sort direction. (Values: 'D' or 'A').
// Defaults to 'A' (ascending) if third argument is omitted.
this.setDefaultSort = function(defCol, colType) {
setDefault();
if (arguments.length > 2) {
setDefaultSeq((arguments[2].charAt(0).toUpperCase() == 'D') ? 'D' : 'A');
}
// If the specified table is not found by ID or sequence, just return.
var tableRef = getTableRefByID(getTableBinding());
if (tableRef) {
var cols = tableRef.tHead.rows[0].cells;
if (cols) {
var colRef = ((defCol <= cols.length) ? cols[defCol - 1] : null);
if (colRef) { this.sortColumn(colRef, colType); }
}
}
resetDefault();
return;
}
// This is the public sorting interface to the TableSorter. It has two required parameters.
// If the table is malformed (no ) or there is one row or less, nothing to sort.
//
// clickedCol - usually passed as 'this', is a pointer to the actual column in the table
// colType - specifies the data type to sort. (Values: 'number', 'textcase',
// 'textignorecase', 'currency', 'date', 'link', 'url', and 'icon')
// More types can be added and new Comparators created if necessary (e.g. hybrids)
// Defaults to null (case-inspecific text order).
this.sortColumn = function (clickedCol, colType) {
if (!init(clickedCol)) { return; }
if (clickedCol == getLastCol()) { setLastSeq(); }
else if (!isDefault()) { resetLastSeq(); }
var rowData = readRows(clickedCol, colType);
if (!rowData) { return; }
rowData.sort(getComparator());
writeRows(rowData);
setIndicator(clickedCol);
setLastCol(clickedCol);
}
}