Math Jazz — Mathias Bynens’s shizzle, y’all



Note: This site might seem inactive… That’s because it is. Don’t worry though, I’m still coding webpages and stuff! If you’re interested, I suggest you get a translator and head over to Qiwi; or you could just check the latest site we’ve been working on: Amiloen. Enjoy!

Highlighting alternate sorttable rows

Or, a way how to highlight alternate table rows without breaking the table’s JavaScript-wise sortability. Or the other way around. Whatever.

The scripts

Well, it should be obvious what they do and how they work, but hey.

Needless to say, both these functions kick butt and add usability to your tables.

The problem

When combining these two scripts, the following happens. As soon as the document containing the table is loaded, the colorTableRows() function performs its task. After this is done, sortTable() initializes, adding links to the table headers (i.e. making them clickable). So far, all is well. Until the user decides to click one of those THs in order to sort the table. Try it out yourself, you should notice utter crappiness as for the highlighted table rows.

The solution

…is pretty simple actually, once you realize that since the sorting reorders the rows, we have to reset the highlights after the actual sorting. This translates into a couple of tweaks to the code, of which I’ll spare you the details. Here’s the full source code to the perfect collaboration of these scripts.

/* http://brothercake.com/site/resources/scripts/domready/
**************************************************************************/

function domFunction(f, a) {
 var n = 0;
 var t = setInterval(function() {
  var c = true;
  n++;
  if(typeof document.getElementsByTagName != 'undefined' && (document.getElementsByTagName('body')[0] != null || document.body != null)) {
   c = false;
   if(typeof a == 'object') {
    for(var i in a) {
     if((a[i] == 'id' && document.getElementById(i) == null) || (a[i] == 'tag' && document.getElementsByTagName(i).length < 1)) { 
      c = true; 
      break; 
     }
    }
   }
   if(!c) { f(); clearInterval(t); }
  }
  if(n >= 60) {
   clearInterval(t);
  }
 }, 250);
};

/* http://kryogenix.org/code/browser/sorttable/
**************************************************************************/

var sci;

function sortTable() {
 if (!document.getElementsByTagName) return;
 tbls = document.getElementsByTagName('table');
 for (ti=0; ti<tbls.length; ti++) {
  thisTbl = tbls[ti];
  st_makeSortable(thisTbl);
 }
}

function st_makeSortable(table) {
 if (table.rows && table.rows.length > 0) {
  var firstRow = table.rows[0];
 }
 if (!firstRow) return;
 for (var i=0; i<firstRow.cells.length; i++) {
  var cell = firstRow.cells[i];
  var txt = st_getInnerText(cell);
  cell.innerHTML = '<a href="" class="sortheader" onclick="st_resortTable(this);return false;">'+txt+'<span class="sortarrow"></span></a>';
 }
}

function st_getInnerText(el) {
 if (typeof el == 'string') return el;
 if (typeof el == 'undefined') return el;
 if (el.innerText) return el.innerText;
 var str = '';
 var cs = el.childNodes;
 var l = cs.length;
 for (var i = 0; i < l; i++) {
  switch (cs[i].nodeType) {
   case 1:
    str += st_getInnerText(cs[i]);
    break;
   case 3:
    str += cs[i].nodeValue;
    break;
  }
 }
 return str;
}

function st_resortTable(lnk) {
 var span;
 for (var ci=0; ci<lnk.childNodes.length; ci++) {
  if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
 }
 var spantext = st_getInnerText(span);
 var td = lnk.parentNode;
 var column = td.cellIndex;
 var table = st_getParent(td, 'TABLE');
 if (table.rows.length <= 1) return;
 var notDate = 0;
 var notCurrency = 0;
 var notNumerical = 0;
 for (var itmc=1; itmc<table.rows.length; itmc++) {
  var itm = st_getInnerText(table.rows[itmc].cells[column]);
  if (!(itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/) || (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/))))
   notDate++;
  if (!(itm.match(/^[?£$]/)))
   notCurrency++;
  if (!(itm.match(/^[\+-]?[\d\.,]+$/)))
   notNumerical++;
 }
    switch (0) {
     case notDate: sortfn = st_sortDate; break;
     case notCurrency: sortfn = st_sortCurrency; break;
     case notNumerical: sortfn = st_sortNumeric; break;
     default: sortfn = st_sortCaseInsensitive;
    }
 sci = column;
 var firstRow = new Array();
 var newRows = new Array();
 for (i=0; i<table.rows[0].length; i++) { firstRow[i] = table.rows[0][i]; }
 for (j=1; j<table.rows.length; j++) { newRows[j-1] = table.rows[j]; }
 newRows.sort(sortfn);
 if (span.getAttribute('sortdir') == 'down') {
  ARROW = '&nbsp;?';
  newRows.reverse();
  span.setAttribute('sortdir', 'up');
 } else {
  ARROW = '&nbsp;?';
  span.setAttribute('sortdir', 'down');
 }
 for (i=0; i<newRows.length; i++) { if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortbottom') == -1))) table.tBodies[0].appendChild(newRows[i]); }
 for (i=0; i<newRows.length; i++) { if (newRows[i].className && (newRows[i].className.indexOf('sortbottom') != -1)) table.tBodies[0].appendChild(newRows[i]); }
 var allspans = document.getElementsByTagName('span');
 for (var ci=0; ci<allspans.length; ci++) {
  if (allspans[ci].className == 'sortarrow') {
   if (st_getParent(allspans[ci], 'table') == st_getParent(lnk, 'table')) {
    allspans[ci].innerHTML = '';
   }
  }
 }
 span.innerHTML = ARROW;
 colorTableRows();
}

