The table sorting script was designed to be as fast and as self contained as possible. That is partly achieved by applying a technique of nesting objects so that most functions are inner functions or inner functions of inner functions. Unfortunately the resulting code takes some understanding and this script is too big to explain in sufficient detail. Hopefully I have provided enough information to enable its use and my article on private static members in JavaScript should provide some explanation of the structure of the code.
<script type="text/javascript" src="scripts/Finalizer.js"> </script>
This script is dependent on my Finalizer script.
<script type="text/javascript">
var SortColumn = function(){
var defaultDirection = 'down'; //'up'|'down'
function getSortFnc(type, index){
var regEx;
switch(type){
case 'byDate':
regEx = /\-/g;
return function (n1,n2){
var d1 = n1.cells[index];
var d2 = n2.cells[index];
if(!d1.sortKey)d1.sortKey =
Date.parse(getInnerText(d1).replace(regEx, '/'));
if(!d2.sortKey)d2.sortKey =
Date.parse(getInnerText(d2).replace(regEx, '/'));
var dif = d1.sortKey - d2.sortKey;
if(dif < 0){
return -1;
}else if(dif > 0){
return 1;
}else{
return 0;
}
};
Do not trust this sort-by-Date implementation because the interpretation of the string argument to Date.parse is implementation dependent and may vary due to local settings.
case 'byNoCase':
return function(n1,n2){
var d1 = n1.cells[index];
var d2 = n2.cells[index];
if(!d1.sortKey)d1.sortKey =
getInnerText(d1).toUpperCase();
if(!d2.sortKey)d2.sortKey =
getInnerText(d2).toUpperCase();
if(d1.sortKey < d2.sortKey){
Functions used with Array.prototype.sort will be called
for each and every comparison used when sorting an array so they should
do as little work as possible if the sorting operation is going to be
quick. These functions need to retrieve the text within the cell,
process it and then do the comparison with it. To avoid doing the
retrieval and processing on each function call the result is being
assigned to an expando property of the cell called sortKey.
This means that the sortKey only need to be created when
the cell is first compared with another, from then on it is available
as a property of the cell. Avoiding the need to re-call
getInnerText and repeat any following processing
(Date creation, case conversion and string to number conversion.
return -1;
}else if(d1.sortKey > d2.sortKey){
return 1;
}else{
return 0;
}
};
case 'byNumber':
regEx = /[^0-9\.]/g;
return function (n1,n2){
var d1 = n1.cells[index];
var d2 = n2.cells[index];
if(!d1.sortKey)d1.sortKey =
(+getInnerText(d1).replace(regEx, ""));
if(!d2.sortKey)d2.sortKey =
(+getInnerText(d2).replace(regEx, ""));
var dif = d1.sortKey - d2.sortKey;
if(dif < 0){
return -1;
}else if(dif > 0){
return 1;
}else{
return 0;
}
};
case 'byCase':
default:
return function(n1,n2){
var d1 = n1.cells[index];
var d2 = n2.cells[index];
if(!d1.sortKey)d1.sortKey = getInnerText(d1);
if(!d2.sortKey)d2.sortKey = getInnerText(d2);
if(d1.sortKey < d2.sortKey){
return -1;
}else if(d1.sortKey > d2.sortKey){
return 1;
}else{
return 0;
}
};
}
}
The preceding function is arranged to allow the easy modification and extension of the sorting abilities of the script.
function getParent(el, pTagName) {
if(el != null){
if((el.nodeType == 1)&&
(el.tagName.toUpperCase() ==
pTagName.toUpperCase())){
return el;
}else if(el.parentNode){
return getParent(el.parentNode, pTagName);
}
}
return null;
}
function getDecendentByTagName(el, dTagName) {
var ndList;
if(el.getElementsByTagName){
ndList = el.getElementsByTagName(dTagName)
}else if(el.all){
ndList = el.all.tags(dTagName);
}
if(!ndList){
ndList = [];
}else if(typeof ndList.length == 'undefined'){
ndList = [ndList];
}
return ndList;
}
function getInnerTextQ(el){ //IE and Opera 7
return el.innerText;
}
function getInnerTextW(el){//other DOM 2 browsers
var str = "";
for (var i=0; i<el.childNodes.length; i++) {
switch (el.childNodes.item(i).nodeType){
case 1: //ELEMENT_NODE
str +=
arguments.callee(el.childNodes.item(i));
break;
case 3: //TEXT_NODE
str += el.childNodes.item(i).nodeValue;
break;
default:
break;
}
}
return str;
}
var getInnerText;
function SortTable(rowAr, contRow){
function getTableRows(){
var a = [];
for(var c = rowAr.length;c--;){
a[c] = rowAr[c];
}
return a;
}
function SortColumn(index, conCell){
function sortIt(){
direction = (direction == 'up')?'down':'up';
for(var c = contRow.length;c--;){
contRow[c].displayOff();
}
if(!orderedRows)orderedRows =
getTableRows().sort(getSortFnc(type, index));
tableEl.removeChild(parentEl);
if(direction == 'up'){
for(var c = 0;c < orderedRows.length;c++){
parentEl.appendChild(orderedRows[c]);
}
}else{
for(var c = orderedRows.length;c--;){
parentEl.appendChild(orderedRows[c]);
}
}
tableEl.appendChild(parentEl);
arrowParent.replaceChild(
indicator['arrow'+direction],dispNode);
dispNode = indicator['arrow'+direction];
return false;
}
var direction = defaultDirection;
var arrowParent, dispNode, orderedRows;
var type = '';
var indicator = {
arrow:(arrow.cloneNode(true)),
arrowup:(arrowUp.cloneNode(true)),
arrowdown:(arrowDown.cloneNode(true))
};
var ndList = getDecendentByTagName(conCell, 'IMG');
if((ndList)&&(ndList.length > 0)){
dispNode = ndList[0];
arrowParent = dispNode.parentNode;
}else{
arrowParent = conCell;
dispNode = arrowParent.appendChild(indicator.arrow);
}
ndList = getDecendentByTagName(conCell, 'A');
if((ndList)&&(ndList.length > 0)){
for(var c = ndList.length;c--;){
if(typeof ndList[c].onclick == 'function'){
var s = ''+ndList[c].onclick;
var i = s.indexOf('SortColumn');
if(i >= 0){
s = s.substring((i+11),s.length);
type = s.substring(0,s.indexOf('('));
ndList[c].onclick = sortIt;
break;
}
}
}
}
var parentEl = rowAr[0].parentNode;
this.displayOff = function(){
if(indicator.arrow != dispNode){
arrowParent.replaceChild(indicator.arrow,dispNode);
dispNode = indicator.arrow;
}
}
this.finalize = function(){
var ndList = getDecendentByTagName(conCell, 'A');
if((ndList)&&(ndList.length > 0)){
for(var c = ndList.length;c--;){
ndList[c].onclick = null;
}
}
arrowParent = (ndList = (conCell =
(this.arrow =
(this.arrowup =
(this.arrowdown =
(dispNode = null))))));
};
}
var temp;
var arrow = document.createElement("SPAN");
temp = document.createElement("IMG");
temp.src = "images/sortBlank.gif";
temp.alt = (temp.title = "");
arrow.appendChild(temp);
var arrowUp = document.createElement("SPAN");
temp = document.createElement("IMG");
temp.src = "images/sortUp.gif";
temp.alt = (temp.title = "(Sorted Ascending)");
arrowUp.appendChild(temp);
var arrowDown = document.createElement("SPAN");
temp = document.createElement("IMG");
temp.src = "images/sortDown.gif";
temp.alt = (temp.title = "(Sorted Descending)");
arrowDown.appendChild(temp);
temp = [];
for(var c = 0;c < contRow.length;c++){
temp[c] = new SortColumn(c, contRow[c]);
}
contRow = temp;
temp = null;
if(typeof finalizeMe != 'undefined'){
finalizeMe(
function(){
for(var c = contRow.length;c--;){
contRow[c].finalize();
contRow[c] = null;
}
for(var c = rowAr.length;c--;){
rowAr[c] = null;
}
arrowDown =
(arrowUp =
(arrow =
(contRow =
(rowAr = null))));
}
);
}
};
function init(obj){
var switchRow = getParent(obj, "TR");
if((switchRow)&&(switchRow.cells)&&
(switchRow.parentNode)&&
(switchRow.replaceChild)&&
(switchRow.cloneNode)&&
(document.createElement)&&
(switchRow.appendChild)){
if(typeof switchRow.innerText != 'undefined'){
getInnerText = getInnerTextQ;
}else if(typeof switchRow.childNodes != 'undefined'){
getInnerText = getInnerTextW;
}
if(getInnerText){
var contTable = getParent(switchRow, "TABLE");
if(contTable){
var rowArr = [];
var rowList;
var ndList =
getDecendentByTagName(contTable, 'TBODY');
for(var c = ndList.length;c--;){
rowList = getDecendentByTagName(ndList[c], 'TR');
for(var i = rowList.length;i--;){
rowArr[rowArr.length] = rowList[i];
}
}
var switchCells = switchRow.cells;
new SortTable(rowArr, switchCells);
rowList = (ndList = (rowArr = null));
return;
}
}
}
arguments.callee.byCase =
(arguments.callee.byDate =
(arguments.callee.byNoCase =
(arguments.callee.byNumber = function(){return true;})));
}
Notice the amount of feature testing done in the init
function. This follows a principle of initialising the script through
a "gateway" that tests all of the required DOM features to
see if the browser is going to be able to execute it and if not the
init function disables the script by assigning a function
to each item in it's public interface that will just
return true; leaving the browser to fall back on the
navigation specified in the link's href attributes.
init.byNumber = function(obj){
init(obj);
if(arguments.callee == init.byNumber){
/*trigger the sorting for this cell by calling the
dynamically assigned onclick method that is now
attached to the A element.*/
return obj.onclick();
}else{
/*script was disabled so fall back to server side.*/
return true;
}
}
Because the init.byNumber function will be changed to a
harmless function if the script decides to disable itself, comparing
arguments.callee and init.byNumber will tell this function
whether its call to init has resulted in the script being
disabled (so just return true to allow the href
navigation) or not (so call the onclick function and return whatever it
returns).
init.byNoCase = function(obj){
init(obj);
if(arguments.callee == init.byNoCase){
return obj.onclick();
}else{
return true;
}
}
init.byCase = function(obj){
init(obj);
if(arguments.callee == init.byCase){
return obj.onclick();
}else{
return true;
}
}
init.byDate = function(obj){
init(obj);
if(arguments.callee == init.byDate){
return obj.onclick();
}else{
return true;
}
}
return init;
}();
</script>
If another sort function is added to the getSortFnc
function then a corresponding init.byNewCriteria function
would need to be added above, following the same pattern as the existing
ones.
<table>
<thead>
<tr>
<th><a href="serverFallback.html?type=byNoCase"
onclick="return SortColumn.byNoCase(this)">
Case Insensitive
The table sorting is triggered by the onclick event of an
A element. The initial onclick function calls a function
property of the SortColumn function which returns true or
false depending on whether the script is going to execute or fall
back to the server script specified in the href attribute.
Notice that the href attribute is provided with a query
string so that the server code (if it really existed) would know how
to sort the table it returned.
The this argument passed to the sorting function is a
reference to the A element to which the onclick function
is attached. It is used by the code to locate the THEAD
and TABLE that contains it.
Alternatively, the script can be initialised in the BODY
onload event. This requires that the SortColumn function
is called directly and provided with a reference to an element that is
a descendant of the THEAD (the TR is the
ideal candidate). So, if the TR had an ID
attribute "trId" the BODY onload
code would be something like:-
<body onload="if(document.getElementById)SortColumn(document.getElementById('trId');">
Doing this will reduce the initial sorting time because some of the set-up work will have already been done when the table head cells are clicked.
<img src="images/sortBlank.gif"alt=""></a>
</th>
<th><a href="serverFallback.html?type=byCase"
onclick="return SortColumn.byCase(this)">
Case Sensitive
<img src="images/sortBlank.gif"alt=""></a>
</th>
<th><a href="serverFallback.html?type=byNumber"
onclick="return SortColumn.byNumber(this)">
Number<img src="images/sortBlank.gif"alt=""></a>
</th>
<th><a href="serverFallback.html?type=byDate"
onclick="return SortColumn.byDate(this)">
Date<img src="images/sortBlank.gif"alt=""></a>
</th>
</tr>
</thead>
The script differentiates between rows that should be sorted and rows
that should not by assuming that rows in any TBODY elements
are to be sorted and rows within THEAD (and TFOOT)
should not. As a result, the cells (logically TH, else the
"TH" strings in the script will need to be changed)
in the THEAD should contain the controlling links). If multiple
TBODY elements are used the DOM manipulation script will migrate
all rows into the first after the first row sort.
<tbody>
<!-- the table rows go here -->
...
</tbody>
</table>