jQueryEasyUI框架中的Datagrid数据表格组件结构详解
基础DOM结构
什么叫“完整的基础DOM结构”,这⾥“基础”的意思是指这个结构不依赖具体数据,不依赖Datagrid的view属性,只要存在Datagrid实例就会存在这样的基础DOM结构;⽽“完整”的意思是指在冻结列,冻结⾏,标题,footer,分页这些功能块都存在时候的DOM结构。
要搞清楚Datagrid的⼯作原理,这个DOM结构必须要烂熟于胸的,我们直接来看这个“基础完整DOM结构”是什么样⼦的:
<!-- datagrid的最外层容器,可以使⽤$(target).datagrid('getPanel')或者$.data(target,'datagrid').panel得到这个DOM对象,这个DOM上其实承载了panel组件-->
<div class="panel datagrid">
<!-- datagrid的标题区域容器,对应于panel组件的header部分,可以使⽤$(target).datagrid('getPanel').panel('header')得到这个DOM对象-->
<div class="panel-header">
<div class="panel-title"></div>
<div class="panel-tool"></div>
</div>
<!-- datagrid的主体区域容器,对应于panel组件的body部分,可以使⽤$(target).datagrid('getPanel').panel('body')得到这个DOM对象-->
<div class="datagrid-wrap panel-body">
<!--⼯具栏-->
<div class="datagrid-toolbar"></div>
<!-- datagrid视图部分的容器,这是datagrid组件DOM结构的核⼼,其基础视图结构跟datagrid的view属性⽆任何关系。-->
<!-- 对应dc.view -->
<div class="datagrid-view">
<!-- div.datagrid-view1负责展⽰冻结列部分(包含⾏号或者frozenColumns)的数据-->
<!-- 对应dc.view1 -->
<div class="datagrid-view1">
<!--列标题部分-->
<div class="datagrid-header">
<!-- 对应dc.header1 -->
<div class="datagrid-header-inner">
<!--样式⾥有htable关键字,h代表header的意思-->
<table class="datagrid-htable">
<tbody>
<tr class="datagrid-header-row"></tr>
</tbody>
</table>
</div>
</div>
<!--列数据部分-->
<div class="datagrid-body">
<!-- 对应dc.body1 -->
<div class="datagrid-body-inner">
<!--frozenRows部分(有数据才会有这个table,故不属于基础DOM结构),固定⾏是1.3.2版本之后才加的功能,注意datagrid-btable-frozen关键样式,btable代码body table的意思-->
<table class="datagrid-btable datagrid-btable-frozen"></table>
<!--普通rows部分(有数据才会有这个table,故不属于基础DOM结构)-->
<table class="datagird-btable"></table>
</div>
</div>
<!--footer部分-->
<div class="datagrid-footer">
<!-- 对应dc.footer1 -->
<div class="datagrid-footer-inner">
<!--ftable代表footer table的意思-->
<table class="datagrid-ftable"></table>
</div>
</div>
</div>
<!-- div.datagrid-view2负责展⽰⾮冻结列部分的数据,⼤家注意到冻结列和普通列视图是分开的,也就是说冻结列和普通列是在不同表格中展⽰的,这样会产⽣⼀个问题,那就是两个表格⾏⾼之间的同步问题。--> <!-- 对应dc.view2 -->
<div class="datagrid-view2">
<!--列标题部分-->
<div class="datagrid-header">
<!-- 对应dc.header2 -->
<div class="datagrid-header-inner">
<table class="datagrid-htable">
<tbody>
<tr class="datagrid-header-row"></tr>
</tbody>
</table>
</div>
</div>
<!--列数据部分,注意这⾥并⽆datagrid-body-inner这个⼦元素,⽽冻结列对应的body却是有的,这个是细微区别-->
<!-- 对应dc.body2 -->
<div class="datagrid-body">
<!--frozenRows部分有数据才会有这个table,故不属于基础DOM结构,固定⾏是1.3.2版本之后才加的功能,-->
<table class="datagrid-btable datagrid-btable-frozen"></table>
<table class="datagrid-btable"></table>
</div>
<!--footer部分-->
<div class="datagrid-footer">
<!-- 对应dc.footer2 -->
<div class="datagrid-footer-inner">
<table class="datagrid-ftable"></table>
</div>
</div>
</div>
</div>
<!--分页部分-->
<div class="datagrid-pager pagination"></div>
</div>
</div>
对于这个DOM结构,我在html代码⾥⾯已经做了简单说明,这⾥提⼀下绑定于Datagrid宿主table上的对象的dc属性,这个dc属性存储了对DOM结构⾥不同部分的引⽤,获取dc
属性的⽅法:
$.data(target,'datagrid').dc;
⽽dc属性跟DOM的对应关系,我也在html中做了详细注释,请⼤家⾃⾏查看,这些都是我们深⼊认识Datagrid组件的基础。
默认视图分析
上⾯对Datagrid组件的⾻架做了很详细的描述。有了⾻架还并不完整,还得有⾎有⾁有⾐服穿才⾏。强⼤的Datagrid组件允许我们⾃⼰定义如何在基础⾻架上长出健壮诱⼈的⾝体,我们只要定义Datagrid的视图就可以实现。
在⼤多数情况下,我们并⽆特别要求,Datagrid给我们提供了默认的视图,默认视图被使⽤在90%以上的场景,所以对默认视图的分析显得⾮常有必要。注意视图⾥⾯定义了哪
些接⼝,哪些⽅法,如果要⾃⼰写视图的话,最好把这些接⼝和⽅法都写齐全。
var view = {
/**
* 填充表格主体数据(⽣成数据部分的各⾏tr)
* @param {DOM object} target datagrid宿主table对应的DOM对象
* @param {DOM object} 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) {
var data = $.data(target, "datagrid");
var opts = data.options;
var rows = ws;
var fields = $(target).datagrid("getColumnFields", frozen);
if(frozen) {
//如果grid不显⽰rownumbers并且也没有frozenColumns的话,直接退出。
if(!(wnumbers || (opts.frozenColumns && opts.frozenColumns.length))) {
return;
}
}
//定义表格字符串,注意这⾥使⽤了数组的join⽅式代替了传统的"+"运算符,在⼤多浏览器中,这样效率会更⾼些。
var html = ["<table class=\"datagrid-btable\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tbody>"];
for(var i = 0; i < rows.length; i++) {
//striped属性,⽤于设置grid数据是否隔⾏变⾊,当然了实现原理很简单。
var cls = (i % 2 && opts.striped) ? "class=\"datagrid-row datagrid-row-alt\"" : "class=\"datagrid-row\"";
/**
* 表格的rowStyler属性⽤于处理数据⾏的css样式,当然了这个样式仅仅是作⽤于tr标签上。
* 这地⽅使⽤call了⽅法来设置上下⽂,如果rowStyler函数内部使⽤了this的话,则this指向datagrid的
宿主table对应的DOM对象。
*/
var style = wStyler ? wStyler.call(target, i, rows[i]) : "";
var styler = 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"
*/
var rowId = wIdPrefix + "-" + (frozen ? 1 : 2) + "-" + i;
html.push("<tr id=\"" + rowId + "\" datagrid-row-index=\"" + i + "\" " + cls + " " + styler + ">");
/**
* 调⽤renderRow⽅法,⽣成⾏数据(⾏内的各列数据)。
* 这⾥的this就是opts.view,之所以⽤call⽅法,只是为了传参进去。这⾥我们使⽤derRow(target,fields,frozen,i,rows[i])来调⽤renderRow⽅法应该也是可以的。 */
html.derRow.call(this, target, fields, frozen, i, rows[i]));
html.push("</tr>");
}
html.push("</tbody></table>");
//⽤join⽅法完成字符创拼接后直接innerHTML到容器内。
$(container).html(html.join(""));
},
/**
* [renderFooter description]
* @param {DOM object} target datagrid宿主table对应的DOM对象
* @param {DOM object} container 可能为dc.footer1或者dc.footer2
* @param {boolean} frozen 是否为frozen区
* @return {undefined} 未返回值
*/
renderFooter: function(target, container, frozen) {
var opts = $.data(target, "datagrid").options;
//获取footer数据
var rows = $.data(target, "datagrid").footer || [];
var columnsFields = $(target).datagrid("getColumnFields", frozen);
//⽣成footer区的table
var footerTable = ["<table class=\"datagrid-ftable\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tbody>"];
for(var i = 0; i < rows.length; i++) {
footerTable.push("<tr class=\"datagrid-row\" datagrid-row-index=\"" + i + "\">");
footerTable.derRow.call(this, target, columnsFields, frozen, i, rows[i]));
footerTable.push("</tr>");
}
footerTable.push("</tbody></table>");
$(container).html(footerTable.join(""));
},
/**
* ⽣成某⼀⾏数据
* @param {DOM object} target datagrid宿主table对应的DOM对象
* @param {array} fields datagrid的字段列表
* @param {boolean} frozen 是否为冻结列
* @param {number} rowIndex ⾏索引(从0开始)
* @param {json object} rowData 某⼀⾏的数据
* @return {string} 单元格的拼接字符串
*/
renderRow: function(target, fields, frozen, rowIndex, rowData) {
var opts = $.data(target, "datagrid").options;
//⽤于拼接字符串的数组
var cc = [];
if(frozen && wnumbers) {
//rowIndex从0开始,⽽⾏号显⽰的时候是从1开始,所以这⾥要加1.
var rowNumber = rowIndex + 1;
//如果分页的话,根据页码和每页记录数重新设置⾏号
if(opts.pagination) {
rowNumber += (opts.pageNumber - 1) * opts.pageSize;
}
/**
* 先拼接⾏号列
* 注意DOM特征,⽤zenCoding可表达为"td.datagrid-td-rownumber>div.datagrid-cell-rownumber"
*/
cc.push("<td class=\"datagrid-td-rownumber\"><div class=\"datagrid-cell-rownumber\">" + rowNumber + "</div></td>");
}
for(var i = 0; i < fields.length; i++) {
var field = fields[i];
var col = $(target).datagrid("getColumnOption", field);
if(col) {
var value = rowData[field];
//获取⽤户定义的单元格样式,⼊参包括:单元格值,当前⾏数据,当前⾏索引(从0开始)
var style = col.styler ? (col.styler(value, rowData, rowIndex) || "") : "";
//如果是隐藏列直接设置display为none,否则设置为⽤户想要的样式
var styler = col.hidden ? "style=\"display:none;" + style + "\"" : (style ? "style=\"" + style + "\"" : "");
cc.push("<td field=\"" + field + "\" " + styler + ">");
//如果当前列是datagrid组件保留的ck列时,则忽略掉⽤户定义的样式,即styler属性对datagrid⾃带的ck列是不起作⽤的。
if(col.checkbox) {
var styler = "";
} el {
var styler = "";
//设置⽂字对齐属性
if(col.align) {
styler += "text-align:" + col.align + ";";
}
//设置⽂字超出td宽时是否⾃动换⾏(设置为⾃动换⾏的话会撑⾼单元格)
if(!wrap) {
styler += "white-space:normal;height:auto;";
} el {
/**
* 并不是nowrap属性为true单元格就肯定不会被撑⾼,这还得看autoRowHeight属性的脸⾊
* 当autoRowHeight属性为true的时候单元格的⾼度是根据单元格内容⽽定的,这种情况主要是⽤于表格⾥展⽰图⽚等媒体。
*/
if(opts.autoRowHeight) {
styler += "height:auto;";
}
}
}
//这个地⽅要特别注意,前⾯所拼接的styler属性并不是作⽤于td标签上,⽽是作⽤于td下的div标签上。
cc.push("<div style=\"" + styler + "\" ");
//如果是ck列,增加"datagrid-cell-check"样式类
if(col.checkbox) {
cc.push("class=\"datagrid-cell-check ");
}
//如果是普通列,增加"datagrid-cell-check"样式类
el {
cc.push("class=\"datagrid-cell " + llClass);
}
cc.push("\">");
/**
* ck列光设置class是不够的,当突然还得append⼀个input进去才是真正的checkbox。此处未设置input的id,只设置了name属性。 * 我们注意到formatter属性对datagird⾃带的ck列同样不起作⽤。
*/
if(col.checkbox) {
cc.push("<input type=\"checkbox\" name=\"" + field + "\" value=\"" + (value != undefined ? value : "") + "\"/>");
}
//普通列
el {
/**
* 如果单元格有formatter,则将formatter后⽣成的DOM放到td>div⾥⾯
* 换句话说,td>div就是如来佛祖的五指⼭,⽽formatter只是孙猴⼦⽽已,猴⼦再怎么变化翻跟头,始终在佛祖⼿⾥。
*/
if(col.formatter) {
cc.push(col.formatter(value, rowData, rowIndex));
}
//操,这是最简单的简况了,将值直接放到td>div⾥⾯。
el {
cc.push(value);
}
}
cc.push("</div>");
cc.push("</td>");
}
}
//返回单元格字符串,注意这个函数内部并未把字符串放到⽂档流中。
return cc.join("");
},
/**
* 刷新⾏数据,只有⼀个⾏索引(从0开始),调⽤的updateRow⽅法,这⾥直接跳过。
* @param {DOM object} target datagrid实例的宿主table对应的DOM对象
* @param {number} rowIndex ⾏索引(从0开始)
* @return {undefined} 未返回数据
*/
refreshRow: function(target, rowIndex) {
this.updateRow.call(this, target, rowIndex, {});
},
/
**
* 刷新⾏数据,该接⼝⽅法肩负着同步⾏⾼,重新计算和布局grid⾯板等重任
* @param {DOM object} target datagrid实例的宿主table对应的DOM对象
* @param {number} rowIndex ⾏索引(从0开始)
* @param {json object} ⾏数据
* @return {undefined} 未返回数据
*/
updateRow: function(target, rowIndex, row) {
var opts = $.data(target, "datagrid").options;
var rows = $(target).datagrid("getRows");
$.extend(rows[rowIndex], row);
var style = wStyler ? wStyler.call(target, rowIndex, rows[rowIndex]) : "";
function updateTableRow(frozen) {
var fields = $(target).datagrid("getColumnFields", frozen);
//这个地⽅查找grid的数据主体表格(可能包含冻结列对应的主体表格和普通列对应的主体表格)
//getTr这个函数,我在博客上介绍过,请参考:www.easyui.info/archives/396.html
var tr = Tr(target, rowIndex, "body", (frozen ? 1 : 2));
var checked = tr.find("div.datagrid-cell-check input[type=checkbox]").is(":checked");
//这⾥调⽤了renderRow⽅法来重新获取当前⾏的html字符串
tr.derRow.call(this, target, fields, frozen, rowIndex, rows[rowIndex]));
tr.attr("style", style || "");
//更新的时候保留checkbox状态(包含两层信息:⼀是有ck列;⼆是ck列被之前就被选中)
if(checked) {
tr.find("div.datagrid-cell-check input[type=checkbox]")._propAttr("checked", true);
}
};
//更新冻结列对应的⾏
updateTableRow.call(this, true);
//更新普通列对应的⾏
updateTableRow.call(this, fal);
//重新布局表格⾯板
$(target).datagrid("fixRowHeight", rowIndex);
},
inrtRow: function(target, rowIndex, row) {
var state = $.data(target, "datagrid");
//options
var opts = state.options;
//document of datagrid
var dc = state.dc;
var data = state.data;
//兼容⽆效的rowIndex,默认设置为在最后⼀⾏追加
if(rowIndex == undefined || rowIndex == null) {
rowIndex = ws.length;
}
/
/为啥不跟上⾯的条件并到⼀起,真是蛋疼
if(rowIndex > ws.length) {
rowIndex = ws.length;
}
/**
* 下移rows
* @param {boolean} frozen 是否为frozen部分
* @return {undefined} ⽆返回值
*/
function moveDownRows(frozen) {
//1:冻结列部分;2:普通列部分
var whichBody = frozen ? 1 : 2;
for(var i = ws.length - 1; i >= rowIndex; i--) {
var tr = Tr(target, i, "body", whichBody);
//注意这地⽅设置了tr的"datagrid-row-index"和"id"属性
tr.attr("datagrid-row-index", i + 1);
tr.attr("id", wIdPrefix + "-" + whichBody + "-" + (i + 1));
//计算⾏号
if(frozen && wnumbers) {
//因rowIndex从0开始,以及须插⼊位置以下的tr要统⼀下移,所以新⾏号为i+2
var rownumber = 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} 未返回值
*/
function doInrt(frozen) {
var whichBody = frozen ? 1 : 2;
//这⾏代码,不知道是⼲嘛的,怕插⼊得太快⽽早早缴械,所以才故意拖延时间的么?
var columnFields = $(target).datagrid("getColumnFields", frozen);
//构造新插⼊⾏的id属性
var trId = wIdPrefix + "-" + whichBody + "-" + rowIndex;
var tr = "<tr id=\"" + trId + "\" class=\"datagrid-row\" datagrid-row-index=\"" + rowIndex + "\"></tr>";
if(rowIndex >= ws.length) {
//如果已经有记录,则插⼊tr即可
ws.length) {
/
/嗯哼,getTr的这个⽤法不多哦,未传⼊⾏索引,第三个参数为"last",随便的意淫⼀下就知道是获取最后⼀⾏了 //然后再在最后⼀⾏后插⼊⼀⾏,注意了,这⾥⽤的后⼊式
Tr(target, "", "last", whichBody).after(tr);
}
//如果表格尚⽆记录,则要⽣成表格,同时插⼊tr
el {
var cc = frozen ? dc.body1 : dc.body2;
cc.html("<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tbody>" + tr + "</tbody></table>");
}
}
//在rowIndex + 1前准确⽆误地插⼊,注意了,这⾥是前⼊式。
el {
Tr(target, rowIndex + 1, "body", whichBody).before(tr);
}
};
//下移frozen部分
moveDownRows.call(this, true);
//下移普通列部分
moveDownRows.call(this, fal);
//插⼊frozen区
doInrt.call(this, true);
//插⼊普通区
doInrt.call(this, fal);
/
/总数加1
//维护ws数组,这地⽅是插⼊⼀个数组元素了
//刷新,其中包含了重新布局grid⾯板等复杂得⼀笔的操作
//插⼊本是件很简单愉快的事情,可是你得为其后果负上沉重的代价
},
/**
* 删除⾏接⼝
* @param {DOM object} target datagrid实例的宿主table对应的DOM对象
* @param {number} rowIndex ⾏索引
* @return {undefined} 未返回值
*/
deleteRow: function(target, rowIndex) {
var state = $.data(target, "datagrid");
var opts = state.options;
var data = state.data;
function moveUpRows(frozen) {
var whichBody = frozen ? 1 : 2;
for(var i = rowIndex + 1; i < ws.length; i++) {
var tr = Tr(target, i, "body", whichBody);
/
/"datagrid-row-index"和"id"属性减⼀
tr.attr("datagrid-row-index", i - 1);
tr.attr("id", wIdPrefix + "-" + whichBody + "-" + (i - 1));
if(frozen && wnumbers) {
var rownumber = i;
if(opts.pagination) {
rownumber += (opts.pageNumber - 1) * opts.pageSize;
}
tr.find("div.datagrid-cell-rownumber").html(rownumber);
}
}
};
//移除⾏
Tr(target, rowIndex).remove();
//上移frozen区
moveUpRows.call(this, true);
//上移普通区
moveUpRows.call(this, fal);
//记录数减⼀
//维护ws数据
},
/**
* 默认的onBeforeRender事件为空
* @param {DOM object} target datagrid实例的宿主table对应的DOM对象
* @param {array} rows 要插⼊的数据
* @return {undefined} 默认未返回值
*/
onBeforeRender: function(target, rows) {},
/**
* 默认的onAfterRender 隐藏footer⾥的⾏号和check
* @param {DOM object} target datagrid实例的宿主table对应的DOM对象
* @return {undefined} 未返回值
*/
onAfterRender: function(target) {
var opts = $.data(target, "datagrid").options;
if(opts.showFooter) {
var footer = $(target).datagrid("getPanel").find("div.datagrid-footer");
footer.find("div.datagrid-cell-rownumber,div.datagrid-cell-check").css("visibility", "hidden"); }
}
};