/*
* SortableTable.js: sortable HTML table class
* authors: Adam Cath (acath) and Dave Pacheco (dap)
* (both email addresses @cs.brown.edu)
*
* Copyright 2006 by the Mocha team. All rights reserved.
*
* Email authors for permission to use. With enough (any?) demand, we might
* release this under a BSD license.
*/
var UP_ARROW_IMAGE = "up.gif"
var DOWN_ARROW_IMAGE = "down.gif"
/*
* Simple comparator converting the Javascript boolean '<' to something
* returning {-1,0,1}
*/
function SortableTable_lessThanComparator(a,b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
/*
* Create a sortable table with the specified columns.
* @param columns an array of objects with these members:
* - headerNode: DOM node
* The node displayed at the top of the column
* - comparator: (cell datum, cell datum) -> {-1,0,1}
* Compares the data of two cells in this column - follows
* standard Java compareTo semantics. If null, then the table
* cannot be sorted by this column.
* - renderer: (cell datum) -> DOM node
* For the data of a given cell in this column, return a
* DOM object to be displayed as the cell.
*/
function SortableTable(columns) {
this.rows = [];
this.columns = columns;
this.table = document.createElement('table');
this.tbody = document.createElement('tbody');
this.thead = document.createElement('thead');
this.table.appendChild(this.thead);
this.table.appendChild(this.tbody);
this.imgs = new Array(columns.length);
var headerRow = document.createElement('tr');
this.thead.appendChild(headerRow);
for (var ii = 0; ii < columns.length; ii++) {
/*
* We make a copy here because otherwise the client can't reuse this
* column specification.
*/
var cell = columns[ii].headerNode.cloneNode(true);
if (columns[ii].comparator != null) {
cell.style.cursor = "pointer";
/* This is the only way I know how to create a proper closure in JS. */
var listenerGenerator = function (object, column) {
return function (_) {
if (column == object.currentSortColumn) {
object.imgs[column].src = object.currentSortAscending ?
"images/"+DOWN_ARROW_IMAGE : "images/"+UP_ARROW_IMAGE;
object.sort(column, !object.currentSortAscending, false);
} else {
/* we're changing columns */
object.imgs[object.currentSortColumn].style.display = "none";
object.imgs[column].style.display = "inline";
object.imgs[column].src = "images/"+UP_ARROW_IMAGE;
object.sort(column, true, false);
}
}
}
Util_addEvent(cell, 'click', listenerGenerator(this, ii));
/* add up/down arrows */
this.imgs[ii] = document.createElement('img');
this.imgs[ii].style.display = "none";
this.imgs[ii].src = "images/"+UP_ARROW_IMAGE;
cell.appendChild(this.imgs[ii]);
}
headerRow.appendChild(cell);
}
this.currentSortColumn = 0;
this.currentSortAscending = true;
this.bodyRowCssClass = '';
this.imgs[this.currentSortColumn].style.display = "inline";
}
/*
* @return the number of data rows in this table
*/
SortableTable.prototype.getNumRows = function () {
return this.rows.length;
}
SortableTable.prototype.getDomNode = function() {
return this.table;
}
/*
* @param name the CSS class that you want header cells to have
*/
SortableTable.prototype.setHeaderCellCssClass = function(name) {
for (var ii = 0; ii < this.columns.length; ++ii) {
this.thead.rows[0].cells[ii].className = name;
}
}
/*
* @param name the CSS class that you want header rows to have
*/
SortableTable.prototype.setHeaderRowCssClass = function(name) {
this.thead.rows[0].className = name;
}
/*
* @param name the CSS class that you want body rows to have
*/
SortableTable.prototype.setBodyRowCssClass = function(name) {
this.bodyRowCssClass = name;
this.rerender();
}
/*
* @param name the CSS class that you want the whole table to have
*/
SortableTable.prototype.setTableCssClass = function(name) {
this.table.className = name;
}
/*
* Adds the row with the given data.
* @param rowid a unique id for this row, which you can use in "remove" to
* remove this row. If this is not unique, the behavior is undefined.
* @param rowdata an object, sortable by your comparator and renderable
* by your renderer, which contains the data for this row.
*/
SortableTable.prototype.put = function (rowid, rowdata) {
if (this.private_findRow(rowid) < 0) {
this.rows.push({ rowid: rowid, rowdata: rowdata });
}
this.sort(this.currentSortColumn, this.currentSortAscending);
}
/*
* Removes the specified row.
* @param rowid the unique id of the row (used to add it originally in
* "put"). If this is not unique, then the behavior is undefined.
*/
SortableTable.prototype.remove = function (rowid) {
var rowNum = this.private_findRow(rowid);
if (rowNum >= 0) {
this.rows.splice(rowNum, 1);
this.tbody.deleteRow(rowNum);
}
}
/*
* Resorts the table by specified column using this object's comparator for
* the column.
* @param index the column index
* @param bAscending true iff we should sort in ascending order
*/
SortableTable.prototype.sort = function (index, bAscending) {
this.currentSortColumn = index;
this.currentSortAscending = bAscending;
if (this.columns[index].comparator != null) {
var thisHack = this;
this.rows.sort(function(a, b) {
return thisHack.columns[index].comparator(a.rowdata, b.rowdata);
});
if (!bAscending) {
this.rows.reverse();
}
}
this.rerender();
}
SortableTable.prototype.rerender = function () {
while (this.tbody.rows.length > 0) {
this.tbody.deleteRow(0);
}
for (var ii = 0; ii < this.rows.length; ++ii) {
var tr = this.tbody.insertRow(-1);
tr.className = this.bodyRowCssClass;
for (var jj = 0; jj < this.columns.length; ++jj) {
tr.appendChild(this.columns[jj].renderer(this.rows[ii].rowdata));
}
}
}
/*
* Removes all rows from this table.
*/
SortableTable.prototype.clear = function () {
this.rows = [];
while (this.tbody.rows.length > 0) {
this.tbody.deleteRow(0);
}
}
SortableTable.prototype.private_findRow = function(rowid) {
for (var ii = 0; ii < this.rows.length; ii++) {
if (this.rows[ii].rowid == rowid)
return ii;
}
return -1;
}