function st_getParent(el, pTagName) {
 if (el == null) return null;
 else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())
  return el;
 else
  return st_getParent(el.parentNode, pTagName);
}

function st_sortDate(a, b) {
 aa = st_getInnerText(a.cells[sci]);
 bb = st_getInnerText(b.cells[sci]);
 if (aa.length == 10) {
  dt1 = aa.substr(6, 4)+aa.substr(3, 2)+aa.substr(0, 2);
 } else {
  yr = aa.substr(6, 2);
  if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
  dt1 = yr+aa.substr(3, 2)+aa.substr(0, 2);
 }
 if (bb.length == 10) {
  dt2 = bb.substr(6, 4)+bb.substr(3, 2)+bb.substr(0, 2);
 } else {
  yr = bb.substr(6, 2);
  if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
  dt2 = yr+bb.substr(3, 2)+bb.substr(0, 2);
 }
 if (dt1==dt2) return 0;
 if (dt1<dt2) return -1;
 return 1;
}

function st_sortCurrency(a, b) { 
 aa = st_getInnerText(a.cells[sci]).replace(/[^0-9.]/g, '');
 bb = st_getInnerText(b.cells[sci]).replace(/[^0-9.]/g, '');
 return parseFloat(aa) - parseFloat(bb);
}

function st_sortNumeric(a, b) { 
 aa = parseFloat(st_getInnerText(a.cells[sci]));
 if (isNaN(aa)) aa = 0;
 bb = parseFloat(st_getInnerText(b.cells[sci])); 
 if (isNaN(bb)) bb = 0;
 return aa-bb;
}

function st_sortCaseInsensitive(a, b) {
 aa = st_getInnerText(a.cells[sci]).toLowerCase();
 bb = st_getInnerText(b.cells[sci]).toLowerCase();
 if (aa==bb) return 0;
 if (aa<bb) return -1;
 return 1;
}

function st_sortDefault(a, b) {
 aa = st_getInnerText(a.cells[sci]);
 bb = st_getInnerText(b.cells[sci]);
 if (aa==bb) return 0;
 if (aa<bb) return -1;
 return 1;
}

/* http://ktk.xs4all.nl/stuff/javascript/table-row-alternate/
**************************************************************************/

function colorTableRows() {
 if (document.getElementsByTagName) {
  var tables = document.getElementsByTagName('table');
  for (var i = 0; i < tables.length; i++) {
   var trs = tables[i].getElementsByTagName('tr');
   for (var j = 1; j < trs.length; j++) {
    trs[j].className = (j % 2 == 0 ? '' : 'alternate');
   }
  }
 }
}

/* Load the scripts
**************************************************************************/

var foobar = new domFunction(function() {
 sortTable();
 colorTableRows();
 });

Note: I’m using the domFunction helper script to load the other scripts; you can of course use Scott Andrew’s addEvent handler instead if you smartassly prefer so.

In order to make it more easy for you to implement this script on your site, I’ll share some basic table-styling CSS below. Feel free to Steal This Code™ and/or make it your own!

table {
 width: 100%;
 border: 1px solid #edf3fe;
 empty-cells: show;
 }

th {
 text-align: center;
 font-weight: bold;
 background: #3d80df;
 padding: 1em;
 color: #fff; /* for THs that won't contain links */
 }

