5 Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
\r
9 Add <script src="sorttable.js"></script> to your HTML
\r
10 Add class="sortable" to any table you'd like to make sortable
\r
11 Click on the headers to sort
\r
13 Thanks to many, many people for contributions and suggestions.
\r
14 Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
\r
15 This basically means: do what you want with it.
\r
19 var stIsIE = /*@cc_on!@*/false;
\r
23 // quit if this function has already been called
\r
24 if (arguments.callee.done) return;
\r
25 // flag this function so we don't do the same thing twice
\r
26 arguments.callee.done = true;
\r
28 if (_timer) clearInterval(_timer);
\r
30 if (!document.createElement || !document.getElementsByTagName) return;
\r
32 sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
\r
34 forEach(document.getElementsByTagName('table'), function(table) {
\r
35 if (table.className.search(/\bsortable\b/) != -1) {
\r
36 sorttable.makeSortable(table);
\r
42 makeSortable: function(table) {
\r
43 if (table.getElementsByTagName('thead').length == 0) {
\r
44 // table doesn't have a tHead. Since it should have, create one and
\r
45 // put the first table row in it.
\r
46 the = document.createElement('thead');
\r
47 the.appendChild(table.rows[0]);
\r
48 table.insertBefore(the,table.firstChild);
\r
50 // Safari doesn't support table.tHead, sigh
\r
51 if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
\r
53 if (table.tHead.rows.length != 1) return; // can't cope with two header rows
\r
55 // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
\r
56 // "total" rows, for example). This is B&R, since what you're supposed
\r
57 // to do is put them in a tfoot. So, if there are sortbottom rows,
\r
58 // for backwards compatibility, move them to tfoot (creating it if needed).
\r
59 sortbottomrows = [];
\r
60 for (var i=0; i<table.rows.length; i++) {
\r
61 if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
\r
62 sortbottomrows[sortbottomrows.length] = table.rows[i];
\r
65 if (sortbottomrows) {
\r
66 if (table.tFoot == null) {
\r
67 // table doesn't have a tfoot. Create one.
\r
68 tfo = document.createElement('tfoot');
\r
69 table.appendChild(tfo);
\r
71 for (var i=0; i<sortbottomrows.length; i++) {
\r
72 tfo.appendChild(sortbottomrows[i]);
\r
74 delete sortbottomrows;
\r
77 // work through each column and calculate its type
\r
78 headrow = table.tHead.rows[0].cells;
\r
79 for (var i=0; i<headrow.length; i++) {
\r
80 // manually override the type with a sorttable_type attribute
\r
81 if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
\r
82 mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
\r
83 if (mtch) { override = mtch[1]; }
\r
84 if (mtch && typeof sorttable["sort_"+override] == 'function') {
\r
85 headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
\r
87 headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
\r
89 // make it clickable to sort
\r
90 headrow[i].sorttable_columnindex = i;
\r
91 headrow[i].sorttable_tbody = table.tBodies[0];
\r
92 dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
\r
94 if (this.className.search(/\bsorttable_sorted\b/) != -1) {
\r
95 // if we're already sorted by this column, just
\r
96 // reverse the table, which is quicker
\r
97 sorttable.reverse(this.sorttable_tbody);
\r
98 this.className = this.className.replace('sorttable_sorted',
\r
99 'sorttable_sorted_reverse');
\r
100 this.removeChild(document.getElementById('sorttable_sortfwdind'));
\r
101 sortrevind = document.createElement('span');
\r
102 sortrevind.id = "sorttable_sortrevind";
\r
103 sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴';
\r
104 this.appendChild(sortrevind);
\r
107 if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
\r
108 // if we're already sorted by this column in reverse, just
\r
109 // re-reverse the table, which is quicker
\r
110 sorttable.reverse(this.sorttable_tbody);
\r
111 this.className = this.className.replace('sorttable_sorted_reverse',
\r
112 'sorttable_sorted');
\r
113 this.removeChild(document.getElementById('sorttable_sortrevind'));
\r
114 sortfwdind = document.createElement('span');
\r
115 sortfwdind.id = "sorttable_sortfwdind";
\r
116 sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
\r
117 this.appendChild(sortfwdind);
\r
121 // remove sorttable_sorted classes
\r
122 theadrow = this.parentNode;
\r
123 forEach(theadrow.childNodes, function(cell) {
\r
124 if (cell.nodeType == 1) { // an element
\r
125 cell.className = cell.className.replace('sorttable_sorted_reverse','');
\r
126 cell.className = cell.className.replace('sorttable_sorted','');
\r
129 sortfwdind = document.getElementById('sorttable_sortfwdind');
\r
130 if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
\r
131 sortrevind = document.getElementById('sorttable_sortrevind');
\r
132 if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
\r
134 this.className += ' sorttable_sorted';
\r
135 sortfwdind = document.createElement('span');
\r
136 sortfwdind.id = "sorttable_sortfwdind";
\r
137 sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
\r
138 this.appendChild(sortfwdind);
\r
140 // build an array to sort. This is a Schwartzian transform thing,
\r
141 // i.e., we "decorate" each row with the actual sort key,
\r
142 // sort based on the sort keys, and then put the rows back in order
\r
143 // which is a lot faster because you only do getInnerText once per row
\r
145 col = this.sorttable_columnindex;
\r
146 rows = this.sorttable_tbody.rows;
\r
147 for (var j=0; j<rows.length; j++) {
\r
148 row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
\r
150 /* If you want a stable sort, uncomment the following line */
\r
151 //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
\r
152 /* and comment out this one */
\r
153 row_array.sort(this.sorttable_sortfunction);
\r
155 tb = this.sorttable_tbody;
\r
156 for (var j=0; j<row_array.length; j++) {
\r
157 tb.appendChild(row_array[j][1]);
\r
166 guessType: function(table, column) {
\r
167 // guess the type of a column based on its first non-blank row
\r
168 sortfn = sorttable.sort_alpha;
\r
169 for (var i=0; i<table.tBodies[0].rows.length; i++) {
\r
170 text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
\r
172 if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
\r
173 return sorttable.sort_numeric;
\r
175 // check for a date: dd/mm/yyyy or dd/mm/yy
\r
176 // can have / or . or - as separator
\r
177 // can be mm/dd as well
\r
178 possdate = text.match(sorttable.DATE_RE)
\r
180 // looks like a date
\r
181 first = parseInt(possdate[1]);
\r
182 second = parseInt(possdate[2]);
\r
184 // definitely dd/mm
\r
185 return sorttable.sort_ddmm;
\r
186 } else if (second > 12) {
\r
187 return sorttable.sort_mmdd;
\r
189 // looks like a date, but we can't tell which, so assume
\r
190 // that it's dd/mm (English imperialism!) and keep looking
\r
191 sortfn = sorttable.sort_ddmm;
\r
199 getInnerText: function(node) {
\r
200 // gets the text we want to use for sorting for a cell.
\r
201 // strips leading and trailing whitespace.
\r
202 // this is *not* a generic getInnerText function; it's special to sorttable.
\r
203 // for example, you can override the cell text with a customkey attribute.
\r
204 // it also gets .value for <input> fields.
\r
206 if (!node) return "";
\r
208 hasInputs = (typeof node.getElementsByTagName == 'function') &&
\r
209 node.getElementsByTagName('input').length;
\r
211 if (node.getAttribute("sorttable_customkey") != null) {
\r
212 return node.getAttribute("sorttable_customkey");
\r
214 else if (typeof node.textContent != 'undefined' && !hasInputs) {
\r
215 return node.textContent.replace(/^\s+|\s+$/g, '');
\r
217 else if (typeof node.innerText != 'undefined' && !hasInputs) {
\r
218 return node.innerText.replace(/^\s+|\s+$/g, '');
\r
220 else if (typeof node.text != 'undefined' && !hasInputs) {
\r
221 return node.text.replace(/^\s+|\s+$/g, '');
\r
224 switch (node.nodeType) {
\r
226 if (node.nodeName.toLowerCase() == 'input') {
\r
227 return node.value.replace(/^\s+|\s+$/g, '');
\r
230 return node.nodeValue.replace(/^\s+|\s+$/g, '');
\r
234 var innerText = '';
\r
235 for (var i = 0; i < node.childNodes.length; i++) {
\r
236 innerText += sorttable.getInnerText(node.childNodes[i]);
\r
238 return innerText.replace(/^\s+|\s+$/g, '');
\r
246 reverse: function(tbody) {
\r
247 // reverse the rows in a tbody
\r
249 for (var i=0; i<tbody.rows.length; i++) {
\r
250 newrows[newrows.length] = tbody.rows[i];
\r
252 for (var i=newrows.length-1; i>=0; i--) {
\r
253 tbody.appendChild(newrows[i]);
\r
259 each sort function takes two parameters, a and b
\r
260 you are comparing a[0] and b[0] */
\r
261 sort_numeric: function(a,b) {
\r
262 aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
\r
263 if (isNaN(aa)) aa = 0;
\r
264 bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
\r
265 if (isNaN(bb)) bb = 0;
\r
268 sort_alpha: function(a,b) {
\r
269 if (a[0]==b[0]) return 0;
\r
270 if (a[0]<b[0]) return -1;
\r
273 sort_ddmm: function(a,b) {
\r
274 mtch = a[0].match(sorttable.DATE_RE);
\r
275 y = mtch[3]; m = mtch[2]; d = mtch[1];
\r
276 if (m.length == 1) m = '0'+m;
\r
277 if (d.length == 1) d = '0'+d;
\r
279 mtch = b[0].match(sorttable.DATE_RE);
\r
280 y = mtch[3]; m = mtch[2]; d = mtch[1];
\r
281 if (m.length == 1) m = '0'+m;
\r
282 if (d.length == 1) d = '0'+d;
\r
284 if (dt1==dt2) return 0;
\r
285 if (dt1<dt2) return -1;
\r
288 sort_mmdd: function(a,b) {
\r
289 mtch = a[0].match(sorttable.DATE_RE);
\r
290 y = mtch[3]; d = mtch[2]; m = mtch[1];
\r
291 if (m.length == 1) m = '0'+m;
\r
292 if (d.length == 1) d = '0'+d;
\r
294 mtch = b[0].match(sorttable.DATE_RE);
\r
295 y = mtch[3]; d = mtch[2]; m = mtch[1];
\r
296 if (m.length == 1) m = '0'+m;
\r
297 if (d.length == 1) d = '0'+d;
\r
299 if (dt1==dt2) return 0;
\r
300 if (dt1<dt2) return -1;
\r
304 shaker_sort: function(list, comp_func) {
\r
305 // A stable sort function to allow multi-level sorting of data
\r
306 // see: http://en.wikipedia.org/wiki/Cocktail_sort
\r
307 // thanks to Joseph Nahmias
\r
309 var t = list.length - 1;
\r
314 for(var i = b; i < t; ++i) {
\r
315 if ( comp_func(list[i], list[i+1]) > 0 ) {
\r
316 var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
\r
324 for(var i = t; i > b; --i) {
\r
325 if ( comp_func(list[i], list[i-1]) < 0 ) {
\r
326 var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
\r
336 /* ******************************************************************
\r
337 Supporting functions: bundled here to avoid depending on a library
\r
338 ****************************************************************** */
\r
340 // Dean Edwards/Matthias Miller/John Resig
\r
342 /* for Mozilla/Opera9 */
\r
343 if (document.addEventListener) {
\r
344 document.addEventListener("DOMContentLoaded", sorttable.init, false);
\r
347 /* for Internet Explorer */
\r
350 document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
\r
351 var script = document.getElementById("__ie_onload");
\r
352 script.onreadystatechange = function() {
\r
353 if (this.readyState == "complete") {
\r
354 sorttable.init(); // call the onload handler
\r
360 if (/WebKit/i.test(navigator.userAgent)) { // sniff
\r
361 var _timer = setInterval(function() {
\r
362 if (/loaded|complete/.test(document.readyState)) {
\r
363 sorttable.init(); // call the onload handler
\r
368 /* for other browsers */
\r
369 window.onload = sorttable.init;
\r
371 // written by Dean Edwards, 2005
\r
372 // with input from Tino Zijdel, Matthias Miller, Diego Perini
\r
374 // http://dean.edwards.name/weblog/2005/10/add-event/
\r
376 function dean_addEvent(element, type, handler) {
\r
377 if (element.addEventListener) {
\r
378 element.addEventListener(type, handler, false);
\r
380 // assign each event handler a unique ID
\r
381 if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
\r
382 // create a hash table of event types for the element
\r
383 if (!element.events) element.events = {};
\r
384 // create a hash table of event handlers for each element/event pair
\r
385 var handlers = element.events[type];
\r
387 handlers = element.events[type] = {};
\r
388 // store the existing event handler (if there is one)
\r
389 if (element["on" + type]) {
\r
390 handlers[0] = element["on" + type];
\r
393 // store the event handler in the hash table
\r
394 handlers[handler.$$guid] = handler;
\r
395 // assign a global event handler to do all the work
\r
396 element["on" + type] = handleEvent;
\r
399 // a counter used to create unique IDs
\r
400 dean_addEvent.guid = 1;
\r
402 function removeEvent(element, type, handler) {
\r
403 if (element.removeEventListener) {
\r
404 element.removeEventListener(type, handler, false);
\r
406 // delete the event handler from the hash table
\r
407 if (element.events && element.events[type]) {
\r
408 delete element.events[type][handler.$$guid];
\r
413 function handleEvent(event) {
\r
414 var returnValue = true;
\r
415 // grab the event object (IE uses a global event object)
\r
416 event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
\r
417 // get a reference to the hash table of event handlers
\r
418 var handlers = this.events[event.type];
\r
419 // execute each event handler
\r
420 for (var i in handlers) {
\r
421 this.$$handleEvent = handlers[i];
\r
422 if (this.$$handleEvent(event) === false) {
\r
423 returnValue = false;
\r
426 return returnValue;
\r
429 function fixEvent(event) {
\r
430 // add W3C standard event methods
\r
431 event.preventDefault = fixEvent.preventDefault;
\r
432 event.stopPropagation = fixEvent.stopPropagation;
\r
435 fixEvent.preventDefault = function() {
\r
436 this.returnValue = false;
\r
438 fixEvent.stopPropagation = function() {
\r
439 this.cancelBubble = true;
\r
442 // Dean's forEach: http://dean.edwards.name/base/forEach.js
\r
444 forEach, version 1.0
\r
445 Copyright 2006, Dean Edwards
\r
446 License: http://www.opensource.org/licenses/mit-license.php
\r
449 // array-like enumeration
\r
450 if (!Array.forEach) { // mozilla already supports this
\r
451 Array.forEach = function(array, block, context) {
\r
452 for (var i = 0; i < array.length; i++) {
\r
453 block.call(context, array[i], i, array);
\r
458 // generic enumeration
\r
459 Function.prototype.forEach = function(object, block, context) {
\r
460 for (var key in object) {
\r
461 if (typeof this.prototype[key] == "undefined") {
\r
462 block.call(context, object[key], key, object);
\r
467 // character enumeration
\r
468 String.forEach = function(string, block, context) {
\r
469 Array.forEach(string.split(""), function(chr, index) {
\r
470 block.call(context, chr, index, string);
\r
474 // globally resolve forEach enumeration
\r
475 var forEach = function(object, block, context) {
\r
477 var resolve = Object; // default
\r
478 if (object instanceof Function) {
\r
479 // functions have a "length" property
\r
480 resolve = Function;
\r
481 } else if (object.forEach instanceof Function) {
\r
482 // the object implements a custom forEach method so use that
\r
483 object.forEach(block, context);
\r
485 } else if (typeof object == "string") {
\r
486 // the object is a string
\r
488 } else if (typeof object.length == "number") {
\r
489 // the object is array-like
\r
492 resolve.forEach(object, block, context);
\r