今天我们实现对表格的分页支持,不过成品的功能还包括隔行变色,隔列变色,悬浮变色这几个花巧的东西。由于隔行变色是在不可预知的排序环境中进行,因此我们对这些行要做一些特殊处理。
上一部分说过,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");
};
}
},
最后附上完整的例子,想看源码可在运行框生成的页面点右键查看。