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

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

你可能感兴趣的:(排序)