基礎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 = data.data.rows; var fields = $(target).datagrid("getColumnFields", frozen); if(frozen) { //如果grid不顯示rownumbers并且也沒有frozenColumns的話,直接退出。 if(!(opts.rownumbers || (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 = opts.rowStyler ? opts.rowStyler.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 = data.rowIdPrefix + "-" + (frozen ? 1 : 2) + "-" + i; html.push("<tr id="" + 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("")); }, /** * [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.push(this.renderRow.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 && opts.rownumbers) { //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 = ""; } else { var styler = ""; //設置文字對齊屬性 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("<div style="" + 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("<input type="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>"); } } //返回單元格字符串,注意這個函數內部并未把字符串放到文檔流中。 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 = opts.rowStyler ? opts.rowStyler.call(target, rowIndex, rows[rowIndex]) : ""; function updateTableRow(frozen) { var fields = $(target).datagrid("getColumnFields", frozen); //這個地方查找grid的數據主體表格(可能包含凍結列對應的主體表格和普通列對應的主體表格) //getTr這個函數,我在博客上介紹過,請參考:http://www.easyui.info/archives/396.html var tr = opts.finder.getTr(target, rowIndex, "body", (frozen ? 1 : 2)); var checked = tr.find("div.datagrid-cell-check input[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-check input[type=checkbox]")._propAttr("checked", true); } }; //更新凍結列對應的行 updateTableRow.call(this, true); //更新普通列對應的行 updateTableRow.call(this, false); //重新布局表格面板 $(target).datagrid("fixRowHeight", rowIndex); }, insertRow: 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 = data.rows.length; } //為啥不跟上面的條件并到一起,真是蛋疼 if(rowIndex > data.rows.length) { rowIndex = data.rows.length; } /** * 下移rows * @param {boolean} frozen 是否為frozen部分 * @return {undefined} 無返回值 */ function moveDownRows(frozen) { //1:凍結列部分;2:普通列部分 var whichBody = frozen ? 1 : 2; for(var i = data.rows.length - 1; i >= rowIndex; i--) { var tr = 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 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 doInsert(frozen) { var whichBody = frozen ? 1 : 2; //這行代碼,不知道是干嘛的,怕插入得太快而早早繳械,所以才故意拖延時間的么? var columnFields = $(target).datagrid("getColumnFields", frozen); //構造新插入行的id屬性 var trId = state.rowIdPrefix + "-" + whichBody + "-" + rowIndex; var tr = "<tr id="" + 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 { var cc = frozen ? dc.body1 : dc.body2; cc.html("<table cellspacing="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 {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 < data.rows.length; i++) { var tr = 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) { var rownumber = 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 {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"); } } };