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

<script type="text/javascript"> loadEvent(function(){ // guarder(); }) </script>

今天我们实现对表格的分页支持,不过成品的功能还包括隔行变色,隔列变色,悬浮变色这几个花巧的东西。由于隔行变色是在不可预知的排序环境中进行,因此我们对这些行要做一些特殊处理。

上一部分说过,rows这个对象(table.tBodis[0])是不能直接排序,我们把它转化为一个名为index数组,这部分我们将要使用更多这样的容器来提高我们程序的性能。这个东西,Java那边的人美曰其名“对象池”。遍历数组总比遍历DOM好得多,这些数组不是不改变,如sortElements相当于上部分的index,不过它的地位提高,直辖于对象实例,而不再是一个位于实例方法中的即用即弃的临时数组。诸如此类的改动非常多,不过,像上次分离出去的sortby,format方法就完全没有改过。这里值得我们思索——为什么对一个软件系统进行升级,一些模块受到的压力如此大,以致完全崩塌,被迫从新来过,一些模块却如此鲁棒?由于table是个很特殊很特殊的东西,我也只好使用奇技淫巧来对付它了。说一点,你们就明白了,我们可以用文档碎片添加行元素进行删除行的操作,而且所有浏览器都是这个样子,神奇得很(第一部分我就是用它来清空tbody的)……

现在让我们完成第一个新功能——分页。要进行分页,我们必须知道三个参数。一是每页有多条记录,二是当前是第几页,三是总共有多少条记录,其他的我们就可以推算出来。第一个我们用per_page表示,默认是每页4行(毕竟录入数据是非常麻烦的事);第二个用page表示,默认第1页,它是可以动态改变,用于表格的自渲染,做法基本与《一步步教你实现跨浏览器的JS日历》一样,是个递归调用自身的过程,我们会详细讲这个;第三个用records表示,也就是在不分页的情况下tbody的行数,它是用来修正最后一页的行数。由于我们程序经常需要调用它们,我可不愿意看到每调用一次就计算它们一次,继而把它们设为实例属性。


 setOptions:function(options){
        this.options = { //这里集中设置默认属性
          table_id:null,
          per_page:4,
          page:1,
          records:0
        };
        extend(this.options, options || {});//这里是用来重写默认属性
},

records为零,因为我们无法预计将要改为datagrid的那个表格会有多少行,我们需要计算其tbody的行数。不过在这之前,我们需要搞出sortElements,也就是上一部分的index数组。


    var rows = $.TN(tbody,"tr");
    for (var i=0,l = rows.length; i < l; i++) {
        $.sortElements[i] = rows[i];
    }
    var records = $.sortElements.length;

这样我们就可以分页了。不过按照顺序,在我们渲染分页栏前,要渲染tbody,这时的tbody就和上一部分不太一样。以前,tbody有多少行就显示多少行,现在从第一页开始,我们就限制显示的行数,行数等于per_page的值。为此,我们要清空tbody,然后加入需要显示的行数。但这样一来,效率肯定很低。我们也不能通过删除多余的行,因为经过多次排序后,我们也无法预知行的rowIndex。为此,我们设计两个对象池,直辖于实例,方便其他实例方法调用。一个为addElements,另一个为removeElements。在使用它们之前, 我们得先填空它们。


/**********************略***********************/
for (var i=0,l = rows.length; i < l; i++) {
    $.sortElements[i] = rows[i];
}
for(var i = 0,l = $.sortElements.length;i < l ;i++){
    $.removeElements.push($.sortElements[i]);/*要移除的行*/
    if(i < per_page ){
        $.addElements.push($.removeElements[i]);/*要添加的行*/
    }
}
/**********************略***********************/

那么怎样使用它们呢?我们要明白一点,这些对象池与其说是对象(节点),不如说引用,但javascript的引用机制很特别,引用链最终指向一个实体。我们就是利用这一点隔着个数组来删除或添加行元素。为了复用这些代码,我们把它们封装成一个实例方法,它会在翻页后调用。


        drawTbody:function($,remove,add){
          var fragment = document.createDocumentFragment();
          for(var i in remove){
            $.options.tbody.removeChild(remove[i]);//清空tbody
          }     
          var l = ($.options.per_page > add.length) ? add.length : $.options.per_page; //修正最后一页的行数
          for(var i = 0;i < l ;i++){
            fragment.appendChild(add[i]);
            remove[i]=add[i];
          }
          $.options.tbody.appendChild(fragment);
        },

这样做有一好处就是,假如我们的表格为500行,我们在第一页移除500行加入当中的8行后,下次我们只需移除8行,然后从sortElements另找8行补上。如果我们要实现隔行变色,只要对这8行进行遍历加入相应的CSS className。

按着我们创建分页栏,但在分页栏前我们得创建tfoot,因为我们的分页栏是放在tfoot中。为了方便,我们也把它封装成一个实例方法。


  drawTfoot:function($,table,cols){
          if($.options.records > $.options.per_page){
            var tfoot = $.CE("tfoot"),
            tr = $.CE("tr"),
            td = $.CE("td"),
            pagination = $.CE("div");
            table.appendChild(tfoot);
            tfoot.appendChild(tr);
            tr.appendChild(td);
            td.appendChild(pagination);
            td.colSpan = cols;
            pagination.className ="pagination";
            $.options.pagination = pagination;
          }
        },

我们可以看到,在开头我做了一个判断,如果总记录数大于我们给出的per_page,它才会创建分页栏,换言之,分页栏并不是一定有。接着下来,我们要创建分页栏的内容。为什么不合在一起写呢?因为像tfoot那些元素我们只需要创建一次就行,而分页栏每翻一次页它的样子都不一样。软件工程有一个“对可变性的封装原则”(Principle of Encapsulation of Variation)。它很好理解,就是说找到一个系统的可变因素,将之封装起来。像tfoot可有可无,应当封装起来,像分页栏变化频繁,应当封装起来。由于分页栏是放在td元素的div元素中,我们可以用字符串拼接实现。


drawPagination:function($,current){
          var builder = [];
          if(current - 1 <= 0) {
            builder.push('<span class="disabled prev_page"><<</span>');
          }else{
            builder.push('<a class="prev_page" href="javascript:void(0)" title="');
            builder.push(current-1);
            builder.push('"><<</a>');
          };
          for(var i = current-5 ; i < current ;i++ ){
            if(i > 0){
              builder.push('<a class="next_page" href="javascript:void(0)" title="');
              builder.push(i);
              builder.push('">');
              builder.push(i);
              builder.push('</a>');
            }
          }
          builder.push('<span class="current">');
          builder.push(current);
          builder.push('</span>');
          builder.push('<input type="text" title="page">')

          for(var i = 1 ; i < 6;i++){
            if(current + i <= $.options.total){
              builder.push('<a class="next_page" href="javascript:void(0)" title="');
              builder.push(current + i);
              builder.push('">');
              builder.push(current+i);
              builder.push('</a>');
            }
          }

          if(current + 1 <=  $.options.total){
            builder.push('<a class="next_page" href="javascript:void(0)" title="');
            builder.push(current + 1);
            builder.push('">>></a>');
          };

          return builder.join('');
 },

最后是绑定事件,这次我们也把它分离出去。由于分页与排序都是通过点击触发,我们安排它们在同一个侦听器中。


bindClickEvent:function($,table){
    table.onclick = function(){
        var e = arguments[0] || window.event,
        target = e.srcElement ? e.srcElement : e.target,
        currentN = target.nodeName.toLowerCase(),
        parentN  = target.parentNode.nodeName.toLowerCase(),
        grandN = target.parentNode.parentNode.nodeName.toLowerCase();
        if(currentN == 'th' && grandN == 'thead'){
            $.sortRow($,target);
        }else if(currentN == 'a' && parentN == 'div'){
            var page = Number(target.getAttribute('title'));
            $.sortTable($.options.table_id,page);
        }
    }
},

说明一下,如何进行排序的逻辑也分离出去了,确保每个方法短少精悍。根据我在rails中学到的东西,每个方法尽可能压缩到12行以内,只做自己的份内事!这样一旦发生改动,修改的压力就不传递到其他模块中去。


    sortRow:function($,target){
      var colIndex = target.cellIndex,
      index = $.sortElements,
      up = $.options.up,
      down = $.options.down;
      $.colsStatus[colIndex] = ($.colsStatus[colIndex] == null) ? 1
      : $.colsStatus[colIndex] * -1;
      index.sort($.sortby(colIndex,$));
      /***********************渲染排序箭头**********************************/
      if($.colsStatus[colIndex] > 0){
        target.appendChild(up);
        if(//↓/.test(target.innerHTML)){
          target.removeChild(down);
        }
      }else{
        target.appendChild(down);
        if(//↑/.test(target.innerHTML)){
          target.removeChild(up);
        }
      }
      /***************************移除上次添加的,添加排序好的************/
      $.drawTbody($,$.addElements,index);
      /***********修正IE6,7产能保存checkbox,radio状态的Bug*************/
      for(var i=0 ,l = $.checkedElements.length;i< l; i++){
        $.checkedElements[i].checked = true;
      }
    },

基本上这样,我们就完成分页所需要的步骤了,也让我们的类脱胎换骨,极具扩展性与可制定性。让我们看一下,发现像e,target,currentN,parentN等变量以后在每个侦听器都用得到。基于DRY(Don't repeat yourself)的原则,我们也把它分离成一个独立的函数。


      var getEvent = function(event) {
        var e = event || window.event;
        if (!e) {
          var c = this.getEvent.caller;
          while (c) {
            e = c.arguments[0];
            if (e && (Event == e.constructor || MouseEvent  == e.constructor)) {
              break;
            }
            c = c.caller;
          }
        }
        var target = e.srcElement ? e.srcElement : e.target,
        currentN = target.nodeName.toLowerCase(),
        parentN  = target.parentNode.nodeName.toLowerCase(),
        grandN = target.parentNode.parentNode.nodeName.toLowerCase();
        return [e,target,currentN,parentN,grandN];
      }

继而我们的侦听器就改成这个样子。


        bindClickEvent:function($,table){
          table.onclick = function(){
            var ee = getEvent(),
            target =ee[1],
            currentN = ee[2],
            parentN  = ee[3],
            grandN = ee[4];
            if(currentN == 'th' && grandN == 'thead'){
              $.sortRow($,target);
            }else if(currentN == 'a' && parentN == 'div'){
              var page = Number(target.getAttribute('title'));
              $.sortTable($.options.table_id,page);
            }
          }
        },

通过上述改进,核心函数的职责就大大减轻,主要用来调用其他函数。


/**********************略******************************************/
sortTable:function(id,page){//★★核心函数,所有方法在这时集中调用★★
      var $ = this,
      table = $.ID(id);
      if(table == null){ throw "this table is inexistence!"; return };
      var tbody = $.TN(table,"tbody")[0],
      per_page = $.options.per_page,
      page = page || $.options.page,
      start_row = (page-1) * per_page,  /*从第几行开始*/
      end_row = page * per_page;        /*从第几行结束*/
      /**每渲染一次就更新tbody一次,确保里面的行与removeElements的行相同**/
      $.options.tbody = tbody;
      if($.sortElements.length == 0){
        /**************当初次渲染此控件时*****************************/
        /**************将rows对象集合转化为可排序的数组***************/
        var rows = $.TN(tbody,"tr");
        for (var i=0,l = rows.length; i < l; i++) {
          $.sortElements[i] = rows[i];
        };
        for(var i = 0,l = $.sortElements.length;i < l ;i++){
          $.removeElements.push($.sortElements[i]);/*要移除的行*/
          if(i < per_page ){
            $.addElements.push($.removeElements[i]);/*要添加的行*/
          };
        };
        /*******************创建分页栏********************************/
        var records = $.sortElements.length,
        span = $.CE("span"),
        cols = $.sortElements[0].innerHTML.replace(/(
  
  )|(
  
  )/gi,"┢").split("┢").length-1;
        $.options.records = records;
        $.options.total = Math.ceil(records / per_page);//总页数
        /*********************渲染tfoot******************************/
        $.drawTfoot($,table,cols);
        /*************************创建排序箭头**************************/
        var up = span.cloneNode(true);
        up.style.cssText = "font:normal lighter 1em/100% sans-serif;color:#f00;background:#A9EA00;";
        up.innerHTML = "↑";
        $.options.up = up;
        var down = up.cloneNode(true);
        down.innerHTML = "↓"
        $.options.down = down;
      }else{
        /********************当翻页后再次渲染此控件时********************/
        /********************要移除的行就是上次添加的行*****************/
        $.removeElements = $.addElements;
        $.addElements = new Array;
        /********************防止end_row大于总行数**********************/
        (end_row > $.sortElements.length) && (end_row = $.sortElements.length )
        for(var i = start_row;i < end_row ;i++){
          $.addElements.push($.sortElements[i]);/*要添加的行*/
        }
      }
      /*********************渲染tbody*************************************/
      $.drawTbody($,$.removeElements,$.addElements)
      /*********************渲染pagination********************************/
      var pagination = $.options.pagination;
      pagination && (pagination.innerHTML = $.drawPagination($,page));
      /*************************绑定侦听器********************************/
      $.bindClickEvent($,table);
    },
/**********************略********************************************/

试了一下,分页与排序都没有问题,但输入数字想跳到目标页面时,不起作用。当然我们并没有实现这个功能,现在来完成它。为了符合大多数人的习惯,我们用一个onkeyup事件来完成它。当人们输入数字,然后再按回车键,然后转到相应的页面。在键盘有两个回车键,一个keycode为13,另一个为108,为适合不同人士的喜好,我们设计程序对这两个键都会响应。


    bindKeyupEvent:function($,table){
      table.onkeypress = function(){
        var ee = getEvent(),
        e = ee[0],
        target =ee[1],
        keycode = e.which ? e.which : e.keyCode,
        title = target.getAttribute("title"),
        p = target.value;
        if((title == "page") && (keycode == 13 || keycode == 108) && (/^/d+$/.test(p))){
          p = parseInt(p,10);
          if(p < 0 || p > $.options.total){
            alert("输入的数字不能小于零或大于"+ $.options.total+"!");
          }else{
            $.sortTable($.options.table_id,p);
          }
        }
      }
    },

接着下来我们实现隔列变化,原理很简单,通过col标签来添加相应的CSS className实现。我们可以动态生成这些col标签,然后把入到thead之前。


         drawCols:function($,table,cols){
          if(table.innerHTML.toLowerCase().search(/(class=)"?(yellow)"?/) == -1){
            var col=$.CE("col"),
            thead = $.TN(table,"thead")[0],
            fragment=document.createDocumentFragment();
            for(var i=0,l=cols;i < l;i++){
              var _col=col.cloneNode(true);
              if(i%2==0){
                _col.className="grey";
              }else{
                _col.className="yellow";
              }
              fragment.appendChild(_col);
            }
            table.insertBefore(fragment,thead);
          }
        },

然后在核心函数调用就是,由于我们的类极具扩展性,所以我们加入这些功能是极其容易,也适合我们节奏很快的编程生活。不说,继续下一个功能,隔行变色,这个我们在Firefox,Safari等浏览器可以通过:nth-child()这些结构伪类实现,但为照顾IE全家,我们只有手动在tr标签中添加CSS className。幸而并不是所有的行都要添加,我们只需在显示出来的行中添加,也就是在addElements中的元素中进行操作。


        drawZebraRow:function(rows,length) {
          var i = length;
          while(--i > 0){
            removeClass(rows[i],'even');
            if(!+"/v1" && i%2 == 1){
              addClass(rows[i],"even");
            };
          };
        },

然后在drawTbody实例方法中调用,因为这样做的话,无论我们怎样排序,都能正确隔行变色。这里用到removeClass与addClass方法,你可以在《动态添加样式表规则》看到它们的具体函数,这里就不放出来了。


    drawTbody:function($,remove,add){
      var fragment = document.createDocumentFragment();
      for(var i in remove){
        $.options.tbody.removeChild(remove[i]);//清空tbody
      }
      var per_page = $.options.per_page,
      l = (per_page > add.length) ? add.length : per_page; //修正最后一页的行数
      $.drawZebraRow(add,l);      
    /*******************略**************************/
    },

跟着是悬浮变色,用到两个事件mouseover与mouseout,这功能如何实现想必大家知道了。这里就不详述了。


       bindMouseOverEvent:function(table){
          table.onmouseover = function(){
            var ee = getEvent(),
            target =ee[1],
            parentN = ee[3],
            grandN = ee[4];
            if(parentN == 'tr' && grandN == 'tbody'){
              addClass(target.parentNode,"hover");
            };
          }
        },
        bindMouseOutEvent:function(table){
          table.onmouseout = function(){
            var ee = getEvent(),
            target =ee[1],
            parentN = ee[3],
            grandN = ee[4];
            if(parentN == 'tr' && grandN == 'tbody' && hasClass(target.parentNode,"hover")){
              removeClass(target.parentNode,"hover");
            };
          }
        },

最后附上完整的例子,想看源码可在运行框生成的页面点右键查看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值