转:一步步教你实现表格排序(第一部分)
实现表格排序,说仔细点,就是实现对表格的tbody的行进行排序,因为我们一般是把排序的侦听器绑定在thead的格子中。明确这一点,我们可以用以下方法取出我们所需要的变量
/*得到要排序的表格对象*/ var table = document.getElementById(id); /*得到要变动的部分*/ var tbody = table.getElementsByTagName("tbody")[0]; /*得到要排序的行的集合*/ var rows = tbody.getElementsByTagName("tr");
上面的tbody也可以以这样的方式取得
/*得到要变动的部分*/ var tbody = table.tBodies[0];
个人喜好问题,挑自己顺眼的,反正javascript的写法很灵活。获得rows后,我们就可以对它实现排序,这有现成的排序方法sort()供我们调用,但sort()是数组的实例方法,而rows是一个集合,我们需要把它里面的元素逐个取出来放进一个新数组中进行排序。
var index = []; //把要排序的行的引用放到index数组中。 for (var i=0,l = rows.length; i < l; i++) { index[i] = rows[i]; }
然后我们就可以对数组进行排序,然后把排序后的数组元素放到文档碎片中,重新加入到tbody中去。
讲了这么多理论,想必大家都闷了,这时我们需要一个直观的例子来帮助我们一个立体的认识。如果一直关注我的Blog的朋友,想必对我以下的类的写法不会陌生,我们综合上面的步骤把它们塞进TableSorter类的核心函数中便是!
var Class = { create: function() { return function() { this.initialize.apply(this, arguments); } } } var extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } var TableSorter = Class.create();//表格排序器 TableSorter.prototype = { initialize:function(options){//★★入口函数兼构造函数★★ this.setOptions(options); this.sortTable(this.options.table_id); }, setOptions:function(options){ this.options = { //这里集中设置默认属性 table_id:null//必选项 }; extend(this.options, options || {});//这里是用来重写默认属性 }, ID:function(id){return document.getElementById(id) },//getElementById的快捷方式 TN:function(){//getElementsByTagName的快捷方式 if(arguments.length == 1){ return document.getElementsByTagName(arguments[0]) } else if(arguments.length == 2){ return arguments[0].getElementsByTagName(arguments[1]) } }, sortTable:function(id){//★★核心函数,所有方法在这里集中调用★★ var $ = this, table = $.ID(id), tbody = $.TN(table,"tbody")[0], rows = $.TN(tbody,"tr"), index = []; //把要排序的行的引用放到index数组中。 for (var i=0,l = rows.length; i < l; i++) { index[i] = rows[i]; } table.onclick = function(){//★★添加侦听器,直接绑定在table上★★ var e = arguments[0] || window.event, th = e.srcElement ? e.srcElement : e.target, thn = th.nodeName.toLowerCase(), theadn = th.parentNode.parentNode.nodeName.toLowerCase(); if(thn == 'th' && theadn == 'thead'){ index.sort($.sortby); var fragment = document.createDocumentFragment(); for (var i=0,l = index.length; i < l; i++) { fragment.appendChild(index[i]); } tbody.appendChild(fragment); } }; }, sortby : function(row1,row2){ //row1,row2为调用sort方法的数组rows上的两个元素 var value1 = row1.cells[0].firstChild.nodeValue; var value2 = row2.cells[0].firstChild.nodeValue; return value1.localeCompare(value2) } }
继续讲解我们的类,想必主要疑问是集中在添加侦听器这个部分,为什么要这样写,可以看我另一篇博文《利用Delegator模式保护javascript程序的核心与提高执行性能》,为的是节约侦听器,提高性能。现在我们的表头就只一个th元素,只要监听这个就行了,如果多几个,我们也只需要一个!Event Delegation就神奇在这个地方。然后是排序,由于sort()在没有添加参数的情形下,功能特弱,只能对数字进行排序,因此我们得传入一个比较函数进去作参数。网上有的资数说这个比较函数有两参数,分别为比较数组的元素,这说法不太妥。其实只要这个函数返回的是0,-1或其他负数,1或其他正数,它不在乎有多少参数。现在我们这个表格只能排序一次,不能再次反向排序,因此接着下来的部分就是在这里进行扩展。
现在我们一步步来,先解决多列排序的问题,目前只有对一列进行排序也太难看了。看一下我们的实现:
sortby : function(row1,row2){ //row1,row2为调用sort方法的数组rows上的两个元素 var value1 = row1.cells[0].firstChild.nodeValue; var value2 = row2.cells[0].firstChild.nodeValue; return value1.localeCompare(value2) }
我们想把它改写成以下样子,用于传入第三个参数
sortby : function(row1,row2,i){ //row1,row2为调用sort方法的数组rows上的两个元素 //i为列的序数 var value1 = row1.cells[i].firstChild.nodeValue; var value2 = row2.cells[i].firstChild.nodeValue; return value1.localeCompare(value2) }
不过这样做,我们肯定会撞得头破血流
我们也不要妄图绞尽脑汁想怎样传入row1与row2了,这是sort函数自动传入,只要它里面的括号不为空,它就智能地从它的调用者那里挖两个元素传入去!它的内部实现会随游览器的不同而不同。唯一肯定的是如果存在比较函数就肯定有两个东西被传入。许多人就在这里铩羽而归,有些人则干脆不用sort函数,自己实现一个排序函数,这样就可以为所欲为了。我几经思考,最后动用闭包解决了这个问题。既然那两个参数被sort()方法霸着,那就让它自己传入好了,我传我的,它传它,互不干扰。那怎样做到呢?!利用闭包我们可以构造一种特别的函数,外国人称之为currying函数。currying函数会在函数参数个数不足时,返回接受剩余参数的函数。说到这里,答案很明显了,剩余的参数由sort()来分配就行了。
sortby : function (colIndex) { var _cellIndex = colIndex; return function (row1, row2) { var value1 = row1.cells[_cellIndex].firstChild.nodeValue; var value2 = row2.cells[_cellIndex].firstChild.nodeValue; return value1.localeCompare(value2); }; } /***************相应调用的地方改为*******************/ if(thn == 'th' && theadn == 'thead'){ var colIndex = th.cellIndex; index.sort($.sortby(colIndex)); /************************略***********************/ } /************************略***********************/
我们继续扩展sortby函数,让它能处理更多数据类型,基本上,我们是让它们转换上number与string两种类型
sortby : function (colIndex) { var _cellIndex = colIndex; var format = function(s){ if(/^\d+$/.test(s)){/*如果是正整数*/ return parseInt(s,10)/*确保它的类型为number*/ }else if(/^(-?\d+)(\.\d+)$/.test(s)){/*如果是浮点数*/ return parseFloat(s, 10)/*确保它的类型为number*/ }else if(/^(\d{4})-(\d{1,2})-(\d{1,2})$/.test(s)){/*如果是日期*/ return Date.parse(s.replace(/\-/g, '/'));/*确保它的类型为number*/ }else if(/\%$/.test(s)){/*如果是百分数*/ return Number(s.replace("%", ""));/*确保它的类型为number*/ } else {/*如果是字符串*/ return s.toUpperCase() } } return function (row1, row2) { var value1 = format(row1.cells[_cellIndex].innerHTML); var value2 = format(row2.cells[_cellIndex].innerHTML); return typeof value1 == "string" ? value1.localeCompare(value2) : value1 - value2 }; }
由于那两个参数霸着的缘故,所以我们到这里才能考虑radio与checkbox的问题,要得到他们的checked值,必须要有它们的对象,之前的format()函数只要字符串就很好工作了,因此format()对于对象是无可奈何,它最多能判定th里面是不是含有radio元素或checkbox元素(实质都是input元素)。
/********如果是radio元素,我们就返回一个布尔对象**********/ if(s.toLowerCase().search(/(type=)"?(radio)"?/)!=-1){ return true }
我们可以根据这个布尔值,针对radio进行专门处理。我们可以用getElementsByTagName('input')[0]获得这个radio元素,然后取得其checked值,再转化为0或1,然后进行大小比较就是!
if(typeof(value1) == "boolean" ){ value1 = row1.cells[_cellIndex].getElementsByTagName('input')[0].checked; value1 = (value1 == true) ? 1 :0; value2 = row2.cells[_cellIndex].getElementsByTagName('input')[0].checked; value2 = (value2 == true) ? 1 :0; return value1 - value2; }
checkbox的处理和上面一样,到此sortby()这个比较函数被扩展成这个样子:
sortby : function (colIndex) { var _cellIndex = colIndex; var format = function(s){ if(/^\d+$/.test(s)){/*如果是正整数*/ return parseInt(s,10)/*确保它的类型为number*/ }else if(/^(-?\d+)(\.\d+)$/.test(s)){/*如果是浮点数*/ return parseFloat(s, 10)/*确保它的类型为number*/ }else if(/^(\d{4})-(\d{1,2})-(\d{1,2})$/.test(s)){/*如果是日期*/ return Date.parse(s.replace(/\-/g, '/'));/*确保它的类型为number*/ }else if(/\%$/.test(s)){/*如果是百分数*/ return Number(s.replace("%", ""));/*确保它的类型为number*/ }else if(s.toLowerCase().search(/(type=)"?(radio)"?/)!=-1){ return true;/*确保它的类型为boolean*/ }else if(s.toLowerCase().search(/(type=)"?(checkbox)"?/)!=-1){ return false;/*确保它的类型为boolean*/ } else {/*如果是字符串*/ return s.toUpperCase() }; }; return function (row1, row2) { var value1 = format(row1.cells[_cellIndex].innerHTML), value2 = format(row2.cells[_cellIndex].innerHTML), result = 0; switch(typeof value1){ case "string": result = value1.localeCompare(value2); break; case "number" : result = value1 - value2; break; case "boolean" : /*处理radio与checkbox*/ value1 = row1.cells[_cellIndex].getElementsByTagName('input')[0].checked; value1 = (value1 == true) ? 1 :0; value2 = row2.cells[_cellIndex].getElementsByTagName('input')[0].checked; value2 = (value2 == true) ? 1 :0; result = value1 - value2; break; } return result; }; }
这里IE6与IE7又找我们麻烦了,它们在排序后并不保存radio与checkbox的选中状态,这也没什么,在排序前我们把那些选中的input元素放进一个数组中,排序后再遍历数组把它们钩上便是!为此我们为sortby()函数添加上第二个参数,它是TableSorter这个类的实例的引用,通过它我们可以大大减轻sortby()函数的负担。
checkedElements:[], sortby : function (colIndex,$) { var _cellIndex = colIndex; return function (row1, row2) { var value1 = $.format(row1.cells[_cellIndex].innerHTML), value2 = $.format(row2.cells[_cellIndex].innerHTML), result = 0; switch(typeof value1){ case "string": result = value1.localeCompare(value2); break; case "number" : result = value1 - value2; break; case "boolean" : /*处理radio与checkbox*/ var input1 = $.TN(row1.cells[_cellIndex],'input')[0];; value1 = input1.checked; value1 = (value1 == true) ? 1 :0; if(value1){ $.checkedElements.push(input1); } value2 = $.TN(row2.cells[_cellIndex],'input')[0].checked; value2 = (value2 == true) ? 1 :0; result = value1 - value2; break; } return result; }; }, format : function(s){ /************略***************/ }
然后,我们在排序完后重新为这些对象修正checked值:
if(thn == 'th' && theadn == 'thead'){ var colIndex = th.cellIndex; index.sort($.sortby(colIndex,$)); var fragment = document.createDocumentFragment(); for(var i=0,l = index.length; i < l; i++) { fragment.appendChild(index[i]); } tbody.appendChild(fragment); for(var i=0 ,l = $.checkedElements.length;i< l; i++){ $.checkedElements[i].checked = true; } }
眼尖的朋友们可以大叫——“你忘了给input2作判定,保存其checked状态。”放心,基本上要排序的行,sort()函数都会循环调用两次,它们都有机会为row1或row2。
接着下来我们实现反向排序,原来我们的列一旦排序后就不能动了,我们要改变这种状况。由于在sortby()函数引入第二个参数,实现这功能简直易如反掌!我们为了TableSorter添加一个实例属性colsStatus,它是个数组,用于存储每一列的状态,如果没有反向排序我们设为1,反之为-1,这些1或-1是用来给sortby()最后的值相乘的。
if(thn == 'th' && theadn == 'thead'){ var colIndex = th.cellIndex; $.colsStatus[colIndex] = ($.colsStatus[colIndex] == null) ? 1 : $.colsStatus[colIndex] * -1;/★★★/ index.sort($.sortby(colIndex,$)); /**************略****************/ } /**************略****************/ checkedElements:[], colsStatus:[],/★★★★★★★/ sortby : function (colIndex,$) { var _cellIndex = colIndex; return function (row1, row2) { /**************略****************/ result *= $.colsStatus[_cellIndex];/★★★★★★★/ return result; }; }, /**************略****************/
通过colsStatus,我们还可以做些东西,如在表头显示排序箭头。
原帖地址:http://www.cnblogs.com/rubylouvre/archive/2009/08/13/1544365.html