]>
Commit | Line | Data |
---|---|---|
886c5ccd FM |
1 | /* |
2 | SortTable | |
3 | version 2 | |
4 | 7th April 2007 | |
5 | Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ | |
6 | ||
7 | Instructions: | |
8 | Download this file | |
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 | |
12 | ||
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. | |
16 | */ | |
17 | ||
18 | ||
19 | var stIsIE = /*@cc_on!@*/false; | |
20 | ||
21 | sorttable = { | |
22 | init: function() { | |
23 | // quit if this function has already been called | |
24 | if (arguments.callee.done) return; | |
25 | // flag this function so we don't do the same thing twice | |
26 | arguments.callee.done = true; | |
27 | // kill the timer | |
28 | if (_timer) clearInterval(_timer); | |
29 | ||
30 | if (!document.createElement || !document.getElementsByTagName) return; | |
31 | ||
32 | sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; | |
33 | ||
34 | forEach(document.getElementsByTagName('table'), function(table) { | |
35 | if (table.className.search(/\bsortable\b/) != -1) { | |
36 | sorttable.makeSortable(table); | |
37 | } | |
38 | }); | |
39 | ||
40 | }, | |
41 | ||
42 | makeSortable: function(table) { | |
43 | if (table.getElementsByTagName('thead').length == 0) { | |
44 | // table doesn't have a tHead. Since it should have, create one and | |
45 | // put the first table row in it. | |
46 | the = document.createElement('thead'); | |
47 | the.appendChild(table.rows[0]); | |
48 | table.insertBefore(the,table.firstChild); | |
49 | } | |
50 | // Safari doesn't support table.tHead, sigh | |
51 | if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; | |
52 | ||
53 | if (table.tHead.rows.length != 1) return; // can't cope with two header rows | |
54 | ||
55 | // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as | |
56 | // "total" rows, for example). This is B&R, since what you're supposed | |
57 | // to do is put them in a tfoot. So, if there are sortbottom rows, | |
58 | // for backwards compatibility, move them to tfoot (creating it if needed). | |
59 | sortbottomrows = []; | |
60 | for (var i=0; i<table.rows.length; i++) { | |
61 | if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { | |
62 | sortbottomrows[sortbottomrows.length] = table.rows[i]; | |
63 | } | |
64 | } | |
65 | if (sortbottomrows) { | |
66 | if (table.tFoot == null) { | |
67 | // table doesn't have a tfoot. Create one. | |
68 | tfo = document.createElement('tfoot'); | |
69 | table.appendChild(tfo); | |
70 | } | |
71 | for (var i=0; i<sortbottomrows.length; i++) { | |
72 | tfo.appendChild(sortbottomrows[i]); | |
73 | } | |
74 | delete sortbottomrows; | |
75 | } | |
76 | ||
77 | // work through each column and calculate its type | |
78 | headrow = table.tHead.rows[0].cells; | |
79 | for (var i=0; i<headrow.length; i++) { | |
80 | // manually override the type with a sorttable_type attribute | |
81 | if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col | |
82 | mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); | |
83 | if (mtch) { override = mtch[1]; } | |
84 | if (mtch && typeof sorttable["sort_"+override] == 'function') { | |
85 | headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; | |
86 | } else { | |
87 | headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); | |
88 | } | |
89 | // make it clickable to sort | |
90 | headrow[i].sorttable_columnindex = i; | |
91 | headrow[i].sorttable_tbody = table.tBodies[0]; | |
92 | dean_addEvent(headrow[i],"click", function(e) { | |
93 | ||
94 | if (this.className.search(/\bsorttable_sorted\b/) != -1) { | |
95 | // if we're already sorted by this column, just | |
96 | // reverse the table, which is quicker | |
97 | sorttable.reverse(this.sorttable_tbody); | |
98 | this.className = this.className.replace('sorttable_sorted', | |
99 | 'sorttable_sorted_reverse'); | |
100 | this.removeChild(document.getElementById('sorttable_sortfwdind')); | |
101 | sortrevind = document.createElement('span'); | |
102 | sortrevind.id = "sorttable_sortrevind"; | |
103 | sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴'; | |
104 | this.appendChild(sortrevind); | |
105 | return; | |
106 | } | |
107 | if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { | |
108 | // if we're already sorted by this column in reverse, just | |
109 | // re-reverse the table, which is quicker | |
110 | sorttable.reverse(this.sorttable_tbody); | |
111 | this.className = this.className.replace('sorttable_sorted_reverse', | |
112 | 'sorttable_sorted'); | |
113 | this.removeChild(document.getElementById('sorttable_sortrevind')); | |
114 | sortfwdind = document.createElement('span'); | |
115 | sortfwdind.id = "sorttable_sortfwdind"; | |
116 | sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; | |
117 | this.appendChild(sortfwdind); | |
118 | return; | |
119 | } | |
120 | ||
121 | // remove sorttable_sorted classes | |
122 | theadrow = this.parentNode; | |
123 | forEach(theadrow.childNodes, function(cell) { | |
124 | if (cell.nodeType == 1) { // an element | |
125 | cell.className = cell.className.replace('sorttable_sorted_reverse',''); | |
126 | cell.className = cell.className.replace('sorttable_sorted',''); | |
127 | } | |
128 | }); | |
129 | sortfwdind = document.getElementById('sorttable_sortfwdind'); | |
130 | if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } | |
131 | sortrevind = document.getElementById('sorttable_sortrevind'); | |
132 | if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } | |
133 | ||
134 | this.className += ' sorttable_sorted'; | |
135 | sortfwdind = document.createElement('span'); | |
136 | sortfwdind.id = "sorttable_sortfwdind"; | |
137 | sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; | |
138 | this.appendChild(sortfwdind); | |
139 | ||
140 | // build an array to sort. This is a Schwartzian transform thing, | |
141 | // i.e., we "decorate" each row with the actual sort key, | |
142 | // sort based on the sort keys, and then put the rows back in order | |
143 | // which is a lot faster because you only do getInnerText once per row | |
144 | row_array = []; | |
145 | col = this.sorttable_columnindex; | |
146 | rows = this.sorttable_tbody.rows; | |
147 | for (var j=0; j<rows.length; j++) { | |
148 | row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]]; | |
149 | } | |
150 | /* If you want a stable sort, uncomment the following line */ | |
151 | //sorttable.shaker_sort(row_array, this.sorttable_sortfunction); | |
152 | /* and comment out this one */ | |
153 | row_array.sort(this.sorttable_sortfunction); | |
154 | ||
155 | tb = this.sorttable_tbody; | |
156 | for (var j=0; j<row_array.length; j++) { | |
157 | tb.appendChild(row_array[j][1]); | |
158 | } | |
159 | ||
160 | delete row_array; | |
161 | }); | |
162 | } | |
163 | } | |
164 | }, | |
165 | ||
166 | guessType: function(table, column) { | |
167 | // guess the type of a column based on its first non-blank row | |
168 | sortfn = sorttable.sort_alpha; | |
169 | for (var i=0; i<table.tBodies[0].rows.length; i++) { | |
170 | text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); | |
171 | if (text != '') { | |
665264e0 | 172 | if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { |
886c5ccd FM |
173 | return sorttable.sort_numeric; |
174 | } | |
175 | // check for a date: dd/mm/yyyy or dd/mm/yy | |
176 | // can have / or . or - as separator | |
177 | // can be mm/dd as well | |
178 | possdate = text.match(sorttable.DATE_RE) | |
179 | if (possdate) { | |
180 | // looks like a date | |
181 | first = parseInt(possdate[1]); | |
182 | second = parseInt(possdate[2]); | |
183 | if (first > 12) { | |
184 | // definitely dd/mm | |
185 | return sorttable.sort_ddmm; | |
186 | } else if (second > 12) { | |
187 | return sorttable.sort_mmdd; | |
188 | } else { | |
189 | // looks like a date, but we can't tell which, so assume | |
190 | // that it's dd/mm (English imperialism!) and keep looking | |
191 | sortfn = sorttable.sort_ddmm; | |
192 | } | |
193 | } | |
194 | } | |
195 | } | |
196 | return sortfn; | |
197 | }, | |
198 | ||
199 | getInnerText: function(node) { | |
200 | // gets the text we want to use for sorting for a cell. | |
201 | // strips leading and trailing whitespace. | |
202 | // this is *not* a generic getInnerText function; it's special to sorttable. | |
203 | // for example, you can override the cell text with a customkey attribute. | |
204 | // it also gets .value for <input> fields. | |
205 | ||
206 | hasInputs = (typeof node.getElementsByTagName == 'function') && | |
207 | node.getElementsByTagName('input').length; | |
208 | ||
209 | if (node.getAttribute("sorttable_customkey") != null) { | |
210 | return node.getAttribute("sorttable_customkey"); | |
211 | } | |
212 | else if (typeof node.textContent != 'undefined' && !hasInputs) { | |
213 | return node.textContent.replace(/^\s+|\s+$/g, ''); | |
214 | } | |
215 | else if (typeof node.innerText != 'undefined' && !hasInputs) { | |
216 | return node.innerText.replace(/^\s+|\s+$/g, ''); | |
217 | } | |
218 | else if (typeof node.text != 'undefined' && !hasInputs) { | |
219 | return node.text.replace(/^\s+|\s+$/g, ''); | |
220 | } | |
221 | else { | |
222 | switch (node.nodeType) { | |
223 | case 3: | |
224 | if (node.nodeName.toLowerCase() == 'input') { | |
225 | return node.value.replace(/^\s+|\s+$/g, ''); | |
226 | } | |
227 | case 4: | |
228 | return node.nodeValue.replace(/^\s+|\s+$/g, ''); | |
229 | break; | |
230 | case 1: | |
231 | case 11: | |
232 | var innerText = ''; | |
233 | for (var i = 0; i < node.childNodes.length; i++) { | |
234 | innerText += sorttable.getInnerText(node.childNodes[i]); | |
235 | } | |
236 | return innerText.replace(/^\s+|\s+$/g, ''); | |
237 | break; | |
238 | default: | |
239 | return ''; | |
240 | } | |
241 | } | |
242 | }, | |
243 | ||
244 | reverse: function(tbody) { | |
245 | // reverse the rows in a tbody | |
246 | newrows = []; | |
247 | for (var i=0; i<tbody.rows.length; i++) { | |
248 | newrows[newrows.length] = tbody.rows[i]; | |
249 | } | |
250 | for (var i=newrows.length-1; i>=0; i--) { | |
251 | tbody.appendChild(newrows[i]); | |
252 | } | |
253 | delete newrows; | |
254 | }, | |
255 | ||
256 | /* sort functions | |
257 | each sort function takes two parameters, a and b | |
258 | you are comparing a[0] and b[0] */ | |
259 | sort_numeric: function(a,b) { | |
260 | aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); | |
261 | if (isNaN(aa)) aa = 0; | |
262 | bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); | |
263 | if (isNaN(bb)) bb = 0; | |
264 | return aa-bb; | |
265 | }, | |
266 | sort_alpha: function(a,b) { | |
2fbfe666 | 267 | return a[0].localeCompare(b[0]); |
886c5ccd FM |
268 | }, |
269 | sort_ddmm: function(a,b) { | |
270 | mtch = a[0].match(sorttable.DATE_RE); | |
2ef1d246 FM |
271 | if (mtch) { |
272 | y = mtch[3]; m = mtch[2]; d = mtch[1]; | |
273 | if (m.length == 1) m = '0'+m; | |
274 | if (d.length == 1) d = '0'+d; | |
275 | dt1 = y+m+d; | |
276 | } else { | |
277 | dt1 = "00000000"; | |
278 | } | |
886c5ccd | 279 | mtch = b[0].match(sorttable.DATE_RE); |
2ef1d246 FM |
280 | if (mtch) { |
281 | y = mtch[3]; m = mtch[2]; d = mtch[1]; | |
282 | if (m.length == 1) m = '0'+m; | |
283 | if (d.length == 1) d = '0'+d; | |
284 | dt2 = y+m+d; | |
285 | } else { | |
286 | dt2 = "00000000"; | |
287 | } | |
886c5ccd FM |
288 | if (dt1==dt2) return 0; |
289 | if (dt1<dt2) return -1; | |
290 | return 1; | |
291 | }, | |
292 | sort_mmdd: function(a,b) { | |
293 | mtch = a[0].match(sorttable.DATE_RE); | |
2ef1d246 FM |
294 | if (mtch) { |
295 | y = mtch[3]; d = mtch[2]; m = mtch[1]; | |
296 | if (m.length == 1) m = '0'+m; | |
297 | if (d.length == 1) d = '0'+d; | |
298 | dt1 = y+m+d; | |
299 | } else { | |
300 | dt1 = "00000000"; | |
301 | } | |
886c5ccd | 302 | mtch = b[0].match(sorttable.DATE_RE); |
2ef1d246 FM |
303 | if (mtch) { |
304 | y = mtch[3]; d = mtch[2]; m = mtch[1]; | |
305 | if (m.length == 1) m = '0'+m; | |
306 | if (d.length == 1) d = '0'+d; | |
307 | dt2 = y+m+d; | |
308 | } else { | |
309 | dt2 = "00000000"; | |
310 | } | |
886c5ccd FM |
311 | if (dt1==dt2) return 0; |
312 | if (dt1<dt2) return -1; | |
313 | return 1; | |
314 | }, | |
315 | ||
316 | shaker_sort: function(list, comp_func) { | |
317 | // A stable sort function to allow multi-level sorting of data | |
318 | // see: http://en.wikipedia.org/wiki/Cocktail_sort | |
319 | // thanks to Joseph Nahmias | |
320 | var b = 0; | |
321 | var t = list.length - 1; | |
322 | var swap = true; | |
323 | ||
324 | while(swap) { | |
325 | swap = false; | |
326 | for(var i = b; i < t; ++i) { | |
327 | if ( comp_func(list[i], list[i+1]) > 0 ) { | |
328 | var q = list[i]; list[i] = list[i+1]; list[i+1] = q; | |
329 | swap = true; | |
330 | } | |
331 | } // for | |
332 | t--; | |
333 | ||
334 | if (!swap) break; | |
335 | ||
336 | for(var i = t; i > b; --i) { | |
337 | if ( comp_func(list[i], list[i-1]) < 0 ) { | |
338 | var q = list[i]; list[i] = list[i-1]; list[i-1] = q; | |
339 | swap = true; | |
340 | } | |
341 | } // for | |
342 | b++; | |
343 | ||
344 | } // while(swap) | |
345 | } | |
346 | } | |
347 | ||
348 | /* ****************************************************************** | |
349 | Supporting functions: bundled here to avoid depending on a library | |
350 | ****************************************************************** */ | |
351 | ||
352 | // Dean Edwards/Matthias Miller/John Resig | |
353 | ||
354 | /* for Mozilla/Opera9 */ | |
355 | if (document.addEventListener) { | |
356 | document.addEventListener("DOMContentLoaded", sorttable.init, false); | |
357 | } | |
358 | ||
359 | /* for Internet Explorer */ | |
360 | /*@cc_on @*/ | |
361 | /*@if (@_win32) | |
362 | document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); | |
363 | var script = document.getElementById("__ie_onload"); | |
364 | script.onreadystatechange = function() { | |
365 | if (this.readyState == "complete") { | |
366 | sorttable.init(); // call the onload handler | |
367 | } | |
368 | }; | |
369 | /*@end @*/ | |
370 | ||
371 | /* for Safari */ | |
372 | if (/WebKit/i.test(navigator.userAgent)) { // sniff | |
373 | var _timer = setInterval(function() { | |
374 | if (/loaded|complete/.test(document.readyState)) { | |
375 | sorttable.init(); // call the onload handler | |
376 | } | |
377 | }, 10); | |
378 | } | |
379 | ||
380 | /* for other browsers */ | |
381 | window.onload = sorttable.init; | |
382 | ||
383 | // written by Dean Edwards, 2005 | |
384 | // with input from Tino Zijdel, Matthias Miller, Diego Perini | |
385 | ||
386 | // http://dean.edwards.name/weblog/2005/10/add-event/ | |
387 | ||
388 | function dean_addEvent(element, type, handler) { | |
389 | if (element.addEventListener) { | |
390 | element.addEventListener(type, handler, false); | |
391 | } else { | |
392 | // assign each event handler a unique ID | |
393 | if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; | |
394 | // create a hash table of event types for the element | |
395 | if (!element.events) element.events = {}; | |
396 | // create a hash table of event handlers for each element/event pair | |
397 | var handlers = element.events[type]; | |
398 | if (!handlers) { | |
399 | handlers = element.events[type] = {}; | |
400 | // store the existing event handler (if there is one) | |
401 | if (element["on" + type]) { | |
402 | handlers[0] = element["on" + type]; | |
403 | } | |
404 | } | |
405 | // store the event handler in the hash table | |
406 | handlers[handler.$$guid] = handler; | |
407 | // assign a global event handler to do all the work | |
408 | element["on" + type] = handleEvent; | |
409 | } | |
410 | }; | |
411 | // a counter used to create unique IDs | |
412 | dean_addEvent.guid = 1; | |
413 | ||
414 | function removeEvent(element, type, handler) { | |
415 | if (element.removeEventListener) { | |
416 | element.removeEventListener(type, handler, false); | |
417 | } else { | |
418 | // delete the event handler from the hash table | |
419 | if (element.events && element.events[type]) { | |
420 | delete element.events[type][handler.$$guid]; | |
421 | } | |
422 | } | |
423 | }; | |
424 | ||
425 | function handleEvent(event) { | |
426 | var returnValue = true; | |
427 | // grab the event object (IE uses a global event object) | |
428 | event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); | |
429 | // get a reference to the hash table of event handlers | |
430 | var handlers = this.events[event.type]; | |
431 | // execute each event handler | |
432 | for (var i in handlers) { | |
433 | this.$$handleEvent = handlers[i]; | |
434 | if (this.$$handleEvent(event) === false) { | |
435 | returnValue = false; | |
436 | } | |
437 | } | |
438 | return returnValue; | |
439 | }; | |
440 | ||
441 | function fixEvent(event) { | |
442 | // add W3C standard event methods | |
443 | event.preventDefault = fixEvent.preventDefault; | |
444 | event.stopPropagation = fixEvent.stopPropagation; | |
445 | return event; | |
446 | }; | |
447 | fixEvent.preventDefault = function() { | |
448 | this.returnValue = false; | |
449 | }; | |
450 | fixEvent.stopPropagation = function() { | |
451 | this.cancelBubble = true; | |
452 | } | |
453 | ||
454 | // Dean's forEach: http://dean.edwards.name/base/forEach.js | |
455 | /* | |
456 | forEach, version 1.0 | |
457 | Copyright 2006, Dean Edwards | |
458 | License: http://www.opensource.org/licenses/mit-license.php | |
459 | */ | |
460 | ||
461 | // array-like enumeration | |
462 | if (!Array.forEach) { // mozilla already supports this | |
463 | Array.forEach = function(array, block, context) { | |
464 | for (var i = 0; i < array.length; i++) { | |
465 | block.call(context, array[i], i, array); | |
466 | } | |
467 | }; | |
468 | } | |
469 | ||
470 | // generic enumeration | |
471 | Function.prototype.forEach = function(object, block, context) { | |
472 | for (var key in object) { | |
473 | if (typeof this.prototype[key] == "undefined") { | |
474 | block.call(context, object[key], key, object); | |
475 | } | |
476 | } | |
477 | }; | |
478 | ||
479 | // character enumeration | |
480 | String.forEach = function(string, block, context) { | |
481 | Array.forEach(string.split(""), function(chr, index) { | |
482 | block.call(context, chr, index, string); | |
483 | }); | |
484 | }; | |
485 | ||
486 | // globally resolve forEach enumeration | |
487 | var forEach = function(object, block, context) { | |
488 | if (object) { | |
489 | var resolve = Object; // default | |
490 | if (object instanceof Function) { | |
491 | // functions have a "length" property | |
492 | resolve = Function; | |
493 | } else if (object.forEach instanceof Function) { | |
494 | // the object implements a custom forEach method so use that | |
495 | object.forEach(block, context); | |
496 | return; | |
497 | } else if (typeof object == "string") { | |
498 | // the object is a string | |
499 | resolve = String; | |
500 | } else if (typeof object.length == "number") { | |
501 | // the object is array-like | |
502 | resolve = Array; | |
503 | } | |
504 | resolve.forEach(object, block, context); | |
505 | } | |
506 | }; | |
507 |