【原创】JQWidgets-TreeGrid 2、初探源码
已知JQWidgets的TreeGrid组件依赖于jqxcore.js、jqxtreegrid.js,实际上它还依赖于jqxdatatable.js。我们先通过一个例子,来探索本次的话题。
需求:
图1
如图,我们有个表格,它具有【收起-展开】的功能,图中标红的部分是JQWidgets的expand-button模型。
目前默认是第一列,根据系统的实际需求,处理人可以串联邀请多个其他的处理人进行审批工作,那我们在展示时,应该针对审批结果来渲染【收起-展开】的按钮(用户是先关注结果,结果是串联,然后再点击展开,查看哪些人被邀请)。
但翻遍了官方API,也没有看到有接口可以改变expand-button所在的列,最后跟踪源码,发现了一些端倪。
解决方案探索:
我们先来跟踪初始化函数、看一下TreeGrid的大致执行流程。
1、当我们调用 $("#treeGrid").jqxTreeGrid(iniObj)时,他首先进入到jqxcore.js中,分析断点行处的a.jqx.applyWidget函数,顾名思义,类似于js的apply函数,其中参数c为我们的treeGrid的HTMLElement,参数f为"jqxTreeGrid"字符串,参数b就是我们初始化时的赋予的对象(iniObj)。由此我们可以知道TreeGrid初始化时,是先经过jqxcore.js进行中转,jqxcore.js的a.fn[f]类似一个中转器。
备注:如果没有特殊的配置,一般是调用applyWidget方法,断点处d=a.data(c,f),a实际上就是JQuery,根据笔者测试 $.data(name,value) 似乎默认都是返回undefined。
图2
2、控制权到了applyWidget的具体实现,他有一个关键的调用e.createInstanced(d)。
d是我们传入的参数对象iniObj
e是在上面有一段代码进行初始化(LIne:3360)
e ? (e.host = g, e.element = b) : e = new a.jqx["_" + c],"" == b.id && (b.id = a.jqx.utilities.createId())
由第一步可知,e根本没有传入,所以此处e实际上就等于new a.jqx["_"+c];//c=jqxTreeGrid。
到此,我们总结一下前面的步骤,当我们在调用$("#treeGrid").jqxTreeGrid(iniObj)时,代码会走到 a.jqx.applyWidge方法中,而jqxTreeGrid方法最终会作为一个字符串参数传入到a.jqx.applyWidget。
而后,我们分析applyWidget的代码实现,我们可以简单认为,$("#treeGrid").jqxTreeGrid(iniObj) 实际上就是 a.jqx["_jqxTreeGrid"].createInstance(iniObj)。
图3
3、当我们进入createInstanced方法时,会发现,代码居然进入到了神秘的jqxdatatable.js(图4),而后再次走下去才进入到jqxtreegrid.js(图5)。
图4
图5
实际上,在第二步时,我们忽略了一个关键代码:代码其实是在循环中调用e.createInstance时,而循环的主体是变量i,往上看,i的赋值代码如下:
for (var i = new Array, e = h.instance; e;) e.isInitialized = !1, i.push(e), e = e.base;
我们发现i的值实际上是push进去的,所以我们可以得出结论:jqxTreeGrid对象的base属性是一个jqxdatatable对象!!
我们打开jqxtreegrid.js,目光锁定第8行,jqxtreegrid.js果然和jqxDataTable有关系,再根据base属性的意思,我们猜想这个JqxTreeGrid是基于JqxDataTable实现的(得出这个结论似乎没什么卵用 ((╯' - ')╯ ┻━┻ )
好了,我们回到关键问题,目前我们大致了解数据流向以及总体结构,我们的问题是想要修改expand-button所在的列,看着似乎比较远,但实际上以及差不多摸到真相了。
图6
4、总体浏览一下jqxtreegrid.js文件,发现文件并不大,不到一千行代码,该文件主要声明了一些TreeGrid特有的方法和关键的_renderrows方法,众所周知,一般框架都会存在一个rederer渲染器,渲染器一般是根据对象内部数据进行html元素的描绘,因此,我们聚焦这个_renderrows方法。
在jqxTreeGrid中,如果想要拥有【收起-展开】的功能,则需要在dataAdapter中定义hierarchy属性(dataAdapter的例子可以参考笔者的另外一篇文章),所以,我们现在_renderrows方法中搜索hierarchy这关键字。
经过笔者煞费苦心的跟踪、分析代码,最后定位到jqxtreegrid.js中Line:300-Line:447行就是 画表格的关键代码,且我们一直寻找的expand-button也在其中。先附上jqxtreegrid.js中Line:300-Line:447行的代码
for (var K = b.source._source.hierarchy && b.source._source.hierarchy.groupingDataFields ? b.source.s_ource.hierarchy.groupingDataFields.length : 0, L = 0; L < j.length; L++) { var M = j[L], N = M.uid; K > 0 && M[d.level] < K && (N = M.uid), void 0 === M.uid && (M.uid = b.dataview.generatekey()); var F = '<tr data-key="' + N + '" role="row" id="row' + L + b.element.id + '">', O = '<tr data-key="' + N + '" role="row" id="row' + L + b.element.id + '">'; if (M.aggregate) var F = '<tr data-role="summaryrow" role="row" id="row' + L + b.element.id + '">', O = '<tr data-role="summaryrow" role="row" id="row' + L + b.element.id + '">'; var P = 0; if (b.rowinfo[N]) void 0 === b.rowinfo[N].checked && (b.rowinfo[N].checked = M[d.checked]), void 0 === b.rowinfo[N].icon && (b.rowinfo[N].icon = M[d.icon]), void 0 === b.rowinfo[N].aggregate && (b.rowinfo[N].aggregate = M[d.aggregate]), void 0 === b.rowinfo[N].row && (b.rowinfo[N].row = M), void 0 === b.rowinfo[N].leaf && (b.rowinfo[N].leaf = M[d.leaf]), void 0 === b.rowinfo[N].expanded && (b.rowinfo[N].expanded = M[d.expanded]); else { var Q = M[d.checked]; void 0 === Q && (Q = !1), b.rowinfo[N] = { selected: M[d.selected], checked: Q, icon: M[d.icon], aggregate: M.aggregate, row: M, leaf: M[d.leaf], expanded: M[d.expanded] } } var R = b.rowinfo[N]; R.row = M, M.originalRecord && (R.originalRecord = M.originalRecord); for (var S = 0, u = 0; u < h; u++) { var T = b.columns.records[u]; (T.pinned || b.rtl && b.columns.records[h - 1].pinned) && (E = !0); var w = T.width; w < T.minwidth && (w = T.minwidth), w > T.maxwidth && (w = T.maxwidth), w -= s, w < 0 && (w = 0); var g = b.toTP("jqx-cell") + " " + b.toTP("jqx-grid-cell") + " " + b.toTP("jqx-item"); T.pinned && (g += " " + b.toTP("jqx-grid-cell-pinned")), b.sortcolumn === T.displayfield && (g += " " + b.toTP("jqx-grid-cell-sort")), b.altRows && L % 2 != 0 && (g += " " + b.toTP("jqx-grid-cell-alt")), b.rtl && (g += " " + b.toTP("jqx-cell-rtl")); var U = ""; if (K > 0 && !i && !M.aggregate && M[d.level] < K) { U += ' colspan="' + h + '"'; for (var D = 0, V = 0; V < h; V++) { var W = b.columns.records[V]; if (!W.hidden) { var X = W.width; X < W.minwidth && (w = W.minwidth), X > W.maxwidth && (w = W.maxwidth), X -= s, X < 0 && (X = 0), D += X } } w = D } var x = '<td role="gridcell"' + U + ' style="max-width:' + w + "px; width:" + w + "px;", Y = '<td role="gridcell"' + U + ' style="pointer-events: none; visibility: hidden; border-color: transparent; max-width:' + w + "px; width:" + w + "px;"; u == h - 1 && 1 == h && (x += "border-right-color: transparent;", Y += "border-right-color: transparent;"), K > 0 && M[d.level] < K && !M.aggregate ? b.rtl && (g += " " + b.toTP("jqx-right-align")) : "left" != T.cellsalign && (g += "right" === T.cellsalign ? " " + b.toTP("jqx-right-align") : " " + b.toTP("jqx-center-align")), R && (R.selected && b.editKey !== N && "none" !== b.selectionMode && (g += " " + b.toTP("jqx-grid-cell-selected"), g += " " + b.toTP("jqx-fill-state-pressed")), R.locked && (g += " " + b.toTP("jqx-grid-cell-locked")), R.aggregate && (g += " " + b.toTP("jqx-grid-cell-pinned"))), T.hidden ? (x += "display: none;", Y += "display: none;", b._hiddencolumns = !0) : (0 != S || b.rtl ? (x += "border-right-width: 0px;", Y += "border-right-width: 0px;") : (x += "border-left-width: 0px;", Y += "border-left-width: 0px;"), S++, P += s + w), T.pinned && (x += "pointer-events: auto;", Y += "pointer-events: auto;"); var Z = ""; if (0 != b.source.hierarchy.length && M.records && (!M.records || 0 !== M.records.length) || this.virtualModeCreateRecords || (R.leaf = !0), M.records && M.records.length > 0 && (R.leaf = !1), b.dataview.filters.length > 0 && M.records && M.records.length > 0) { for (var $ = !1, _ = 0; _ < M.records.length; _++) if (M.records[_]._visible !== !1 && void 0 == M.records[_].aggregate) { $ = !0; break } $ ? R.leaf = !1 : R.leaf = !0 } R && !R.leaf && (R.expanded ? (Z += b.toTP("jqx-tree-grid-expand-button") + " ", Z += b.rtl ? b.toTP("jqx-grid-group-expand-rtl") : b.toTP("jqx-grid-group-expand"), Z += " " + b.toTP("jqx-icon-arrow-down")) : (Z += b.toTP("jqx-tree-grid-collapse-button") + " ", b.rtl ? (Z += b.toTP("jqx-grid-group-collapse-rtl"), Z += " " + b.toTP("jqx-icon-arrow-left")) : (Z += b.toTP("jqx-grid-group-collapse"), Z += " " + b.toTP("jqx-icon-arrow-right")))), (!b.autoRowHeight || 1 === S || b.autoRowHeight && !T.autoCellHeight) && (g += " " + b.toTP("jqx-grid-cell-nowrap")); var aa = b._getcellvalue(T, R.row); if (K > 0 && !M.aggregate && M[d.level] < K && (aa = M.label), "" != T.cellsFormat && a.jqx.dataFormat && (a.jqx.dataFormat.isDate(aa) ? aa = a.jqx.dataFormat.formatdate(aa, T.cellsFormat, b.gridlocalization) : (a.jqx.dataFormat.isNumber(aa) || !isNaN(parseFloat(aa)) && isFinite(aa)) && (aa = a.jqx.dataFormat.formatnumber(aa, T.cellsFormat, b.gridlocalization))), "" != T.cellclassname && T.cellclassname) if ("string" == typeof T.cellclassname) g += " " + T.cellclassname; else { var ba = T.cellclassname(L, T.datafield, b._getcellvalue(T, R.row), R.row, aa); ba && (g += " " + ba) } if ("" != T.cellsRenderer && T.cellsRenderer) { var ca = T.cellsRenderer(N, T.datafield, b._getcellvalue(T, R.row), R.row, aa); void 0 !== ca && (aa = ca) } if (R.aggregate && T.aggregates) { var da = M.siblings.slice(0, M.siblings.length - 1), ea = b._calculateaggregate(T, null, !0, da); if (M[T.displayfield] = "", ea) if (T.aggregatesRenderer) { if (ea) { var fa = T.aggregatesRenderer(ea[T.datafield], T, null, b.getcolumnaggregateddata(T.datafield, T.aggregates, !1, da), "subAggregates"); aa = fa, M[T.displayfield] += name + ":" + ea[T.datafield] + "\n" } } else aa = "", M[T.displayfield] = "", a.each(ea, function () { var a = this; for (obj in a) { var c = obj; c = b._getaggregatename(c); var d = '<div style="position: relative; margin: 0px; overflow: hidden;">' + c + ":" + a[obj] + "</div>"; aa += d, M[T.displayfield] += c + ":" + a[obj] + "\n" } }); else aa = "" } if (1 === S && !b.rtl || T == C && b.rtl || K > 0 && M[d.level] < K) { for (var ga = "", ha = b.toThemeProperty("jqx-tree-grid-indent"), ia = R.leaf ? 1 : 0, ja = 0; ja < M[d.level] + ia; ja++) ga += "<span class='" + ha + "'></span>"; var ka = "<span class='" + Z + "'></span>", la = "", ma = ""; if (this.checkboxes && !M.aggregate) { var na = b.toThemeProperty("jqx-tree-grid-checkbox") + " " + ha + " " + b.toThemeProperty("jqx-checkbox-default") + " " + b.toThemeProperty("jqx-fill-state-normal") + " " + b.toThemeProperty("jqx-rc-all"), oa = !0; if (a.isFunction(this.checkboxes) && (oa = this.checkboxes(N, M), void 0 == oa && (oa = !1)), oa) if (R) { var pa = R.checked; 0 == this.hierarchicalCheckboxes && null === pa && (pa = !1), la += pa ? "<span class='" + na + "'><div class='" + b.toThemeProperty("jqx-tree-grid-checkbox-tick") + " " + b.toThemeProperty("jqx-checkbox-check-checked") + "'></div></span>" : pa === !1 ? "<span class='" + na + "'></span>" : "<span class='" + na + "'><div class='" + b.toThemeProperty("jqx-tree-grid-checkbox-tick") + " " + b.toThemeProperty("jqx-checkbox-check-indeterminate") + "'></div></span>" } else la += "<span class='" + na + "'></span>" } if (this.icons && !M.aggregate) { var qa = b.toThemeProperty("jqx-tree-grid-icon") + " " + ha; if (b.rtl) var qa = b.toThemeProperty("jqx-tree-grid-icon") + " " + b.toThemeProperty("jqx-tree-grid-icon-rtl") + " " + ha; var ra = b.toThemeProperty("jqx-tree-grid-icon-size") + " " + ha, sa = R.icon; a.isFunction(this.icons) && (R.icon = this.icons(N, M), R.icon && (sa = !0)), sa && (ma += R.icon ? "<span class='" + qa + "'><img class='" + ra + "' src='" + R.icon + "'/></span>" : "<span class='" + qa + "'></span>") } var ta = b.autoRowHeight && 1 === S && T.autoCellHeight ? " " + b.toTP("jqx-grid-cell-wrap") : "", ua = ga + ka + la + ma + "<span class='" + b.toThemeProperty("jqx-tree-grid-title") + ta + "'>" + aa + "</span>"; aa = b.rtl ? "<span class='" + b.toThemeProperty("jqx-tree-grid-title") + ta + "'>" + aa + "</span>" + ma + la + ka + ga : ua } if (K > 0 && i && u >= K && M[d.level] < K && (x += "padding-left: 5px; border-left-width: 0px;", Y += "padding-left: 5px; border-left-width: 0px;", aa = "<span style='visibility: hidden;'>-</span>"), x += '" class="' + g + '">', x += aa, x += "</td>", Y += '" class="' + g + '">', Y += aa, Y += "</td>", T.pinned ? (O += x, F += x) : (F += x, E && (O += Y)), K > 0 && !i && M[d.level] < K && !M.aggregate) break } if (0 == f && (b.table[0].style.width = P + 2 + "px", f = P), F += "</tr>", O += "</tr>", A += F, B += O, b.rowDetails && !M.aggregate && this.rowDetailsRenderer) { var va = '<tr data-role="row-details"><td valign="top" align="left" style="pointer-events: auto; max-width:' + w + "px; width:" + w + 'px; overflow: hidden; border-left: none; border-right: none;" colspan="' + b.columns.records.length + '" role="gridcell"', g = b.toTP("jqx-cell") + " " + b.toTP("jqx-grid-cell") + " " + b.toTP("jqx-item"); g += " " + b.toTP("jqx-details"), g += " " + b.toTP("jqx-reset"); var wa = this.rowDetailsRenderer(N, M); wa && (va += '" class="' + g + '"><div style="pointer-events: auto; overflow: hidden;"><div data-role="details">' + wa + "</div></div></td></tr>", A += va, B += va) } }
根据笔者层层努力,最终跟踪到代码块中的第112行: 1===S。改成4===S后,刷新页面,果然生效。
图7
但是随之问题又出来了,框架将这个写死了,我们不能将这个写死,因此可以考虑改为可配置方式,此时我们有两个选择,一是修改jqxdatatable.js、二是修改jqxtreegrid.js,考虑到_renderrows属于jqxtreegrid.js,那最终决定在jqxtreegrid.js添加我们的代码:
图8
图9
图10
调用代码
$("#treeGrid").jqxTreeGrid( { source: dataAdapter, hierarchyIconColumnField:'Fresult', columns: [ { text: 'id', dataField: 'Fid', width: 140 ,hidden:true}, { text: '序号', dataField: 'order', width: 50 ,align: 'center'}, { text: '处理人', dataField: 'Foperator', width: 180 ,align: 'center',cellsAlign: 'left',cellsRenderer: function (row, column, value){ return '****'; }}, { text: '所属环节', dataField: 'Fdepartment', width: 100 ,align: 'center',cellsAlign: 'center'}, { text: '审批结果', dataField: 'Fresult', width: 180, align: 'center', cellsAlign: 'center' }, { text: '上传日期', dataField: 'Ftime', width: 160, align: 'center', cellsAlign: 'center',cellsFormat: "yyyy-MM-dd HH:mm:ss" }, { text: '备注意见', dataField: 'Fopinion', width: 500, align: 'center', cellsAlign: 'center' ,cellsRenderer: function (row, column, value){ return '****'; }}, { text: '附件名称', dataField: 'Foption_name', width: 300, align: 'center', cellsAlign: 'center' }, { text: '附件类型', dataField: 'Foption_type', width: 160, align: 'center', cellsAlign: 'center'} ], ready:function(){ $("#treeGrid").jqxTreeGrid('expandAll'); }, theme:'light', columnsResize:true });
最终效果:
图11
总结:
JQWidgets实际上仍然有很多不足的地方,但当我们摸清其代码的规律,自己动手去修改其代码且完成达到目的,是一件颇具满足感的事情。
本文主要是根据一个小需求来探索TreeGrid组件、所有的JQWdigets都根据jqxcore.js的applyWidget去分发事件,如果是初始化对象时,则会调用对应组件的createInstance方法。每个JQWdigets组件都有renderer渲染器、如果想要修改控件展示逻辑,则应该深入其代码进行研究。
可能目前的需求只需要该某行代码,而我们只关注这些代码,不去扩展,当以后需求变更或增加时,还得重新回过头来继续研究这些代码的扩展部分,对我们来说,迟早的事情理应要一次性做到位,切忌得过且过