转:一步步教你实现表格排序(第一部分)

实现表格排序,说仔细点,就是实现对表格的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

原文地址:https://www.cnblogs.com/davinci/p/1652196.html