jQuery EasyUI框架中的Datagrid数据表格组件结构详解
基础DOM结构
什么叫“完整的基础DOM结构”,这里“基础”的意思是指这个结构不依赖具体数据,不依赖Datagrid的view属性,只要存在Datagrid实例就会存在这样的基础DOM结构;而“完整”的意思是指在冻结列,冻结行,标题,footer,分页这些功能块都存在时候的DOM结构。
要搞清楚Datagrid的工作原理,这个DOM结构必须要烂熟于胸的,我们直接来看这个“基础完整DOM结构”是什么样子的:
<!--datagrid的最外层容器,可以使用$(target).datagrid('getPanel')或者$.data(target,'datagrid').panel得到这个DOM对象,这个DOM上其实承载了panel组件-->
<divclass="paneldatagrid">
<!--datagrid的标题区域容器,对应于panel组件的header部分,可以使用$(target).datagrid('getPanel').panel('header')得到这个DOM对象-->
<divclass="panel-header">
<divclass="panel-title"></div>
<divclass="panel-tool"></div>
</div>
<!--datagrid的主体区域容器,对应于panel组件的body部分,可以使用$(target).datagrid('getPanel').panel('body')得到这个DOM对象-->
<divclass="datagrid-wrappanel-body">
<!--工具栏-->
<divclass="datagrid-toolbar"></div>
<!--datagrid视图部分的容器,这是datagrid组件DOM结构的核心,其基础视图结构跟datagrid的view属性无任何关系。-->
<!--对应dc.view-->
<divclass="datagrid-view">
<!--div.datagrid-view1负责展示冻结列部分(包含行号或者frozenColumns)的数据-->
<!--对应dc.view1-->
<divclass="datagrid-view1">
<!--列标题部分-->
<divclass="datagrid-header">
<!--对应dc.header1-->
<divclass="datagrid-header-inner">
<!--样式里有htable关键字,h代表header的意思-->
<tableclass="datagrid-htable">
<tbody>
<trclass="datagrid-header-row"></tr>
</tbody>
</table>
</div>
</div>
<!--列数据部分-->
<divclass="datagrid-body">
<!--对应dc.body1-->
<divclass="datagrid-body-inner">
<!--frozenRows部分(有数据才会有这个table,故不属于基础DOM结构),固定行是1.3.2版本之后才加的功能,注意datagrid-btable-frozen关键样式,btable代码bodytable的意思-->
<tableclass="datagrid-btabledatagrid-btable-frozen"></table>
<!--普通rows部分(有数据才会有这个table,故不属于基础DOM结构)-->
<tableclass="datagird-btable"></table>
</div>
</div>
<!--footer部分-->
<divclass="datagrid-footer">
<!--对应dc.footer1-->
<divclass="datagrid-footer-inner">
<!--ftable代表footertable的意思-->
<tableclass="datagrid-ftable"></table>
</div>
</div>
</div>
<!--div.datagrid-view2负责展示非冻结列部分的数据,大家注意到冻结列和普通列视图是分开的,也就是说冻结列和普通列是在不同表格中展示的,这样会产生一个问题,那就是两个表格行高之间的同步问题。-->
<!--对应dc.view2-->
<divclass="datagrid-view2">
<!--列标题部分-->
<divclass="datagrid-header">
<!--对应dc.header2-->
<divclass="datagrid-header-inner">
<tableclass="datagrid-htable">
<tbody>
<trclass="datagrid-header-row"></tr>
</tbody>
</table>
</div>
</div>
<!--列数据部分,注意这里并无datagrid-body-inner这个子元素,而冻结列对应的body却是有的,这个是细微区别-->
<!--对应dc.body2-->
<divclass="datagrid-body">
<!--frozenRows部分有数据才会有这个table,故不属于基础DOM结构,固定行是1.3.2版本之后才加的功能,-->
<tableclass="datagrid-btabledatagrid-btable-frozen"></table>
<tableclass="datagrid-btable"></table>
</div>
<!--footer部分-->
<divclass="datagrid-footer">
<!--对应dc.footer2-->
<divclass="datagrid-footer-inner">
<tableclass="datagrid-ftable"></table>
</div>
</div>
</div>
</div>
<!--分页部分-->
<divclass="datagrid-pagerpagination"></div>
</div>
</div>
对于这个DOM结构,我在html代码里面已经做了简单说明,这里提一下绑定于Datagrid宿主table上的对象的dc属性,这个dc属性存储了对DOM结构里不同部分的引用,获取dc属性的方法:
$.data(target,'datagrid').dc;
而dc属性跟DOM的对应关系,我也在html中做了详细注释,请大家自行查看,这些都是我们深入认识Datagrid组件的基础。
默认视图分析
上面对Datagrid组件的骨架做了很详细的描述。有了骨架还并不完整,还得有血有肉有衣服穿才行。强大的Datagrid组件允许我们自己定义如何在基础骨架上长出健壮诱人的身体,我们只要定义Datagrid的视图就可以实现。
在大多数情况下,我们并无特别要求,Datagrid给我们提供了默认的视图,默认视图被使用在90%以上的场景,所以对默认视图的分析显得非常有必要。注意视图里面定义了哪些接口,哪些方法,如果要自己写视图的话,最好把这些接口和方法都写齐全。
varview={
/**
*填充表格主体数据(生成数据部分的各行tr)
*@param{DOMobject}targetdatagrid宿主table对应的DOM对象
*@param{DOMobject}container数据主体容器。包含两个可能的值,即:
*1.frozen部分body1,对应的DOM对象为:div.datagrid-view>div.datagrid-view1>div.datagrid-body>div.datagrid-body-inner
*2.常规部分body2,对应的DOM对象为:div.datagrid-view>div.datagrid-view2>div.datagrid-body
*@param{boolean}frozen是否是冻结列
*@return{undefined}未返回值
*/
render:function(target,container,frozen){
vardata=$.data(target,"datagrid");
varopts=data.options;
varrows=data.data.rows;
varfields=$(target).datagrid("getColumnFields",frozen);
if(frozen){
//如果grid不显示rownumbers并且也没有frozenColumns的话,直接退出。
if(!(opts.rownumbers||(opts.frozenColumns&&opts.frozenColumns.length))){
return;
}
}
//定义表格字符串,注意这里使用了数组的join方式代替了传统的"+"运算符,在大多浏览器中,这样效率会更高些。
varhtml=["<tableclass=\"datagrid-btable\"cellspacing=\"0\"cellpadding=\"0\"border=\"0\"><tbody>"];
for(vari=0;i<rows.length;i++){
//striped属性,用于设置grid数据是否隔行变色,当然了实现原理很简单。
varcls=(i%2&&opts.striped)?"class=\"datagrid-rowdatagrid-row-alt\"":"class=\"datagrid-row\"";
/**
*表格的rowStyler属性用于处理数据行的css样式,当然了这个样式仅仅是作用于tr标签上。
*这地方使用call了方法来设置上下文,如果rowStyler函数内部使用了this的话,则this指向datagrid的宿主table对应的DOM对象。
*/
varstyle=opts.rowStyler?opts.rowStyler.call(target,i,rows[i]):"";
varstyler=style?"style=\""+style+"\"":"";
/**
*rowId:行的唯一标示,对应于tr的id属性,其由以下几部分组成:
*1.字符窜常量:"datagrid-row-r";
*2.全局索引index:该索引值从1开始递增,同一个datagrid组件实例拥有唯一值,如果同一页面内有多个datagrid实例,那么其值从1递增分配给每个datagrid实例;
*3.冻结列标识frozen:该标识用于标示是否是冻结列(包含行号和用户指定的frozenColumns),"1"代表冻结列,"2"代表非冻结列;
*4.行数索引:该值才是真正代表“第几行”的意思,该值从0开始递增
*如页面内第一个datagrid实例的非冻结列第10行数据的rowId为"datagrid-row-r1-2-9"
*/
varrowId=data.rowIdPrefix+"-"+(frozen?1:2)+"-"+i;
html.push("<trid=\""+rowId+"\"datagrid-row-index=\""+i+"\""+cls+""+styler+">");
/**
*调用renderRow方法,生成行数据(行内的各列数据)。
*这里的this就是opts.view,之所以用call方法,只是为了传参进去。这里我们使用this.renderRow(target,fields,frozen,i,rows[i])来调用renderRow方法应该也是可以的。
*/
html.push(this.renderRow.call(this,target,fields,frozen,i,rows[i]));
html.push("</tr>");
}
html.push("</tbody></table>");
//用join方法完成字符创拼接后直接innerHTML到容器内。
$(container).html(html.join(""));
},
/**
*[renderFooterdescription]
*@param{DOMobject}targetdatagrid宿主table对应的DOM对象
*@param{DOMobject}container可能为dc.footer1或者dc.footer2
*@param{boolean}frozen是否为frozen区
*@return{undefined}未返回值
*/
renderFooter:function(target,container,frozen){
varopts=$.data(target,"datagrid").options;
//获取footer数据
varrows=$.data(target,"datagrid").footer||[];
varcolumnsFields=$(target).datagrid("getColumnFields",frozen);
//生成footer区的table
varfooterTable=["<tableclass=\"datagrid-ftable\"cellspacing=\"0\"cellpadding=\"0\"border=\"0\"><tbody>"];
for(vari=0;i<rows.length;i++){
footerTable.push("<trclass=\"datagrid-row\"datagrid-row-index=\""+i+"\">");
footerTable.push(this.renderRow.call(this,target,columnsFields,frozen,i,rows[i]));
footerTable.push("</tr>");
}
footerTable.push("</tbody></table>");
$(container).html(footerTable.join(""));
},
/**
*生成某一行数据
*@param{DOMobject}targetdatagrid宿主table对应的DOM对象
*@param{array}fieldsdatagrid的字段列表
*@param{boolean}frozen是否为冻结列
*@param{number}rowIndex行索引(从0开始)
*@param{jsonobject}rowData某一行的数据
*@return{string}单元格的拼接字符串
*/
renderRow:function(target,fields,frozen,rowIndex,rowData){
varopts=$.data(target,"datagrid").options;
//用于拼接字符串的数组
varcc=[];
if(frozen&&opts.rownumbers){
//rowIndex从0开始,而行号显示的时候是从1开始,所以这里要加1.
varrowNumber=rowIndex+1;
//如果分页的话,根据页码和每页记录数重新设置行号
if(opts.pagination){
rowNumber+=(opts.pageNumber-1)*opts.pageSize;
}
/**
*先拼接行号列
*注意DOM特征,用zenCoding可表达为"td.datagrid-td-rownumber>div.datagrid-cell-rownumber"
*/
cc.push("<tdclass=\"datagrid-td-rownumber\"><divclass=\"datagrid-cell-rownumber\">"+rowNumber+"</div></td>");
}
for(vari=0;i<fields.length;i++){
varfield=fields[i];
varcol=$(target).datagrid("getColumnOption",field);
if(col){
varvalue=rowData[field];
//获取用户定义的单元格样式,入参包括:单元格值,当前行数据,当前行索引(从0开始)
varstyle=col.styler?(col.styler(value,rowData,rowIndex)||""):"";
//如果是隐藏列直接设置display为none,否则设置为用户想要的样式
varstyler=col.hidden?"style=\"display:none;"+style+"\"":(style?"style=\""+style+"\"":"");
cc.push("<tdfield=\""+field+"\""+styler+">");
//如果当前列是datagrid组件保留的ck列时,则忽略掉用户定义的样式,即styler属性对datagrid自带的ck列是不起作用的。
if(col.checkbox){
varstyler="";
}else{
varstyler="";
//设置文字对齐属性
if(col.align){
styler+="text-align:"+col.align+";";
}
//设置文字超出td宽时是否自动换行(设置为自动换行的话会撑高单元格)
if(!opts.nowrap){
styler+="white-space:normal;height:auto;";
}else{
/**
*并不是nowrap属性为true单元格就肯定不会被撑高,这还得看autoRowHeight属性的脸色
*当autoRowHeight属性为true的时候单元格的高度是根据单元格内容而定的,这种情况主要是用于表格里展示图片等媒体。
*/
if(opts.autoRowHeight){
styler+="height:auto;";
}
}
}
//这个地方要特别注意,前面所拼接的styler属性并不是作用于td标签上,而是作用于td下的div标签上。
cc.push("<divstyle=\""+styler+"\"");
//如果是ck列,增加"datagrid-cell-check"样式类
if(col.checkbox){
cc.push("class=\"datagrid-cell-check");
}
//如果是普通列,增加"datagrid-cell-check"样式类
else{
cc.push("class=\"datagrid-cell"+col.cellClass);
}
cc.push("\">");
/**
*ck列光设置class是不够的,当突然还得append一个input进去才是真正的checkbox。此处未设置input的id,只设置了name属性。
*我们注意到formatter属性对datagird自带的ck列同样不起作用。
*/
if(col.checkbox){
cc.push("<inputtype=\"checkbox\"name=\""+field+"\"value=\""+(value!=undefined?value:"")+"\"/>");
}
//普通列
else{
/**
*如果单元格有formatter,则将formatter后生成的DOM放到td>div里面
*换句话说,td>div就是如来佛祖的五指山,而formatter只是孙猴子而已,猴子再怎么变化翻跟头,始终在佛祖手里。
*/
if(col.formatter){
cc.push(col.formatter(value,rowData,rowIndex));
}
//操,这是最简单的简况了,将值直接放到td>div里面。
else{
cc.push(value);
}
}
cc.push("</div>");
cc.push("</td>");
}
}
//返回单元格字符串,注意这个函数内部并未把字符串放到文档流中。
returncc.join("");
},
/**
*刷新行数据,只有一个行索引(从0开始),调用的updateRow方法,这里直接跳过。
*@param{DOMobject}targetdatagrid实例的宿主table对应的DOM对象
*@param{number}rowIndex行索引(从0开始)
*@return{undefined}未返回数据
*/
refreshRow:function(target,rowIndex){
this.updateRow.call(this,target,rowIndex,{});
},
/**
*刷新行数据,该接口方法肩负着同步行高,重新计算和布局grid面板等重任
*@param{DOMobject}targetdatagrid实例的宿主table对应的DOM对象
*@param{number}rowIndex行索引(从0开始)
*@param{jsonobject}行数据
*@return{undefined}未返回数据
*/
updateRow:function(target,rowIndex,row){
varopts=$.data(target,"datagrid").options;
varrows=$(target).datagrid("getRows");
$.extend(rows[rowIndex],row);
varstyle=opts.rowStyler?opts.rowStyler.call(target,rowIndex,rows[rowIndex]):"";
functionupdateTableRow(frozen){
varfields=$(target).datagrid("getColumnFields",frozen);
//这个地方查找grid的数据主体表格(可能包含冻结列对应的主体表格和普通列对应的主体表格)
//getTr这个函数,我在博客上介绍过,请参考:http://www.easyui.info/archives/396.html
vartr=opts.finder.getTr(target,rowIndex,"body",(frozen?1:2));
varchecked=tr.find("div.datagrid-cell-checkinput[type=checkbox]").is(":checked");
//这里调用了renderRow方法来重新获取当前行的html字符串
tr.html(this.renderRow.call(this,target,fields,frozen,rowIndex,rows[rowIndex]));
tr.attr("style",style||"");
//更新的时候保留checkbox状态(包含两层信息:一是有ck列;二是ck列被之前就被选中)
if(checked){
tr.find("div.datagrid-cell-checkinput[type=checkbox]")._propAttr("checked",true);
}
};
//更新冻结列对应的行
updateTableRow.call(this,true);
//更新普通列对应的行
updateTableRow.call(this,false);
//重新布局表格面板
$(target).datagrid("fixRowHeight",rowIndex);
},
insertRow:function(target,rowIndex,row){
varstate=$.data(target,"datagrid");
//options
varopts=state.options;
//documentofdatagrid
vardc=state.dc;
vardata=state.data;
//兼容无效的rowIndex,默认设置为在最后一行追加
if(rowIndex==undefined||rowIndex==null){
rowIndex=data.rows.length;
}
//为啥不跟上面的条件并到一起,真是蛋疼
if(rowIndex>data.rows.length){
rowIndex=data.rows.length;
}
/**
*下移rows
*@param{boolean}frozen是否为frozen部分
*@return{undefined}无返回值
*/
functionmoveDownRows(frozen){
//1:冻结列部分;2:普通列部分
varwhichBody=frozen?1:2;
for(vari=data.rows.length-1;i>=rowIndex;i--){
vartr=opts.finder.getTr(target,i,"body",whichBody);
//注意这地方设置了tr的"datagrid-row-index"和"id"属性
tr.attr("datagrid-row-index",i+1);
tr.attr("id",state.rowIdPrefix+"-"+whichBody+"-"+(i+1));
//计算行号
if(frozen&&opts.rownumbers){
//因rowIndex从0开始,以及须插入位置以下的tr要统一下移,所以新行号为i+2
varrownumber=i+2;
//有分页的话,行号还要加上分页数据
if(opts.pagination){
rownumber+=(opts.pageNumber-1)*opts.pageSize;
}
tr.find("div.datagrid-cell-rownumber").html(rownumber);
}
}
};
/**
*插入了,要插两个地方的哦(如果你是男人,你可以淫荡地笑一下)
*@param{boolean}frozen是否是frozen部分
*@return{undefined}未返回值
*/
functiondoInsert(frozen){
varwhichBody=frozen?1:2;
//这行代码,不知道是干嘛的,怕插入得太快而早早缴械,所以才故意拖延时间的么?
varcolumnFields=$(target).datagrid("getColumnFields",frozen);
//构造新插入行的id属性
vartrId=state.rowIdPrefix+"-"+whichBody+"-"+rowIndex;
vartr="<trid=\""+trId+"\"class=\"datagrid-row\"datagrid-row-index=\""+rowIndex+"\"></tr>";
if(rowIndex>=data.rows.length){
//如果已经有记录,则插入tr即可
if(data.rows.length){
//嗯哼,getTr的这个用法不多哦,未传入行索引,第三个参数为"last",随便的意淫一下就知道是获取最后一行了
//然后再在最后一行后插入一行,注意了,这里用的后入式
opts.finder.getTr(target,"","last",whichBody).after(tr);
}
//如果表格尚无记录,则要生成表格,同时插入tr
else{
varcc=frozen?dc.body1:dc.body2;
cc.html("<tablecellspacing=\"0\"cellpadding=\"0\"border=\"0\"><tbody>"+tr+"</tbody></table>");
}
}
//在rowIndex+1前准确无误地插入,注意了,这里是前入式。
else{
opts.finder.getTr(target,rowIndex+1,"body",whichBody).before(tr);
}
};
//下移frozen部分
moveDownRows.call(this,true);
//下移普通列部分
moveDownRows.call(this,false);
//插入frozen区
doInsert.call(this,true);
//插入普通区
doInsert.call(this,false);
//总数加1
data.total+=1;
//维护data.rows数组,这地方是插入一个数组元素了
data.rows.splice(rowIndex,0,row);
//刷新,其中包含了重新布局grid面板等复杂得一笔的操作
//插入本是件很简单愉快的事情,可是你得为其后果负上沉重的代价
this.refreshRow.call(this,target,rowIndex);
},
/**
*删除行接口
*@param{DOMobject}targetdatagrid实例的宿主table对应的DOM对象
*@param{number}rowIndex行索引
*@return{undefined}未返回值
*/
deleteRow:function(target,rowIndex){
varstate=$.data(target,"datagrid");
varopts=state.options;
vardata=state.data;
functionmoveUpRows(frozen){
varwhichBody=frozen?1:2;
for(vari=rowIndex+1;i<data.rows.length;i++){
vartr=opts.finder.getTr(target,i,"body",whichBody);
//"datagrid-row-index"和"id"属性减一
tr.attr("datagrid-row-index",i-1);
tr.attr("id",state.rowIdPrefix+"-"+whichBody+"-"+(i-1));
if(frozen&&opts.rownumbers){
varrownumber=i;
if(opts.pagination){
rownumber+=(opts.pageNumber-1)*opts.pageSize;
}
tr.find("div.datagrid-cell-rownumber").html(rownumber);
}
}
};
//移除行
opts.finder.getTr(target,rowIndex).remove();
//上移frozen区
moveUpRows.call(this,true);
//上移普通区
moveUpRows.call(this,false);
//记录数减一
data.total-=1;
//维护data.rows数据
data.rows.splice(rowIndex,1);
},
/**
*默认的onBeforeRender事件为空
*@param{DOMobject}targetdatagrid实例的宿主table对应的DOM对象
*@param{array}rows要插入的数据
*@return{undefined}默认未返回值
*/
onBeforeRender:function(target,rows){},
/**
*默认的onAfterRender隐藏footer里的行号和check
*@param{DOMobject}targetdatagrid实例的宿主table对应的DOM对象
*@return{undefined}未返回值
*/
onAfterRender:function(target){
varopts=$.data(target,"datagrid").options;
if(opts.showFooter){
varfooter=$(target).datagrid("getPanel").find("div.datagrid-footer");
footer.find("div.datagrid-cell-rownumber,div.datagrid-cell-check").css("visibility","hidden");
}
}
};