/* A little bit of Paul Fitts */
th a {
 display: block;
 width: 100%;
 padding: 1em;
 text-decoration: none;
 color: #cef;
 margin: -1em 0 -1em -1em;
 background: #3d80df;
 }

th a:hover {
 background: #f3008a;
 }

/* If the browser supports CSS3,
we don't actually need colorTableRows() */
tr:nth-child(odd) {
 background: #edf3fe;
 }

/* But we'll use it anyway because we're so cool */
tr.alternate {
 background: #edf3fe;
 }

Conclusion

Given the right treatment, sortTable() and colorTableRows() go together so symbiotically.

Thanks

Props to Stuart and Krijn for the nifty scripts, and kudos to João for fixing the bug he discovered himself. What a guy.

Filed under JavaScript, CSS, XHTML · September 2nd, 2005

Comments (20)

Listed below are the responses for this entry.

  1. Krijn Hoetmer:
    This commenter’s Gravatar

    Hey, I’ve got a new referrer \o/ Clever usage of the modulo operator for alternating rows there BTW ;)

    Comment posted on September 2nd, 2005 @ 7:58 pm
  2. Jeroen Mulder:
    This commenter’s Gravatar

    Very, very nice! I’ve been quite impressed with the sortTable() function, but always felt something was missing. Looks like you filled that void :-)

    Comment posted on September 3rd, 2005 @ 12:55 pm
  3. João Craveiro:
    This commenter’s Gravatar

    Sorting by name isn’t working that well — could it be because of that numerical title, 1982?

    Here’s what I get when trying to sort by name (I’ll use the numbers to avoid cluttering your comments):

    On loading (TH: name)
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    1st click (TH: name ?)
    3, 10, 6, 1, 7, 9, 4, 2, 5, 8 (correct order)
    2nd click (TH: name ?)
    3, 8, 5, 2, 4, 9, 7, 1, 10, 6 (from 8 to 10, we have a correctly reverse-ordered set of titles, but 3 and 6 are out of place)
    3nd click (TH: name ?)
    5, 8, 2, 4, 9, 7, 1, 10, 6, 3 (weird stuff; it’s very similiar to the previous state)
    4th click (TH: name ?)
    8, 5, 2, 4, 9, 7, 1, 6, 10, 3 (correct [reverse] order — note that it’s the 1st click state backwards)
    5th click
    == 1st click
    6th click
    == 2nd click

    … … and so on … …

    Comment posted on September 3rd, 2005 @ 2:54 pm
  4. Mathias:
    This commenter’s Gravatar

    Sorting by name isn’t working that well — could it be because of that numerical title, 1982? […]

    Now that’s some funky shit! Looks like you’ve discovered a bug in sortTable(). I think 1982 is confusing the st_resortTable() function, especially the part in which it attempts to determine the column type (date/currency/numeric). Any ideas?

    I’ll inform Stuart. After all it’s his original code; perhaps he knows of a proper solution.

    Comment posted on September 3rd, 2005 @ 4:18 pm
  5. João Craveiro:
    This commenter’s Gravatar

    Well, my first thought, given your comment, was that the script was using only one item to determine the column type (date/currency/numeric). Looking at the code gave me the certainty of it.

    So, the thing goes like this: the script is using the wrong logic to determine the column type. A numerical column is not a column with, at least, one (which?) numerical value, but a column with no other type of values but numerical. Same goes for date and currency, being “text” the default case.

    I’ll see if I can come up with a correction and, if so, I could e-mail it to you (I see you don’t have your address published, so, if you also prefer e-mail as the mean, please send me one so I can send you the goods).

    Comment posted on September 3rd, 2005 @ 4:32 pm
  6. Mathias:
    This commenter’s Gravatar

    I noticed the same faulty behaviour on the play count column. The original script doesn’t treat a formatted number with grouped thousands (in this case 103,612,460) as a number.

    Thanks for the email, I can’t believe you actually solved this problem in, like, five minutes. Respect! I updated the online demo, as well as the script’s code displayed in this post. (The bad example page however is left untouched, as after all that should contain the original code.)

    Just for the record: my email address is in fact published; it can be found on the contact page.

    Comment posted on September 3rd, 2005 @ 5:22 pm
  7. Henrik Lied:
    This commenter’s Gravatar

    Very nice, Mathias, I love it!

    Comment posted on September 4th, 2005 @ 11:14 pm
  8. Stuart Langridge:
    This commenter’s Gravatar

    João Craveiro is entirely correct; the script should, in an ideal world, determine the type of a column by assessing all values in it, not just the top one. The major reason that it doesn’t is that it takes rather a long time, in large tables, to match a regexp against every value in the column. Certainly if you’re using shorter tables it would be a good fix to change to João’s solution.

    Comment posted on September 6th, 2005 @ 2:57 pm
  9. Mathias:
    This commenter’s Gravatar

    João Craveiro is entirely correct; the script should, in an ideal world, determine the type of a column by assessing all values in it, not just the top one. The major reason that it doesn’t is that it takes rather a long time, in large tables, to match a regexp against every value in the column. Certainly if you’re using shorter tables it would be a good fix to change to João’s solution.

    Perhaps it would be a good idea to not check one column value exclusively (as with the original sortTable()), not check all values (as with João’s fix), but for instance check three: the first value, the last value, and one somewhere in the middle of the column. If most of those three are dates, then the column type is most probably date; if most are currencies, then that column should probably be treated as such; else, one can suppose the column contains numeric data.

    Comment posted on September 10th, 2005 @ 10:42 am
  10. João Craveiro:
    This commenter’s Gravatar

    (…) check three: the first value, the last value, and one somewhere in the middle of the column. If most of those three are dates, then the column type is most probably date; if most are currencies, then that column should probably be treated as such; else, one can suppose the column contains numeric data.

    Yes, despite being, by far, the most accurate (but still not 100%) way of determining how a column should be sorted, checking all rows as a O(n) complexity: the bigger the table, the longer it takes. Checking 3 items, or even a fraction of the rows that’s proportioal to their amount (e.g., if the table had like 42 rows, one could check 4 or 5 rows instead of just 3) is a fair solution, but leaves the correct sorting of the column to luck.

    Oh, and when you say if most of those three are dates and if most are currencies, it should be all of the checked items.

    Comment posted on September 10th, 2005 @ 2:13 pm
  11. Mathias:
    This commenter’s Gravatar

    Oh, and when you say if most of those three are dates and if most are currencies, it should be all of the checked items.

    Wasn’t there a problem involving a song title being 1982, which of course looks like a number, but isn’t in the context of this very table?

    I should’ve probably said if most of those are look like they’re dates; I guess that’s more clear.

    Comment posted on September 11th, 2005 @ 9:46 am
  12. João Craveiro:
    This commenter’s Gravatar

    Wasn’t there a problem involving a song title being 1982, which of course looks like a number, but isn’t in the context of this very table?

    I should’ve probably said if most of those are look like they’re dates; I guess that’s more clear.

    Precisely. Assume you have a column like that one, but with its first, last and middle values (in the present ordering) solely numerical, while the rest were normal text titles. The column should be sorted as text, but will be as numbers, because most (indeed, all) of the considered values are numerical. In fact, that’s what occured before: 1982 happened to be the first value in some orderings, forcing the column to be sorted as numbers.

    Comment posted on September 11th, 2005 @ 12:39 pm
  13. Mathias:
    This commenter’s Gravatar

    Precisely. Assume you have a column like that one, but with its first, last and middle values (in the present ordering) solely numerical, while the rest were normal text titles. The column should be sorted as text, but will be as numbers, because most (indeed, all) of the considered values are numerical. In fact, that’s what occured before: 1982 happened to be the first value in some orderings, forcing the column to be sorted as numbers.

    Alright, so that was stupid. I took for granted special values like that would always be either on top or on the bottom of the list, shamelessly forgetting about the initial state of the loaded page. Doh!

    Comment posted on September 12th, 2005 @ 8:27 pm
  14. Jeff Minard:
    This commenter’s Gravatar

    I use a table sorting thing as well and have run into similiar issues.

    The issue of the comma delimited numbers was problematic for me as well. I tried string replacing commas, but that didn’t work too well. What I ended up doing was assigning a class tag to each TD with the unformatted number and sorting on that if it existed.

    As for the wrong sort order being deteremined, you could do much the same thing. Simple add a class="date" or class="string" etc to the TH.

    Also, a good improvement to add is class="nosort" for columns that can’t/shouldn’t be sorted.

    Comment posted on September 21st, 2005 @ 3:59 pm
  15. Mathias:
    This commenter’s Gravatar

    What I ended up doing was assigning a class tag to each TD with the unformatted number and sorting on that if it existed.

    Actually, CLASS is an attribute (there is no such thing as <class/>, right?). And that solution doesn’t sound very semantic to me…

    As for the wrong sort order being deteremined, you could do much the same thing. Simple add a class="date" or class="string" etc to the TH.

    That also is a lot of unnecessary/additional work/markup.

    Also, a good improvement to add is class="nosort" for columns that can’t/shouldn’t be sorted.

    In fact, that’s not very sem—okay. I can see it improves usability though.

    Comment posted on September 21st, 2005 @ 7:05 pm
  16. Anne van Kesteren:
    This commenter’s Gravatar

    What is exactly better than sortTableRows (); from Robbert Broersma?

    Comment posted on September 25th, 2005 @ 10:39 pm
  17. Jim:
    This commenter’s Gravatar

    The problem with this kind of approach is that every script that manipulates tables has to be rewritten to not break in this manner. This isn’t always possible, for example page authors can’t rewrite userjs scripts, and userjs script authors can’t write generic scripts that preserve styles such as this.

    The right way of doing it is to hook colo[u]rTableRows() up to the table’s DOMSubtreeModified event. That way, the document tree modification and the style update is decoupled, so you don’t have to code them to work together. Unfortunately, the only rendering engine that supports that event right now is KHTML. Grumble…

    Comment posted on October 16th, 2005 @ 8:50 pm
  18. Bernd:
    This commenter’s Gravatar

    I like it a lot!!!
    But then you always think something is missing:

    • coloring the column which is used for sorting (especially useful if you have to scroll)
    • a reset button

    I solved the first one with a new bit of CSS

    td.sortCol {
    	background: #f9efe6;
    	}

    …and a modification to colorTableRows():

    function colorTableRows() {
    	if (document.getElementsByTagName) {
    		var tables = document.getElementsByTagName(’table’);
    		for (var i = 0; i < tables.length; i++) {
    			var trs = tables[i].getElementsByTagName('tr');
    			for (var j = 1; j < trs.length; j++) {
    				trs[j].className = (j % 2 == 0 ? '' : 'alternate');
    				// new stuff following
    				var tds = trs[j].getElementsByTagName('td');
    				for (var k = 0; k < tds.length; k++) {
    					tds[k].className = (k==sci ? 'sortCol' : '');
    				}
    				// new stuff end
    			}
    		}
    	}
    }

    The second issue of the resetting I solved only for one table on a page (because that is all I needed). I added one global variable after:

    var sci;
    var originalRows = new Array();

    …and two new functions:

    function get_original(table) {
    	var firstRow = new Array();
    	for (i=0; i<table.rows[0].length; i++) {
    		firstRow[i] = table.rows[0][i];
    	}
    	for (j=1; j<table.rows.length; j++) {
    		originalRows[j-1] = table.rows[j];
    	}
    }
    
    function resetSort(table) {
    	for (i=0; i<originalRows.length; i++) {
    		table.tBodies[0].appendChild(originalRows[i]);
    	}
    	sci = 'NULL';
    	var allspans = document.getElementsByTagName('span');
    	for (var ci=0; ci<allspans.length; ci++) {
    		allspans[ci].innerHTML = '';
    	}
    	colorTableRows();
    }

    Then I call get_original() in sortTable():

    function sortTable() {
    	if (!document.getElementsByTagName) return;
    	tbls = document.getElementsByTagName('table');
    	for (ti=0; ti<tbls.length; ti++) {
    		thisTbl = tbls[ti];
    	st_makeSortable(thisTbl);
    	// new call
    	get_original(thisTbl);
    	}
    }

    On my HTML page I added the following button:

    <input onclick="resetSort(getElementById('log_table'));" type="button" value="Reset sorting">

    Pressing it will restore the original table sorting, get rid of the arrow and of the background coloring.

    Comment posted on October 27th, 2005 @ 4:25 pm
  19. Laurent Domisse:
    This commenter’s Gravatar

    What a wonderful piece of javascript….very helpful !
    I would like to use it for my own GPL package with the full credit if you are agree.
    Really Nice ! :)

    Comment posted on August 28th, 2006 @ 10:26 pm
  20. Carl Simons:
    This commenter’s Gravatar

    I tried replacing the original sort table v2 with this new one and it sorts too slow. The old one sorts my giant table as fast as I can click, this new one lags a bit even with colorTableRows() removed…

    Comment posted on April 15th, 2008 @ 5:15 pm

Trackbacks & Pingbacks (1)

Listed below are resources on the web that mention this article.

  1. If..Else Log: Highlighted, sortable tables:
    This commenter’s Gravatar

    Highlighted, sortable tables
    Mathias shows how to create a dynamically sortable, alternately highlighted table. […]

    Trackback made on September 3rd, 2005 @ 12:37 pm