<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>Table Sorter Proof of Concept</title>
<style type="text/css"> table { width: 80%; margin: 1em auto; border-collapse: collapse; }
thead th, tfoot th { padding: 0.5em; text-align: left; border: 1px solid black; background-color: #AAF; }
tfoot td { border-top: 1px solid black; }
tbody td { padding: 0.5em; border-left: 1px solid black; border-right: 1px solid black; }
tbody tr.odd { background-color: #DDF; }
td.numeric, th.numeric { text-align: right; }
</style> <!-- common.js --> <script type='text/javascript'> /** * addEvent written by Dean Edwards, 2005 * with input from Tino Zijdel * * http://dean.edwards.name/weblog/2005/10/add-event/ **/ function addEvent(element, type, handler) { // assign each event handler a unique ID if (!handler.$$guid) handler.$$guid = addEvent.guid++; // create a hash table of event types for the element if (!element.events) element.events = {}; // create a hash table of event handlers for each element/event pair var handlers = element.events[type]; if (!handlers) { handlers = element.events[type] = {}; // store the existing event handler (if there is one) if (element["on" + type]) { handlers[0] = element["on" + type]; } } // store the event handler in the hash table handlers[handler.$$guid] = handler; // assign a global event handler to do all the work element["on" + type] = handleEvent; }; // a counter used to create unique IDs addEvent.guid = 1;
function removeEvent(element, type, handler) { // delete the event handler from the hash table if (element.events && element.events[type]) { delete element.events[type][handler.$$guid]; } };
function handleEvent(event) { var returnValue = true; // grab the event object (IE uses a global event object) event = event || fixEvent(window.event); // get a reference to the hash table of event handlers var handlers = this.events[event.type]; // execute each event handler for (var i in handlers) { this.$$handleEvent = handlers[i]; if (this.$$handleEvent(event) === false) { returnValue = false; } } return returnValue; };
function fixEvent(event) { // add W3C standard event methods event.preventDefault = fixEvent.preventDefault; event.stopPropagation = fixEvent.stopPropagation; return event; }; fixEvent.preventDefault = function() { this.returnValue = false; }; fixEvent.stopPropagation = function() { this.cancelBubble = true; };
// end from Dean Edwards
/** * Creates an Element for insertion into the DOM tree. * From http://simon.incutio.com/archive/2003/06/15/javascriptWithXML * * @param element the element type to be created. * e.g. ul (no angle brackets) **/ function createElement(element) { if (typeof document.createElementNS != 'undefined') { return document.createElementNS('http://www.w3.org/1999/xhtml', element); } if (typeof document.createElement != 'undefined') { return document.createElement(element); } return false; }
/** * "targ" is the element which caused this function to be called * from http://www.quirksmode.org/js/events_properties.html **/ function getEventTarget(e) { var targ; if (!e) { e = window.event; } if (e.target) { targ = e.target; } else if (e.srcElement) { targ = e.srcElement; } if (targ.nodeType == 3) { // defeat Safari bug targ = targ.parentNode; }
return targ; }
</script> <!-- css.js --> <script type='text/javascript'> /** * Written by Neil Crosby. * http://www.workingwith.me.uk/ * * Use this wherever you want, but please keep this comment at the top of this file. * * Copyright (c) 2006 Neil Crosby * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. **/ var css = { /** * Returns an array containing references to all elements * of a given tag type within a certain node which have a given class * * @param node the node to start from * (e.g. document, * getElementById('whateverStartpointYouWant') * ) * @param searchClass the class we're wanting * (e.g. 'some_class') * @param tag the tag that the found elements are allowed to be * (e.g. '*', 'div', 'li') **/ getElementsByClass : function(node, searchClass, tag) { var classElements = new Array(); var els = node.getElementsByTagName(tag); var elsLen = els.length; var pattern = new RegExp("(^|\s)"+searchClass+"(\s|$)"); for (var i = 0, j = 0; i < elsLen; i++) { if (this.elementHasClass(els[i], searchClass) ) { classElements[j] = els[i]; j++; } } return classElements; },
/** * PRIVATE. Returns an array containing all the classes applied to this * element. * * Used internally by elementHasClass(), addClassToElement() and * removeClassFromElement(). **/ privateGetClassArray: function(el) { return el.className.split(' '); },
/** * PRIVATE. Creates a string from an array of class names which can be used * by the className function. * * Used internally by addClassToElement(). **/ privateCreateClassString: function(classArray) { return classArray.join(' '); },
/** * Returns true if the given element has been assigned the given class. **/ elementHasClass: function(el, classString) { if (!el) { return false; } var regex = new RegExp('\b'+classString+'\b'); if (el.className.match(regex)) { return true; }
return false; },
/** * Adds classString to the classes assigned to the element with id equal to * idString. **/ addClassToId: function(idString, classString) { this.addClassToElement(document.getElementById(idString), classString); },
/** * Adds classString to the classes assigned to the given element. * If the element already has the class which was to be added, then * it is not added again. **/ addClassToElement: function(el, classString) { var classArray = this.privateGetClassArray(el);
if (this.elementHasClass(el, classString)) { return; // already has element so don't need to add it }
classArray.push(classString);
el.className = this.privateCreateClassString(classArray); },
/** * Removes the given classString from the list of classes assigned to the * element with id equal to idString **/ removeClassFromId: function(idString, classString) { this.removeClassFromElement(document.getElementById(idString), classString); },
/** * Removes the given classString from the list of classes assigned to the * given element. If the element has the same class assigned to it twice, * then only the first instance of that class is removed. **/ removeClassFromElement: function(el, classString) { var classArray = this.privateGetClassArray(el);
for (x in classArray) { if (classString == classArray[x]) { classArray[x] = ''; break; } }
el.className = this.privateCreateClassString(classArray); } } </script> <!-- standardista-table-sorting.js --> <script type='text/javascript'> /** * Written by Neil Crosby. * http://www.workingwith.me.uk/articles/scripting/standardista_table_sorting * * This module is based on Stuart Langridge's "sorttable" code. Specifically, * the determineSortFunction, sortCaseInsensitive, sortDate, sortNumeric, and * sortCurrency functions are heavily based on his code. This module would not * have been possible without Stuart's earlier outstanding work. * * Use this wherever you want, but please keep this comment at the top of this file. * * Copyright (c) 2006 Neil Crosby * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. **/ var standardistaTableSorting = {
that: false, isOdd: false,
sortColumnIndex : -1, lastAssignedId : 0, newRows: -1, lastSortedTable: -1,
/** * Initialises the Standardista Table Sorting module **/ init : function() { // first, check whether this web browser is capable of running this script if (!document.getElementsByTagName) { return; } this.that = this; this.run(); }, /** * Runs over each table in the document, making it sortable if it has a class * assigned named "sortable" and an id assigned. **/ run : function() { var tables = document.getElementsByTagName("table"); for (var i=0; i < tables.length; i++) { var thisTable = tables[i]; if (css.elementHasClass(thisTable, 'sortable')) { this.makeSortable(thisTable); } } }, /** * Makes the given table sortable. **/ makeSortable : function(table) { // first, check if the table has an id. if it doesn't, give it one if (!table.id) { table.id = 'sortableTable'+this.lastAssignedId++; } // if this table does not have a thead, we don't want to know about it if (!table.tHead || !table.tHead.rows || 0 == table.tHead.rows.length) { return; } // we'll assume that the last row of headings in the thead is the row that // wants to become clickable var row = table.tHead.rows[table.tHead.rows.length - 1]; for (var i=0; i < row.cells.length; i++) { // create a link with an onClick event which will // control the sorting of the table var linkEl = createElement('a'); linkEl.href = '#'; linkEl.onclick = this.headingClicked; linkEl.setAttribute('columnId', i); linkEl.title = 'Click to sort'; // move the current contents of the cell that we're // hyperlinking into the hyperlink var innerEls = row.cells[i].childNodes; for (var j = 0; j < innerEls.length; j++) { linkEl.appendChild(innerEls[j]); } // and finally add the new link back into the cell row.cells[i].appendChild(linkEl);
var spanEl = createElement('span'); spanEl.className = 'tableSortArrow'; spanEl.appendChild(document.createTextNode('u00A0u00A0')); row.cells[i].appendChild(spanEl);
} if (css.elementHasClass(table, 'autostripe')) { this.isOdd = false; var rows = table.tBodies[0].rows; // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones for (var i=0;i<rows.length;i++) { this.doStripe(rows[i]); } } }, headingClicked: function(e) { var that = standardistaTableSorting.that; // linkEl is the hyperlink that was clicked on which caused // this method to be called var linkEl = getEventTarget(e); // directly outside it is a td, tr, thead and table var td = linkEl.parentNode; var tr = td.parentNode; var thead = tr.parentNode; var table = thead.parentNode; // if the table we're looking at doesn't have any rows // (or only has one) then there's no point trying to sort it if (!table.tBodies || table.tBodies[0].rows.length <= 1) { return false; }
// the column we want is indicated by td.cellIndex var column = linkEl.getAttribute('columnId') || td.cellIndex; //var column = td.cellIndex; // find out what the current sort order of this column is var arrows = css.getElementsByClass(td, 'tableSortArrow', 'span'); var previousSortOrder = ''; if (arrows.length > 0) { previousSortOrder = arrows[0].getAttribute('sortOrder'); } // work out how we want to sort this column using the data in the first cell // but just getting the first cell is no good if it contains no data // so if the first cell just contains white space then we need to track // down until we find a cell which does contain some actual data var itm = '' var rowNum = 0; while ('' == itm && rowNum < table.tBodies[0].rows.length) { itm = that.getInnerText(table.tBodies[0].rows[rowNum].cells[column]); rowNum++; } var sortfn = that.determineSortFunction(itm);
// if the last column that was sorted was this one, then all we need to // do is reverse the sorting on this column if (table.id == that.lastSortedTable && column == that.sortColumnIndex) { newRows = that.newRows; newRows.reverse(); // otherwise, we have to do the full sort } else { that.sortColumnIndex = column; var newRows = new Array();
for (var j = 0; j < table.tBodies[0].rows.length; j++) { newRows[j] = table.tBodies[0].rows[j]; }
newRows.sort(sortfn); }
that.moveRows(table, newRows); that.newRows = newRows; that.lastSortedTable = table.id; // now, give the user some feedback about which way the column is sorted // first, get rid of any arrows in any heading cells var arrows = css.getElementsByClass(tr, 'tableSortArrow', 'span'); for (var j = 0; j < arrows.length; j++) { var arrowParent = arrows[j].parentNode; arrowParent.removeChild(arrows[j]);
if (arrowParent != td) { spanEl = createElement('span'); spanEl.className = 'tableSortArrow'; spanEl.appendChild(document.createTextNode('u00A0u00A0')); arrowParent.appendChild(spanEl); } } // now, add back in some feedback var spanEl = createElement('span'); spanEl.className = 'tableSortArrow'; if (null == previousSortOrder || '' == previousSortOrder || 'DESC' == previousSortOrder) { spanEl.appendChild(document.createTextNode(' u2191')); spanEl.setAttribute('sortOrder', 'ASC'); } else { spanEl.appendChild(document.createTextNode(' u2193')); spanEl.setAttribute('sortOrder', 'DESC'); } td.appendChild(spanEl); return false; },
getInnerText : function(el) { if ('string' == typeof el || 'undefined' == typeof el) { return el; } if (el.innerText) { return el.innerText; // Not needed but it is faster }
var str = el.getAttribute('standardistaTableSortingInnerText'); if (null != str && '' != str) { return str; } str = '';
var cs = el.childNodes; var l = cs.length; for (var i = 0; i < l; i++) { // 'if' is considerably quicker than a 'switch' statement, // in Internet Explorer which translates up to a good time // reduction since this is a very often called recursive function if (1 == cs[i].nodeType) { // ELEMENT NODE str += this.getInnerText(cs[i]); break; } else if (3 == cs[i].nodeType) { //TEXT_NODE str += cs[i].nodeValue; break; } } // set the innertext for this element directly on the element // so that it can be retrieved early next time the innertext // is requested el.setAttribute('standardistaTableSortingInnerText', str); return str; },
determineSortFunction : function(itm) { var sortfn = this.sortCaseInsensitive; if (itm.match(/^dd[/-]dd[/-]dddd$/)) { sortfn = this.sortDate; } if (itm.match(/^dd[/-]dd[/-]dd$/)) { sortfn = this.sortDate; } if (itm.match(/^[?/)) { sortfn = this.sortCurrency; } if (itm.match(/^d?.?d+$/)) { sortfn = this.sortNumeric; } if (itm.match(/^[+-]?d*.?d+([eE]-?d+)?$/)) { sortfn = this.sortNumeric; } if (itm.match(/^([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5])$/)) { sortfn = this.sortIP; }
return sortfn; }, sortCaseInsensitive : function(a, b) { var that = standardistaTableSorting.that; var aa = that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase(); var bb = that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase(); if (aa==bb) { return 0; } else if (aa<bb) { return -1; } else { return 1; } }, sortDate : function(a,b) { var that = standardistaTableSorting.that;
// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX var aa = that.getInnerText(a.cells[that.sortColumnIndex]); var bb = that.getInnerText(b.cells[that.sortColumnIndex]); var dt1, dt2, yr = -1; 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; } else if (dt1<dt2) { return -1; } return 1; },
sortCurrency : function(a,b) { var that = standardistaTableSorting.that;
var aa = that.getInnerText(a.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,''); var bb = that.getInnerText(b.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,''); return parseFloat(aa) - parseFloat(bb); },
sortNumeric : function(a,b) { var that = standardistaTableSorting.that;
var aa = parseFloat(that.getInnerText(a.cells[that.sortColumnIndex])); if (isNaN(aa)) { aa = 0; } var bb = parseFloat(that.getInnerText(b.cells[that.sortColumnIndex])); if (isNaN(bb)) { bb = 0; } return aa-bb; },
makeStandardIPAddress : function(val) { var vals = val.split('.');
for (x in vals) { val = vals[x];
while (3 > val.length) { val = '0'+val; } vals[x] = val; }
val = vals.join('.');
return val; },
sortIP : function(a,b) { var that = standardistaTableSorting.that;
var aa = that.makeStandardIPAddress(that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase()); var bb = that.makeStandardIPAddress(that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase()); if (aa==bb) { return 0; } else if (aa<bb) { return -1; } else { return 1; } },
moveRows : function(table, newRows) { this.isOdd = false;
// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones for (var i=0;i<newRows.length;i++) { var rowItem = newRows[i];
this.doStripe(rowItem);
table.tBodies[0].appendChild(rowItem); } }, doStripe : function(rowItem) { if (this.isOdd) { css.addClassToElement(rowItem, 'odd'); } else { css.removeClassFromElement(rowItem, 'odd'); } this.isOdd = !this.isOdd; }
}
function standardistaTableSortingInit() { standardistaTableSorting.init(); }
addEvent(window, 'load', standardistaTableSortingInit) </script> </head> <body> <table class='sortable'> <thead> <tr> <td></td> <th colspan='2'>Name</th> <td></td> <td></td> <td></td> </tr> <tr> <th>Date</th> <th>Forename</th> <th>Surname</th> <th>Number</th> <th>Price</th> <th>IP Address</th> <th>Scientific</th> </tr> </thead> <tfoot> <tr> <td></td> <td></td> <td></td> <th class='numeric'>385</th> <th class='numeric'>$160.91</th> <td></td> <td></td> </tr> </tfoot> <tbody> <tr> <td>21/01/2006</td> <td>Neil</td> <td>Crosby</td> <td class='numeric'>123</td> <td class='numeric'>$1.96</td> <td>192.168.1.1</td> <td>-12E2</td> </tr> <tr class='odd'> <td>01/02/2006</td> <td>Becca</td> <td>Courtley</td> <td class='numeric'>122</td> <td class='numeric'>$23.95</td> <td>192.167.2.1</td> <td>12E2</td> </tr> <tr> <td>17/11/2004</td> <td>David</td> <td>Freidman</td> <td class='numeric'>048</td> <td class='numeric'>$14.00</td> <td>192.168.2.1</td> <td>13e-2</td> </tr> <tr class='odd'> <td>17/10/2004</td> <td>Sylvia</td> <td>Tyler</td> <td class='numeric'>43</td> <td class='numeric'>$104.00</td> <td>192.168.2.17</td> <td>12.1e2</td> </tr> <tr> <td>17/11/2005</td> <td>Carl</td> <td>Conway</td> <td class='numeric'>49</td> <td class='numeric'>$17.00</td> <td>192.168.02.13</td> <td>12e3</td> </tr> </tbody> </table> </body> </html>
|