5 Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
9 Add <script src="sorttable.js"></script> to your HTML
10 Add class="sortable" to any table you'd like to make sortable
11 Click on the headers to sort
13 Thanks to many, many people for contributions and suggestions.
14 Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
15 This basically means: do what you want with it.
18 var stIsIE
= /*@cc_on!@*/ false;
22 // quit if this function has already been called
23 if (arguments
.callee
.done
)
25 // flag this function so we don't do the same thing twice
26 arguments
.callee
.done
= true;
29 clearInterval(_timer
);
31 if (!document
.createElement
|| !document
.getElementsByTagName
)
34 sorttable
.DATE_RE
= /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
36 forEach(document
.getElementsByTagName('table'), function(table
) {
37 if (table
.className
.search(/\bsortable\b/) != -1) {
38 sorttable
.makeSortable(table
);
43 makeSortable : function(table
) {
44 if (table
.getElementsByTagName('thead').length
== 0) {
45 // table doesn't have a tHead. Since it should have, create one and
46 // put the first table row in it.
47 the
= document
.createElement('thead');
48 the
.appendChild(table
.rows
[0]);
49 table
.insertBefore(the
, table
.firstChild
);
51 // Safari doesn't support table.tHead, sigh
52 if (table
.tHead
== null)
53 table
.tHead
= table
.getElementsByTagName('thead')[0];
55 if (table
.tHead
.rows
.length
!= 1)
56 return; // can't cope with two header rows
58 // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
59 // "total" rows, for example). This is B&R, since what you're supposed
60 // to do is put them in a tfoot. So, if there are sortbottom rows,
61 // for backward compatibility, move them to tfoot (creating it if needed).
63 for (var i
= 0; i
< table
.rows
.length
; i
++) {
64 if (table
.rows
[i
].className
.search(/\bsortbottom\b/) != -1) {
65 sortbottomrows
[sortbottomrows
.length
] = table
.rows
[i
];
69 if (table
.tFoot
== null) {
70 // table doesn't have a tfoot. Create one.
71 tfo
= document
.createElement('tfoot');
72 table
.appendChild(tfo
);
74 for (var i
= 0; i
< sortbottomrows
.length
; i
++) {
75 tfo
.appendChild(sortbottomrows
[i
]);
77 delete sortbottomrows
;
80 // work through each column and calculate its type
81 headrow
= table
.tHead
.rows
[0].cells
;
82 for (var i
= 0; i
< headrow
.length
; i
++) {
83 // manually override the type with a sorttable_type attribute
84 if (!headrow
[i
].className
.match(
85 /\bsorttable_nosort\b/)) { // skip this col
86 mtch
= headrow
[i
].className
.match(/\bsorttable_([a-z0-9]+)\b/);
90 if (mtch
&& typeof sorttable
["sort_" + override
] == 'function') {
91 headrow
[i
].sorttable_sortfunction
= sorttable
["sort_" + override
];
93 headrow
[i
].sorttable_sortfunction
= sorttable
.guessType(table
, i
);
95 // make it clickable to sort
96 headrow
[i
].sorttable_columnindex
= i
;
97 headrow
[i
].sorttable_tbody
= table
.tBodies
[0];
98 dean_addEvent(headrow
[i
], "click", function(e
) {
99 if (this.className
.search(/\bsorttable_sorted\b/) != -1) {
100 // if we're already sorted by this column, just
101 // reverse the table, which is quicker
102 sorttable
.reverse(this.sorttable_tbody
);
103 this.className
= this.className
.replace('sorttable_sorted',
104 'sorttable_sorted_reverse');
105 this.removeChild(document
.getElementById('sorttable_sortfwdind'));
106 sortrevind
= document
.createElement('span');
107 sortrevind
.id
= "sorttable_sortrevind";
108 sortrevind
.innerHTML
= stIsIE
109 ? ' <font face="webdings">5</font>'
111 this.appendChild(sortrevind
);
114 if (this.className
.search(/\bsorttable_sorted_reverse\b/) != -1) {
115 // if we're already sorted by this column in reverse, just
116 // re-reverse the table, which is quicker
117 sorttable
.reverse(this.sorttable_tbody
);
118 this.className
= this.className
.replace('sorttable_sorted_reverse',
120 this.removeChild(document
.getElementById('sorttable_sortrevind'));
121 sortfwdind
= document
.createElement('span');
122 sortfwdind
.id
= "sorttable_sortfwdind";
123 sortfwdind
.innerHTML
= stIsIE
124 ? ' <font face="webdings">6</font>'
126 this.appendChild(sortfwdind
);
130 // remove sorttable_sorted classes
131 theadrow
= this.parentNode
;
132 forEach(theadrow
.childNodes
, function(cell
) {
133 if (cell
.nodeType
== 1) { // an element
135 cell
.className
.replace('sorttable_sorted_reverse', '');
136 cell
.className
= cell
.className
.replace('sorttable_sorted', '');
139 sortfwdind
= document
.getElementById('sorttable_sortfwdind');
141 sortfwdind
.parentNode
.removeChild(sortfwdind
);
143 sortrevind
= document
.getElementById('sorttable_sortrevind');
145 sortrevind
.parentNode
.removeChild(sortrevind
);
148 this.className
+= ' sorttable_sorted';
149 sortfwdind
= document
.createElement('span');
150 sortfwdind
.id
= "sorttable_sortfwdind";
151 sortfwdind
.innerHTML
=
152 stIsIE
? ' <font face="webdings">6</font>' : ' ▾';
153 this.appendChild(sortfwdind
);
155 // build an array to sort. This is a Schwartzian transform thing,
156 // i.e., we "decorate" each row with the actual sort key,
157 // sort based on the sort keys, and then put the rows back in order
158 // which is a lot faster because you only do getInnerText once per row
160 col
= this.sorttable_columnindex
;
161 rows
= this.sorttable_tbody
.rows
;
162 for (var j
= 0; j
< rows
.length
; j
++) {
163 row_array
[row_array
.length
] =
164 [ sorttable
.getInnerText(rows
[j
].cells
[col
]), rows
[j
] ];
166 /* If you want a stable sort, uncomment the following line */
167 sorttable
.shaker_sort(row_array
, this.sorttable_sortfunction
);
168 /* and comment out this one */
169 // row_array.sort(this.sorttable_sortfunction);
171 tb
= this.sorttable_tbody
;
172 for (var j
= 0; j
< row_array
.length
; j
++) {
173 tb
.appendChild(row_array
[j
][1]);
182 guessType : function(table
, column
) {
183 // guess the type of a column based on its first non-blank row
184 sortfn
= sorttable
.sort_alpha
;
185 for (var i
= 0; i
< table
.tBodies
[0].rows
.length
; i
++) {
186 text
= sorttable
.getInnerText(table
.tBodies
[0].rows
[i
].cells
[column
]);
188 if (text
.match(/^-?[」$、]?[\d,.]+%?$/)) {
189 return sorttable
.sort_numeric
;
191 // check for a date: dd/mm/yyyy or dd/mm/yy
192 // can have / or . or - as separator
193 // can be mm/dd as well
194 possdate
= text
.match(sorttable
.DATE_RE
)
197 first
= parseInt(possdate
[1]);
198 second
= parseInt(possdate
[2]);
201 return sorttable
.sort_ddmm
;
202 } else if (second
> 12) {
203 return sorttable
.sort_mmdd
;
205 // looks like a date, but we can't tell which, so assume
206 // that it's dd/mm (English imperialism!) and keep looking
207 sortfn
= sorttable
.sort_ddmm
;
215 getInnerText : function(node
) {
216 // gets the text we want to use for sorting for a cell.
217 // strips leading and trailing whitespace.
218 // this is *not* a generic getInnerText function; it's special to sorttable.
219 // for example, you can override the cell text with a customkey attribute.
220 // it also gets .value for <input> fields.
222 hasInputs
= (typeof node
.getElementsByTagName
== 'function') &&
223 node
.getElementsByTagName('input').length
;
225 if (node
.getAttribute("sorttable_customkey") != null) {
226 return node
.getAttribute("sorttable_customkey");
227 } else if (typeof node
.textContent
!= 'undefined' && !hasInputs
) {
228 return node
.textContent
.replace(/^\s+|\s+$/g, '');
229 } else if (typeof node
.innerText
!= 'undefined' && !hasInputs
) {
230 return node
.innerText
.replace(/^\s+|\s+$/g, '');
231 } else if (typeof node
.text
!= 'undefined' && !hasInputs
) {
232 return node
.text
.replace(/^\s+|\s+$/g, '');
234 switch (node
.nodeType
) {
236 if (node
.nodeName
.toLowerCase() == 'input') {
237 return node
.value
.replace(/^\s+|\s+$/g, '');
240 return node
.nodeValue
.replace(/^\s+|\s+$/g, '');
245 for (var i
= 0; i
< node
.childNodes
.length
; i
++) {
246 innerText
+= sorttable
.getInnerText(node
.childNodes
[i
]);
248 return innerText
.replace(/^\s+|\s+$/g, '');
256 reverse : function(tbody
) {
257 // reverse the rows in a tbody
259 for (var i
= 0; i
< tbody
.rows
.length
; i
++) {
260 newrows
[newrows
.length
] = tbody
.rows
[i
];
262 for (var i
= newrows
.length
- 1; i
>= 0; i
--) {
263 tbody
.appendChild(newrows
[i
]);
269 each sort function takes two parameters, a and b
270 you are comparing a[0] and b[0] */
271 sort_numeric : function(a
, b
) {
272 aa
= parseFloat(a
[0].replace(/[^0-9.-]/g, ''));
275 bb
= parseFloat(b
[0].replace(/[^0-9.-]/g, ''));
280 sort_alpha : function(a
, b
) {
287 sort_ddmm : function(a
, b
) {
288 mtch
= a
[0].match(sorttable
.DATE_RE
);
297 mtch
= b
[0].match(sorttable
.DATE_RE
);
312 sort_mmdd : function(a
, b
) {
313 mtch
= a
[0].match(sorttable
.DATE_RE
);
322 mtch
= b
[0].match(sorttable
.DATE_RE
);
338 shaker_sort : function(list
, comp_func
) {
339 // A stable sort function to allow multi-level sorting of data
340 // see: http://en.wikipedia.org/wiki/Cocktail_sort
341 // thanks to Joseph Nahmias
343 var t
= list
.length
- 1;
348 for (var i
= b
; i
< t
; ++i
) {
349 if (comp_func(list
[i
], list
[i
+ 1]) > 0) {
351 list
[i
] = list
[i
+ 1];
361 for (var i
= t
; i
> b
; --i
) {
362 if (comp_func(list
[i
], list
[i
- 1]) < 0) {
364 list
[i
] = list
[i
- 1];
375 /* ******************************************************************
376 Supporting functions: bundled here to avoid depending on a library
377 ****************************************************************** */
379 // Dean Edwards/Matthias Miller/John Resig
381 /* for Mozilla/Opera9 */
382 if (document.addEventListener) {
383 document.addEventListener("DOMContentLoaded", sorttable.init, false);
386 /* for Internet Explorer */
389 document.write("<script id=__ie_onload defer
390 src=javascript:void(0)><\/script>"); var script =
391 document.getElementById("__ie_onload"); script.onreadystatechange = function() {
392 if (this.readyState == "complete") {
393 sorttable.init(); // call the onload handler
399 if (/WebKit/i.test(navigator
.userAgent
)) { // sniff
400 var _timer
= setInterval(function() {
401 if (/loaded|complete/.test(document
.readyState
)) {
402 sorttable
.init(); // call the onload handler
407 /* for other browsers */
408 window
.onload
= sorttable
.init
;
410 // written by Dean Edwards, 2005
411 // with input from Tino Zijdel, Matthias Miller, Diego Perini
413 // http://dean.edwards.name/weblog/2005/10/add-event/
415 function dean_addEvent(element
, type
, handler
) {
416 if (element
.addEventListener
) {
417 element
.addEventListener(type
, handler
, false);
419 // assign each event handler a unique ID
421 handler
.$$guid
= dean_addEvent
.guid
++;
422 // create a hash table of event types for the element
425 // create a hash table of event handlers for each element/event pair
426 var handlers
= element
.events
[type
];
428 handlers
= element
.events
[type
] = {};
429 // store the existing event handler (if there is one)
430 if (element
["on" + type
]) {
431 handlers
[0] = element
["on" + type
];
434 // store the event handler in the hash table
435 handlers
[handler
.$$guid
] = handler
;
436 // assign a global event handler to do all the work
437 element
["on" + type
] = handleEvent
;
440 // a counter used to create unique IDs
441 dean_addEvent
.guid
= 1;
443 function removeEvent(element
, type
, handler
) {
444 if (element
.removeEventListener
) {
445 element
.removeEventListener(type
, handler
, false);
447 // delete the event handler from the hash table
448 if (element
.events
&& element
.events
[type
]) {
449 delete element
.events
[type
][handler
.$$guid
];
454 function handleEvent(event
) {
455 var returnValue
= true;
456 // grab the event object (IE uses a global event object)
460 ((this.ownerDocument
|| this.document
|| this).parentWindow
|| window
)
462 // get a reference to the hash table of event handlers
463 var handlers
= this.events
[event
.type
];
464 // execute each event handler
465 for (var i
in handlers
) {
466 this.$$handleEvent
= handlers
[i
];
467 if (this.$$handleEvent(event
) === false) {
474 function fixEvent(event
) {
475 // add W3C standard event methods
476 event
.preventDefault
= fixEvent
.preventDefault
;
477 event
.stopPropagation
= fixEvent
.stopPropagation
;
480 fixEvent
.preventDefault = function() { this.returnValue
= false; };
481 fixEvent
.stopPropagation = function() { this.cancelBubble
= true; }
483 // Dean's forEach: http://dean.edwards.name/base/forEach.js
486 Copyright 2006, Dean Edwards
487 License: http://www.opensource.org/licenses/mit-license.php
490 // array-like enumeration
491 if (!Array
.forEach
) { // mozilla already supports this
492 Array
.forEach = function(array
, block
, context
) {
493 for (var i
= 0; i
< array
.length
; i
++) {
494 block
.call(context
, array
[i
], i
, array
);
499 // generic enumeration
500 Function
.prototype.forEach = function(object
, block
, context
) {
501 for (var key
in object
) {
502 if (typeof this.prototype[key
] == "undefined") {
503 block
.call(context
, object
[key
], key
, object
);
508 // character enumeration
509 String
.forEach = function(string
, block
, context
) {
512 function(chr
, index
) { block
.call(context
, chr
, index
, string
); });
515 // globally resolve forEach enumeration
516 var forEach = function(object
, block
, context
) {
518 var resolve
= Object
; // default
519 if (object
instanceof Function
) {
520 // functions have a "length" property
522 } else if (object
.forEach
instanceof Function
) {
523 // the object implements a custom forEach method so use that
524 object
.forEach(block
, context
);
526 } else if (typeof object
== "string") {
527 // the object is a string
529 } else if (typeof object
.length
== "number") {
530 // the object is array-like
533 resolve
.forEach(object
, block
, context
);