Clientside sorting of HTML TABLE in JavaScript

To save network bandwidth and server resources, it is often beneficial to to sorting of tabular data on the client. Here’s a workable solution that I’ve implemented several times.

Additionally, the need to make the solution ‘accessible’ to screen-reader technology and be backward compatible for users without JavaScript often become challenging.

There are some small “quirks” that you should be aware of…

  • The ‘sortTable’ method uses the ID of the ‘TBODY’. Working to remove this requirement through better use of the DOM.
  • Will likely rework this to use Prototype framework which should result in smaller code.
  • Future enhancement will add a class to the header indicating the sort order of the column(s).
  • Creating the ‘SPAN’ for dates to be sorted is best handled by a taglib.
  • Large TABLE’s can take a significant amount of time to sort on a client, so it’s sometimes better to use a server side solution. Developers should use their experience to make this judgment call.

Example code (XHTML logic removed as usual for brevity):

<html>
<head>
<title>Client Side TABLE sorting</title>
<style type=”text/css”>
/* SCROLL */
div.scroll {width:100%;overflow:scroll;}
html>body div.scroll {width:100%;overflow:scroll} /* fixes IE6 hack */
/*** TABULAR ***/
tr.even {background-color:#eee;}
th.first, td.first {border-width:0;}
th.memo {text-align:left;padding:0;}
td.sorted {background-color: #f0f0f0;}
th.sorted {background-color: #99f;}
tr.even td.sorted { background-color: #d0d0d0; }
table.sorted tr.error { background-color:red; }
table.sorted tr.scroll th { background-color:#99f; text-align:left;}
table.sorted tr.scroll th a.sorted { color:#fff; text-decoration:none;}
table.sorted tr.scroll th a.sorted:hover { color:#fff; text-decoration:underline;}
</style>
<script type=”text/javascript”>
//—————————————————————————–
// sortTable(id, col, rev,xcase)
//
// id – ID of the TABLE, TBODY, THEAD or TFOOT element to be sorted.
// col – Index of the column to sort, 0 = first column, 1 = second column, etc.
// rev – If true, the column is sorted in reverse (descending) order initially.
// xcase – makes sort NOT case sensitive.
//
// The following is an example of jsp code for setting up the reformatted copy of a date
// field to allow sorting by date:
//
// <fmt:formatDate var=”varSortDate” value=”${searchResult.dateOfBirth}” pattern=”${sortableDateTimePattern}” />
// <span title=”<c:out value=”${varSortDate}” />”><fmt:formatDate value=”${searchResult.dateOfBirth}” pattern=”${dateFormatPattern}” /></span> | etc.
//
// The above code creates html code that contains, for example, a line like the following:
//
// <span title=”19640101000000″>01/01/1964</span>
//
// This sort routing concatenates the title element and the text node
// content to sort on the following string:
//
// 1964010100000001/01/1964
//
// This effective ignores the date containing slashes and used the yyyyMMdd etc. value.
// Fields that are not dates should not use the span element and title attribute, unless
// it is desired to sort on something other than the text node content.
//—————————————————————————–
function sortTable(id, col, rev, xcase) {
// Get the table or table section to sort.
var tblEl = xgetHelper(id);
if(tblEl != null){
// The first time this function is called for a given table, set up an array of reverse sort flags.
if (tblEl.reverseSort == null) {
tblEl.reverseSort = new Array();
// Also, assume the column zero is initially sorted.
tblEl.lastColumn = 0; // was 1
}

// If this column has not been sorted before, set the initial sort direction.
if (tblEl.reverseSort[col] == null)
tblEl.reverseSort[col] = rev;

// If this column was the last one sorted, reverse its sort direction.
if (col == tblEl.lastColumn)
tblEl.reverseSort[col] = !tblEl.reverseSort[col];

// Remember this column as the last one sorted.
tblEl.lastColumn = col;
// Set the table display style to “none” – necessary for Netscape 6 browsers.
var oldDsply = tblEl.style.display;
tblEl.style.display = “none”;
// Sort the rows based on the content of the specified column using a selection sort.

var tmpEl;
var i, j;
var minVal, minIdx;
var testVal;
var cmp;
for (i = 0; i < tblEl.rows.length – 1; i++) {

// Assume the current row has the minimum value.
minIdx = i;
minVal = getTextValue(tblEl.rows[i].cells[col], xcase);

// Search the rows that follow the current one for a smaller value.
for (j = i + 1; j < tblEl.rows.length; j++) {
testVal = getTextValue(tblEl.rows[j].cells[col], xcase);
cmp = compareValues(minVal, testVal);
// Negate the comparison result if the reverse sort flag is set.
if (tblEl.reverseSort[col])
cmp = -cmp;
// If this row has a smaller value than the current minimum, remember its
// position and update the current minimum value.
if (cmp > 0) {
minIdx = j;
minVal = testVal;
}
}

// By now, we have the row with the smallest value. Remove it from the
// table and insert it before the current row.
if (minIdx > i) {
tmpEl = tblEl.removeChild(tblEl.rows[minIdx]);
tblEl.insertBefore(tmpEl, tblEl.rows[i]);
}
}

// Make it look pretty.
makePretty(tblEl, col);

// Restore the table’s display style.
tblEl.style.display = oldDsply;
}

return false;
}

//—————————————————————————–
// Functions to get and compare values during a sort.
//—————————————————————————–

// This code is necessary for browsers that don’t reflect the DOM constants
// (like IE).
if (document.ELEMENT_NODE == null) {
document.ELEMENT_NODE = 1;
document.TEXT_NODE = 3;
}

function getTextValue(el, xcase){
var i;
var s;
var spanTitleValue;

// Find and concatenate the values of all text nodes contained within the element.
s = “”;

for (i = 0; i < el.childNodes.length; i++) {
if (el.childNodes[i].nodeType == 1) {
if (el.childNodes[i].nodeName != null) {
if (el.childNodes[i].nodeName == “SPAN”) {
spanTitleValue = el.childNodes[i].getAttribute(“Title”);
s += spanTitleValue;
}
}
else {
}
// Use recursion to get text within sub-elements.
s += getTextValue(el.childNodes[i]);
}
else if (el.childNodes[i].nodeType == document.TEXT_NODE) {
s += el.childNodes[i].nodeValue;
}
else {
// Gets here when element is empty! <span title=””></span>
//alert(‘Error — Not element or text node’);
}
}
return normalizeString(s, xcase);
}

function compareValues(v1, v2) {

var f1, f2;
// If the values are numeric, convert them to floats.

f1 = parseFloat(v1);
f2 = parseFloat(v2);
if (!isNaN(f1) && !isNaN(f2)) {
v1 = f1;
v2 = f2;
}

// Compare the two values.
if (v1 == v2)
return 0;
if (v1 > v2)
return 1
return -1;
}

// Regular expressions for normalizing white space.
var whtSpEnds = new RegExp(“^\\s*|\\s*$”, “g”);
var whtSpMult = new RegExp(“\\s\\s+”, “g”);

function normalizeString(s, xcase) {

s = s.replace(whtSpMult, ” “); // Collapse any multiple whites space.
s = s.replace(whtSpEnds, “”); // Remove leading or trailing white space.

var rc = s;
if(xcase == true) {
rc = s.toUpperCase();
}
return rc;
}

//—————————————————————————–
// Functions to update the table appearance after a sort.
//—————————————————————————–

// Style class names.
var rowClsNm = “even”;
var colClsNm = “sorted”;

// Regular expressions for setting class names.
var rowTest = new RegExp(rowClsNm, “gi”);
var colTest = new RegExp(colClsNm, “gi”);

function makePretty(tblEl, col) {
var i, j;
var rowEl, cellEl;

// Set style classes on each row to alternate their appearance.
for (i = 0; i < tblEl.rows.length; i++) {
rowEl = tblEl.rows[i];
rowEl.className = rowEl.className.replace(rowTest, “”);
if (i % 2 != 0)
rowEl.className += ” ” + rowClsNm;
rowEl.className = normalizeString(rowEl.className);
// Set style classes on each column (other than the name column) to
// highlight the one that was sorted.
for (j = 0; j < tblEl.rows[i].cells.length; j++) { /* was j=2 */
cellEl = rowEl.cells[j];
cellEl.className = cellEl.className.replace(colTest, “”);
if (j == col)
cellEl.className += ” ” + colClsNm;
cellEl.className = normalizeString(cellEl.className);
}
}

// Find the table header and highlight the column that was sorted.
var el = tblEl.parentNode.tHead;
rowEl = el.rows[el.rows.length – 1];
// Set style classes for each column as above.
for (i = 2; i < rowEl.cells.length; i++) {
cellEl = rowEl.cells[i];
cellEl.className = cellEl.className.replace(colTest, “”);
// Highlight the header of the sorted column.
if (i == col)
cellEl.className += ” ” + colClsNm;
cellEl.className = normalizeString(cellEl.className);
}
}
function xgetHelper(id){
var obj = null;
try {
obj = document.getElementById(id);
} catch(z) {
//var dummy=alert(“Error:” + z);
}
return obj;
}
</script>
</head>
<body>
<fieldset>
<div id=”ex_div” class=”scroll”>
<table summary=”” id=”names” class=”sorted” cellspacing=”0″>
<colgroup>
<col style=”first labels” />
<col style=”form_fields” />
</colgroup>
<thead>
<tr class=”scroll”>
<th scope=”col” id=”ex_0″><a href=”javascript:void(0);” class=”sorted” onclick=”return sortTable(‘ex_sort’,0,true,true);”>Alpha</a></th>
<th scope=”col” id=”ex_1″><a href=”javascript:void(0);” class=”sorted” onclick=”return sortTable(‘ex_sort’,1,true,true);”>Date</a></th>
<th scope=”col” id=”ex_2″><a href=”javascript:void(0);” class=”sorted” onclick=”return sortTable(‘ex_sort’,2,true,true);”>Url</a></th>
</tr>
</thead>
<tbody class=”scroll” id=”ex_sort”>
<tr class=”even”>
<td headers=”ex_0″>Alpha</td>
<td headers=”ex_1″><span title=”20070701″>July 2, 2007</span></td>
<td headers=”ex_2″><a href=”javascript:void(0);” onclick=”alert(‘view.php?userid=6’);”>5</a></td>
</tr>
<tr class=”odd”>
<td headers=”ex_0″>Bravo</td>
<td headers=”ex_1″><span title=”20050903″>Sept 3, 2005</span></td>
<td headers=”ex_2″><a href=”javascript:void(0);” onclick=”alert(‘view.php?userid=8’);”>4</a></td>
</tr>
<tr class=”even”>
<td headers=”ex_0″>Charlie</td>
<td headers=”ex_1″><span title=”19700709″>July 9, 1970</span></td>
<td headers=”ex_2″><a href=”javascript:void(0);” onclick=”alert(‘view.php?userid=4’);”>2</a></td>
</tr>
<tr class=”odd”>
<td headers=”ex_0″>Delta</td>
<td headers=”ex_1″><span title=”20001213″>Dec. 13, 2000</span></td>
<td headers=”ex_2″><a href=”javascript:void(0);” onclick=”alert(‘view.php?userid=5’);”>3</a></td>
</tr>
<tr class=”even”>
<td headers=”ex_0″>Echo</td>
<td headers=”ex_1″><span title=”20010911″>Sept 11, 2001</span></td>
<td headers=”ex_2″><a href=”javascript:void(0);” onclick=”alert(‘view.php?userid=2’);”>1</a></td>
</tr>
</tbody>
</table>
</div>
</fieldset>
</body>
</html>

Cheers!

Open Source Media Player

There’s a lot of free software out there, but just like with Instant Messaging software you have to install many of them to be flexible.

Examples:

Several years ago I stumbled upon the VideoLAN Client (aka VLC) when researching methods to stream video from my ReplayTV (Tivo predecessor/clone) to my laptop as the files were in MPEG4 format and my previous clients didn’t support it.

I urge you to check it out as it’s an all in one solution, so that you don’t need all of that other bloat-ware installed.

G’day!

P.S. – I should add that this plays most video and audio formats and is available on most platforms, not just Windows.

Free Instant Messaging (IM) Clients

As indicated way back in my ‘Contact Information’ entry, I use several IM client networks. To make it easier on myself (and my poor computers), it’s usually easier to get an integrated client that connect to many services within one program.

My favorites:

For an even easier way to connect without software installations and to bypass many corporate proxy’s and/or firewalls is to use a web client.

My favorite (as I’m not aware of any others that are still online):

TTYL!

Custom JavaScript error notification

Debugging JavaScript errors is a time-consuming effort requiring keen eyes and a sharp mind.

MSIE typically only gives a cryptic ‘Object Expected’ error message and little more (even with the Microsoft Script Debugger installed!).

Some tools like FireBug and the Venkman debugger (both for Mozilla/Firefox) help in this matter, but often it helps to have an alert when an issue occurs.

Here’s a simple implementation that I’ve found useful…

[script type=”text/javascript”]
window.onerror=myErrorHandler;

function myErrorHandler(msg,url,l){
var txt=”There was an error on this page.\n”;
txt+=”Error: ” + msg + “\n”;
txt+=”URL: ” + url + “\n”;
txt+=”Line: ” + l + “\n\n”;
txt+=”Click OK to continue.\n\n”;
alert(txt); return true; }
[/script]

REFERENCES:

That’s it….

JavaScript (intro)

JavaScript is one of the foundations of the internet as we currently know it, but is often misunderstood. It is the “J” in AJAX (to be discussed elsewhere), and is typically used for creation of interactive browser applications with client-side (browser) functionalities such as FORM validation and manipulation of onscreen elements via the DOM (to be discussed elsewhere).

JavaScript is more appropriately called ECMAScript, as it is a ‘Standard’ from the ECMA organization. Early incarnations of this specification were called LiveScript (by Netscape). Microsoft, in typical form, created a VisualBASIC like version that they called JScript, though while mostly compatible, has some proprietary differences.

Implementation:
It’s always preferred to add this to your HEAD section (or the equivalent in HTTP Headers):

<meta http-equiv=”Content-Script-Type” content=”text/javascript” />

To include external files containing JavaScript:

<script type=”text/javascript” src=”/filename.js”></script>

To include XHTML compliant blocks of JavaScript in your page:

<script type=”text/javascript”>
<!– <![CDATA[

//]]> — >
</script>

DO NOT use the deprecated ‘language’ attribute:

<script language=”JavaScript”>

</script>

P3P 1.0 Implementation guide

Standards documentation is available from W3C at:

NOTES:

  1. Version P3P 1.1 is currently in the works.
  2. Throughout the specifications you’ll see references to “Well-Known Location”, this refers to the default path and naming of these files in the /w3c/ folder.
  3. In my examples below, I have left MOST data empty, the “

xxx” indicates a field that must match between these files.
HTML:


<html>
<head>
<link type="text/xml" rel="P3Pv1" href="/w3c/p3p.xml" />
</head>
<body>
...
</body>
</html>

HTTP Header:

p3p: policyref="/w3c/p3p.xml", CP="TST"

/w3c/p3p.xml:


<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<META xmlns="http://www.w3.org/2002/01/P3Pv1">
<POLICY-REFERENCES>
<POLICY-REF about="/w3c/privacy.xml#xxx">
<INCLUDE>/*</INCLUDE>
<COOKIE-INCLUDE name="*" value="*" domain="*" path="*" />
</POLICY-REF>
</POLICY-REFERENCES>
</META>

/w3c/prixacy.xml


<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<POLICIES xmlns="http://www.w3.org/2002/01/P3Pv1">
<POLICY name="xxx" discuri="/index.html" xml:lang="en">
<ENTITY>
<DATA-GROUP>
<DATA ref="#business.name"></DATA>
<DATA ref="#business.department"></DATA>
<DATA ref="#business.contact-info.postal.name.given"></DATA>
<DATA ref="#business.contact-info.postal.street"></DATA>
<DATA ref="#business.contact-info.postal.city"></DATA>
<DATA ref="#business.contact-info.postal.stateprov"></DATA>
<DATA ref="#business.contact-info.postal.postalcode"></DATA>
<DATA ref="#business.contact-info.postal.country"></DATA>
<DATA ref="#business.contact-info.online.email"></DATA>
<DATA ref="#business.contact-info.telecom.telephone.intcode"></DATA>
<DATA ref="#business.contact-info.telecom.telephone.loccode"></DATA>
<DATA ref="#business.contact-info.telecom.telephone.number"></DATA>
<DATA ref="#business.contact-info.online.uri"></DATA>
</DATA-GROUP>
</ENTITY>
<ACCESS><nonident/></ACCESS>
<DISPUTES-GROUP>
<DISPUTES resolution-type="service" service="/index.html" short-description="Customer Service">
<LONG-DESCRIPTION></LONG-DESCRIPTION>
<REMEDIES><correct/></REMEDIES>
</DISPUTES>
</DISPUTES-GROUP>
<STATEMENT>
<CONSEQUENCE>We record some information in order to serve your request and to secure and improve our Web site.</CONSEQUENCE>
<PURPOSE><current/><develop/><admin/></PURPOSE>
<RECIPIENT><ours/></RECIPIENT>
<RETENTION><stated-purpose/></RETENTION>
<DATA-GROUP>
<DATA ref="#dynamic.clickstream"/>
<DATA ref="#dynamic.http"/>
</DATA-GROUP>
</STATEMENT>
</POLICY>
</POLICIES>

REFERENCES:

  • http://www.w3.org/TR/2000/CR-P3P-20001215/
  • http://msdn.microsoft.com/en-us/library/ie/ms537343%28v=vs.85%29.aspx#unsatisfactory_cookies