/** * layui.treeTable * 树表组件 */ layui.define(['table'], function (exports) { 'use strict'; var $ = layui.$; var form = layui.form; var table = layui.table; var hint = layui.hint(); // api var treeTable = { config: {}, // 事件 on: table.on, // 遍历字段 eachCols: table.eachCols, index: table.index, set: function (options) { var that = this; that.config = $.extend({}, that.config, options); return that; }, resize: table.resize, getOptions: table.getOptions, hideCol: table.hideCol, renderData: table.renderData }; // 操作当前实例 var thisTreeTable = function () { var that = this; var options = that.config; var id = options.id || options.index; return { config: options, reload: function (options, deep) { that.reload.call(that, options, deep); }, reloadData: function (options, deep) { treeTable.reloadData(id, options, deep); } }; }; /** * 获取当前实例 * @param {string} id 表格id * @returns {Class} */ var getThisTable = function (id) { var that = thisTreeTable.that[id]; if (!that) hint.error( id ? "The treeTable instance with ID '" + id + "' not found" : 'ID argument required' ); return that || null; }; // 字符 var MOD_NAME = 'treeTable'; var MOD_ID = 'lay-table-id'; var HIDE = 'layui-hide'; var ELEM_VIEW = '.layui-table-view'; // var ELEM_TREE = '.layui-table-tree'; // var ELEM_TOOL = '.layui-table-tool'; // var ELEM_BOX = '.layui-table-box'; // var ELEM_HEADER = '.layui-table-header'; var ELEM_BODY = '.layui-table-body'; var ELEM_MAIN = '.layui-table-main'; // var ELEM_FIXED = '.layui-table-fixed'; var ELEM_FIXL = '.layui-table-fixed-l'; var ELEM_FIXR = '.layui-table-fixed-r'; var ELEM_CHECKED = 'layui-table-checked'; var TABLE_TREE = 'layui-table-tree'; var LAY_DATA_INDEX = 'LAY_DATA_INDEX'; var LAY_DATA_INDEX_HISTORY = 'LAY_DATA_INDEX_HISTORY'; var LAY_PARENT_INDEX = 'LAY_PARENT_INDEX'; var LAY_CHECKBOX_HALF = 'LAY_CHECKBOX_HALF'; var LAY_EXPAND = 'LAY_EXPAND'; var LAY_HAS_EXPANDED = 'LAY_HAS_EXPANDED'; var LAY_ASYNC_STATUS = 'LAY_ASYNC_STATUS'; var LAY_CASCADE = ['all', 'parent', 'children', 'none']; var HTML_TAG_RE = /<[^>]+?>/; var ICON_PROPS = [ 'flexIconClose', 'flexIconOpen', 'iconClose', 'iconOpen', 'iconLeaf', 'icon' ]; /** * 构造器 * @class */ var Class = function (options) { var that = this; that.index = ++treeTable.index; that.config = $.extend(true, {}, that.config, treeTable.config, options); // 处理一些属性 that.init(); that.render(); }; var updateCache = function (id, childrenKey, data) { var tableCache = table.cache[id]; layui.each(data || tableCache, function (index, item) { var itemDataIndex = item[LAY_DATA_INDEX] || ''; if (itemDataIndex.indexOf('-') !== -1) { tableCache[itemDataIndex] = item; } item[childrenKey] && updateCache(id, childrenKey, item[childrenKey]); }); }; var updateOptions = function (id, options, reload) { var that = getThisTable(id); reload === 'reloadData' || (that.status = { // 用于记录一些状态信息 expand: {} // 折叠状态 }); var thatOptionsTemp = $.extend(true, {}, that.getOptions(), options); var treeOptions = thatOptionsTemp.tree; var childrenKey = treeOptions.customName.children; var idKey = treeOptions.customName.id; // 处理属性 delete options.hasNumberCol; delete options.hasChecboxCol; delete options.hasRadioCol; table.eachCols( null, function (i1, item1) { if (item1.type === 'numbers') { options.hasNumberCol = true; } else if (item1.type === 'checkbox') { options.hasChecboxCol = true; } else if (item1.type === 'radio') { options.hasRadioCol = true; } }, thatOptionsTemp.cols ); var parseData = options.parseData; var done = options.done; // treeTable重载数据时,会先加载显示顶层节点,然后根据重载数据前的子节点展开状态,展开相应的子节点, // 那么如果重载数据前有滚动条滚动在某个位子,重新加载时顶层节点如果比较少,只显示顶层节点时没有滚动条的情况下, // 自动展开子节点后,滚动条就会显示在顶部,无法保持在重载数据之前的位置。 // 处理保持滚动条的问题,重载数据前记录滚动条的位置 if (reload === 'reloadData' && thatOptionsTemp.scrollPos === 'fixed') { that.scrollTopCache = that.config.elem.next().find(ELEM_BODY).scrollTop(); } if (thatOptionsTemp.url) { // 异步加载的时候需要处理parseData进行转换 if (!reload || (reload && parseData && !parseData.mod)) { options.parseData = function () { var parseDataThat = this; var args = arguments; var retData = args[0]; if (layui.type(parseData) === 'function') { retData = parseData.apply(parseDataThat, args) || args[0]; } var dataName = parseDataThat.response.dataName; // 处理 isSimpleData if (treeOptions.data.isSimpleData && !treeOptions.async.enable) { // 异步加载和 isSimpleData 不应该一起使用 retData[dataName] = that.flatToTree(retData[dataName]); } // 处理节点状态 updateStatus( retData[dataName], function (item) { item[LAY_EXPAND] = LAY_EXPAND in item ? item[LAY_EXPAND] : item[idKey] !== undefined && that.status.expand[item[idKey]]; }, childrenKey ); if ( parseDataThat.autoSort && parseDataThat.initSort && parseDataThat.initSort.type ) { layui.sort( retData[dataName], parseDataThat.initSort.field, parseDataThat.initSort.type === 'desc', true ); } that.initData(retData[dataName]); return retData; }; options.parseData.mod = true; } } else { if (options.data !== undefined) { options.data = options.data || []; // 处理 isSimpleData if (treeOptions.data.isSimpleData) { options.data = that.flatToTree(options.data); } that.initData(options.data); } } if (!reload || (reload && done && !done.mod)) { options.done = function () { var args = arguments; var doneThat = this; // undefined: 初始 render 或 reload,两者本质没有区别可以不做区分 // 'reloadData': 重载数据 // 'renderData': 重新渲染数据 var renderType = args[3]; var isRenderData = renderType === 'renderData'; if (!isRenderData) { delete that.isExpandAll; } var tableView = this.elem.next(); that.updateStatus(null, { LAY_HAS_EXPANDED: false // 去除已经打开过的状态 }); // 更新cache中的内容 将子节点也存到cache中 updateCache(id, childrenKey); // 更新全选框的状态 var layTableAllChooseElem = tableView.find( '[name="layTableCheckbox"][lay-filter="layTableAllChoose"]' ); if (layTableAllChooseElem.length) { var checkStatus = treeTable.checkStatus(id); layTableAllChooseElem.prop({ checked: checkStatus.isAll && checkStatus.data.length, indeterminate: !checkStatus.isAll && checkStatus.data.length }); } if ( !isRenderData && thatOptionsTemp.autoSort && thatOptionsTemp.initSort && thatOptionsTemp.initSort.type ) { treeTable.sort(id); } that.renderTreeTable(tableView); // 恢复滚动条位置 if (renderType === 'reloadData' && doneThat.scrollPos === 'fixed') { tableView.find(ELEM_BODY).scrollTop(that.scrollTopCache); } if (layui.type(done) === 'function') { return done.apply(doneThat, args); } }; options.done.mod = true; } // 处理图标 if (options && options.tree && options.tree.view) { layui.each(ICON_PROPS, function (i, iconProp) { if (options.tree.view[iconProp] !== undefined) { options.tree.view[iconProp] = that.normalizedIcon( options.tree.view[iconProp] ); } }); } }; Class.prototype.init = function () { var that = this; var options = that.config; var cascade = options.tree.data.cascade; if (LAY_CASCADE.indexOf(cascade) === -1) { options.tree.data.cascade = 'all'; // 超出范围的都重置为全联动 } // 先初始一个空的表格以便拿到对应的表格实例信息 var tableIns = table.render( $.extend({}, options, { data: [], url: '', done: null }) ); var id = tableIns.config.id; thisTreeTable.that[id] = that; // 记录当前实例对象 that.tableIns = tableIns; updateOptions(id, options); }; // 初始默认配置 Class.prototype.config = { tree: { customName: { children: 'children', // 节点数据中保存子节点数据的属性名称 isParent: 'isParent', // 节点数据保存节点是否为父节点的属性名称 name: 'name', // 节点数据保存节点名称的属性名称 id: 'id', // 唯一标识的属性名称 pid: 'parentId', // 父节点唯一标识的属性名称 icon: 'icon' // 图标的属性名称 }, view: { indent: 14, // 层级缩进量 flexIconClose: '', // 关闭时候的折叠图标 flexIconOpen: '', // 打开时候的折叠图标 showIcon: true, // 是否显示图标(节点类型图标) icon: '', // 节点图标,如果设置了这个属性或者数据中有这个字段信息,不管打开还是关闭都以这个图标的值为准 iconClose: '', // 关闭时候的图标 iconOpen: '', // 打开时候的图标 iconLeaf: '', // 叶子节点的图标 showFlexIconIfNotParent: false, // 当节点不是父节点的时候是否显示折叠图标 dblClickExpand: true, // 双击节点时,是否自动展开父节点的标识 expandAllDefault: false // 默认展开所有节点 }, data: { isSimpleData: false, // 是否简单数据模式 rootPid: null, // 根节点的父 ID 值 cascade: 'all' // 级联方式 默认全部级联:all 可选 级联父 parent 级联子 children }, async: { enable: false, // 是否开启异步加载模式,只有开启的时候其他参数才起作用 url: '', // 异步加载的接口,可以根据需要设置与顶层接口不同的接口,如果相同可以不设置该参数 type: null, // 请求的接口类型,设置可缺省同上 contentType: null, // 提交参数的数据类型,设置可缺省同上 headers: null, // 设置可缺省同上 where: null, // 设置可缺省同上 autoParam: [] // 自动参数 }, callback: { beforeExpand: null, // 展开前的回调 return false 可以阻止展开的动作 onExpand: null // 展开之后的回调 } } }; Class.prototype.normalizedIcon = function (iconStr) { return iconStr ? HTML_TAG_RE.test(iconStr) ? iconStr : '' : ''; }; Class.prototype.getOptions = function () { var that = this; if (that.tableIns) { return table.getOptions(that.tableIns.config.id); // 获取表格的实时配置信息 } else { return that.config; } }; function flatToTree(flatArr, idKey, pIdKey, childrenKey, rootPid) { idKey = idKey || 'id'; pIdKey = pIdKey || 'parentId'; childrenKey = childrenKey || 'children'; // 创建一个空的 map 对象,用于保存所有的节点 var map = {}; var rootNodes = []; var idTemp = ''; var pidTemp = ''; layui.each(flatArr, function (index, item) { idTemp = idKey + item[idKey]; pidTemp = idKey + item[pIdKey]; // 将节点存入 map 对象 if (!map[idTemp]) { map[idTemp] = {}; map[idTemp][childrenKey] = []; } // 合并节点 var tempObj = {}; tempObj[childrenKey] = map[idTemp][childrenKey]; map[idTemp] = $.extend({}, item, tempObj); var isRootNode = rootPid ? map[idTemp][pIdKey] === rootPid : !map[idTemp][pIdKey]; if (isRootNode) { rootNodes.push(map[idTemp]); } else { if (!map[pidTemp]) { map[pidTemp] = {}; map[pidTemp][childrenKey] = []; } map[pidTemp][childrenKey].push(map[idTemp]); } }); return rootNodes; } Class.prototype.flatToTree = function (tableData) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var tableId = options.id; tableData = tableData || table.cache[tableId]; return flatToTree( tableData, customName.id, customName.pid, customName.children, treeOptions.data.rootPid ); }; Class.prototype.treeToFlat = function (tableData, parentId, parentIndex) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var childrenKey = customName.children; var pIdKey = customName.pid; var flat = []; layui.each(tableData, function (i1, item1) { var dataIndex = (parentIndex ? parentIndex + '-' : '') + i1; var dataNew = $.extend({}, item1); dataNew[pIdKey] = typeof item1[pIdKey] !== 'undefined' ? item1[pIdKey] : parentId; flat.push(dataNew); flat = flat.concat( that.treeToFlat(item1[childrenKey], item1[customName.id], dataIndex) ); }); return flat; }; // 通过当前行数据返回 treeNode 信息 Class.prototype.getTreeNode = function (data) { var that = this; if (!data) { return hint.error('Node data not found'); } // var options = that.getOptions(); // var treeOptions = options.tree; // var tableId = options.id; // var customName = treeOptions.customName; // 带上一些常用的方法 return { data: data, dataIndex: data[LAY_DATA_INDEX], getParentNode: function () { return that.getNodeByIndex(data[LAY_PARENT_INDEX]); } }; }; // 通过 index 返回节点信息 Class.prototype.getNodeByIndex = function (index) { var that = this; var treeNodeData = that.getNodeDataByIndex(index); if (!treeNodeData) { return hint.error('Node data not found by index: ' + index); } var options = that.getOptions(); // var treeOptions = options.tree; // var customName = treeOptions.customName; // var parentKey = customName.parent; var tableId = options.id; var treeNode = { data: treeNodeData, dataIndex: treeNodeData[LAY_DATA_INDEX], getParentNode: function () { return that.getNodeByIndex(treeNodeData[LAY_PARENT_INDEX]); }, update: function (data) { return treeTable.updateNode(tableId, index, data); }, remove: function () { return treeTable.removeNode(tableId, index); }, expand: function (opts) { return treeTable.expandNode( tableId, $.extend({}, opts, { index: index }) ); }, setChecked: function (opts) { return treeTable.setRowChecked( tableId, $.extend({}, opts, { index: index }) ); } }; treeNode.dataIndex = index; return treeNode; }; // 通过 id 获取节点信息 Class.prototype.getNodeById = function (id) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var idKey = customName.id; // 通过 id 拿到数据的 dataIndex var dataIndex = ''; var tableDataFlat = treeTable.getData(options.id, true); layui.each(tableDataFlat, function (i1, item1) { if (item1[idKey] === id) { dataIndex = item1[LAY_DATA_INDEX]; return true; } }); if (!dataIndex) { return; } // 用 index return that.getNodeByIndex(dataIndex); }; // 通过 index 获取节点数据 Class.prototype.getNodeDataByIndex = function (index, clone, newValue) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var tableCache = table.cache[tableId]; // 获取当前行中的数据 var dataCache = tableCache[index]; // 若非删除操作,则返回合并后的数据 if (newValue !== 'delete' && dataCache) { $.extend(dataCache, newValue); return clone ? $.extend({}, dataCache) : dataCache; } // 删除操作 var dataRet = tableCache; var indexArr = String(index).split('-'); // if (options.url || indexArr.length > 1) tableCache = null // 只有在删除根节点的时候才需要处理 // 根据 index 进行数据处理 for ( var i = 0, childrenKey = treeOptions.customName.children; i < indexArr.length; i++ ) { if (newValue && i === indexArr.length - 1) { if (newValue === 'delete') { // 删除并返回当前数据 // 同步 cache --- 此段代码注释缘由:data 属性模式造成数据重复执行 splice (@Gitee: #I7Z0A/I82E2S) /*if (tableCache) { layui.each(tableCache, function (i1, item1) { if (item1[LAY_DATA_INDEX] === index) { tableCache.splice(i1, 1); return true; } }) }*/ return (i ? dataRet[childrenKey] : dataRet).splice(indexArr[i], 1)[0]; } else { // 更新值 $.extend((i ? dataRet[childrenKey] : dataRet)[indexArr[i]], newValue); } } dataRet = i ? dataRet[childrenKey][indexArr[i]] : dataRet[indexArr[i]]; } return clone ? $.extend({}, dataRet) : dataRet; }; treeTable.getNodeDataByIndex = function (id, index) { var that = getThisTable(id); if (!that) return; return that.getNodeDataByIndex(index, true); }; // 判断是否是父节点 /* var checkIsParent = function(data, isParentKey, childrenKey) { isParentKey = isParentKey || 'isParent'; childrenKey = childrenKey || 'children'; layui.each(data, function(i1, item1) { if (!(isParentKey in item1)) { item1[isParentKey] = !!(item1[childrenKey] && item1[childrenKey].length); checkIsParent(item1[childrenKey]); } }); }; */ Class.prototype.initData = function (data, parentIndex) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; data = data || that.getTableData(); var customName = treeOptions.customName; var isParentKey = customName.isParent; var childrenKey = customName.children; var update = function (data, parentIndex) { layui.each(data, function (i1, item1) { if (!(isParentKey in item1)) { item1[isParentKey] = !!( item1[childrenKey] && item1[childrenKey].length ); } item1[LAY_DATA_INDEX_HISTORY] = item1[LAY_DATA_INDEX]; item1[LAY_PARENT_INDEX] = parentIndex = parentIndex || ''; var dataIndex = (item1[LAY_DATA_INDEX] = (parentIndex ? parentIndex + '-' : '') + i1); update(item1[childrenKey] || [], dataIndex); }); }; update(data, parentIndex); updateCache(tableId, childrenKey, data); return data; }; // 与 tableId 有关带防抖的方法 var debounceFn = (function () { var fn = {}; return function (tableId, func, wait) { if (!fn[tableId]) { fn[tableId] = layui.debounce(func, wait); } return fn[tableId]; }; })(); // 优化参数,添加一个 getNodeByIndex 方法 只传 表格id 和行 dataIndex 分几步优化 todo var expandNode = function ( treeNode, expandFlag, sonSign, focus, callbackFlag, done ) { // treeNode // 需要展开的节点 var trElem = treeNode.trElem; var tableViewElem = treeNode.tableViewElem || trElem.closest(ELEM_VIEW); var tableId = treeNode.tableId || tableViewElem.attr(MOD_ID); var options = treeNode.options || table.getOptions(tableId); var dataIndex = treeNode.dataIndex || trElem.attr('lay-data-index'); // 可能出现多层 var treeTableThat = getThisTable(tableId); var treeOptions = options.tree || {}; var customName = treeOptions.customName || {}; var isParentKey = customName.isParent; var trData = treeTableThat.getNodeDataByIndex(dataIndex); // 后续调优:对已经展开的节点进行展开和已经关闭的节点进行关闭应该做优化减少不必要的代码执行 todo var isToggle = layui.type(expandFlag) !== 'boolean'; var trExpand = isToggle ? !trData[LAY_EXPAND] : expandFlag; var retValue = trData[isParentKey] ? trExpand : null; if ( callbackFlag && trExpand != trData[LAY_EXPAND] && (!trData[LAY_ASYNC_STATUS] || trData[LAY_ASYNC_STATUS] === 'local') ) { var beforeExpand = treeOptions.callback.beforeExpand; if (layui.type(beforeExpand) === 'function') { if (beforeExpand(tableId, trData, expandFlag) === false) { return retValue; } } } var trExpanded = trData[LAY_HAS_EXPANDED]; // 展开过,包括异步加载 // 找到表格中的同类节点(需要找到lay-data-index一致的所有行) var trsElem = tableViewElem.find('tr[lay-data-index="' + dataIndex + '"]'); var flexIconElem = trsElem.find('.layui-table-tree-flexIcon'); treeTableThat.updateNodeIcon({ scopeEl: trsElem, isExpand: trExpand, isParent: trData[isParentKey] }); trData[LAY_EXPAND] = trExpand; var trDataId = trData[customName.id]; trDataId !== undefined && (treeTableThat.status.expand[trDataId] = trExpand); if (retValue === null) { return retValue; } var childNodes = trData[customName.children] || []; // 处理子节点展示与否 if (trExpand) { // 展开 if (trExpanded) { // 已经展开过 if (!childNodes.length) return; //异步如果子节点没有数据情况下双点行展开所有已展开的节点问题解决 trsElem .nextAll( childNodes .map(function (value, index, array) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .removeClass(HIDE); layui.each(childNodes, function (i1, item1) { if (!item1[isParentKey]) { return; } if (sonSign && !isToggle && !item1[LAY_EXPAND]) { // 非状态切换的情况下 // 级联展开子节点 expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); } else if (item1[LAY_EXPAND]) { // 初始化级联展开 expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, true, undefined, undefined, undefined, done ); } }); } else { var asyncSetting = treeOptions.async || {}; var asyncUrl = asyncSetting.url || options.url; if ( asyncSetting.enable && trData[isParentKey] && (!trData[LAY_ASYNC_STATUS] || trData[LAY_ASYNC_STATUS] === 'error') ) { trData[LAY_ASYNC_STATUS] = 'loading'; flexIconElem.html( '' ); // 异步获取子节点数据成功之后处理方法 var asyncSuccessFn = function (data) { trData[LAY_ASYNC_STATUS] = 'success'; trData[customName.children] = data; treeTableThat.initData( trData[customName.children], trData[LAY_DATA_INDEX] ); expandNode( treeNode, true, isToggle ? false : sonSign, focus, callbackFlag, done ); }; var format = asyncSetting.format; // 自定义数据返回方法 if (layui.type(format) === 'function') { format(trData, options, asyncSuccessFn); return retValue; } var params = {}; // 参数 var data = $.extend(params, asyncSetting.where || options.where); var asyncAutoParam = asyncSetting.autoParam; layui.each(asyncAutoParam, function (index, item) { // var itemStr = item; var itemArr = item.split('='); data[itemArr[0].trim()] = trData[(itemArr[1] || itemArr[0]).trim()]; }); var asyncContentType = asyncSetting.contentType || options.contentType; if ( asyncContentType && asyncContentType.indexOf('application/json') == 0 ) { // 提交 json 格式 data = JSON.stringify(data); } var asyncType = asyncSetting.method || options.method; var asyncDataType = asyncSetting.dataType || options.dataType; var asyncJsonpCallback = asyncSetting.jsonpCallback || options.jsonpCallback; var asyncHeaders = asyncSetting.headers || options.headers; var asyncParseData = asyncSetting.parseData || options.parseData; var asyncResponse = asyncSetting.response || options.response; var ajaxOptions = { type: asyncType || 'get', url: asyncUrl, contentType: asyncContentType, data: data, dataType: asyncDataType || 'json', jsonpCallback: asyncJsonpCallback, headers: asyncHeaders || {}, success: function (res) { // 若有数据解析的回调,则获得其返回的数据 if (typeof asyncParseData === 'function') { res = asyncParseData.call(options, res) || res; } // 检查数据格式是否符合规范 if (res[asyncResponse.statusName] != asyncResponse.statusCode) { trData[LAY_ASYNC_STATUS] = 'error'; trData[LAY_EXPAND] = false; // 异常处理 todo flexIconElem.html( '' ); // 事件 } else { // 正常返回 asyncSuccessFn(res[asyncResponse.dataName]); } }, error: function (e, msg) { trData[LAY_ASYNC_STATUS] = 'error'; trData[LAY_EXPAND] = false; // 异常处理 todo typeof options.error === 'function' && options.error(e, msg); } }; if (options.ajax) { options.ajax(ajaxOptions, 'treeNodes'); } else { $.ajax(ajaxOptions); } return retValue; } trExpanded = trData[LAY_HAS_EXPANDED] = true; if (childNodes.length) { // 判断是否需要排序 if (options.initSort && (!options.url || options.autoSort)) { var initSort = options.initSort; if (initSort.type) { layui.sort( childNodes, initSort.field, initSort.type === 'desc', true ); } else { // 恢复默认 layui.sort(childNodes, table.config.indexName, null, true); } } treeTableThat.initData( trData[customName.children], trData[LAY_DATA_INDEX] ); // 将数据通过模板得出节点的html代码 var str2 = table.getTrHtml( tableId, childNodes, null, null, dataIndex ); var str2Obj = { trs: $(str2.trs.join('')), trs_fixed: $(str2.trs_fixed.join('')), trs_fixed_r: $(str2.trs_fixed_r.join('')) }; var dataLevel = dataIndex.split('-').length - 1; var dataLevelNew = (dataLevel || 0) + 1; layui.each(childNodes, function (childIndex, childItem) { str2Obj.trs .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); str2Obj.trs_fixed .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); str2Obj.trs_fixed_r .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); }); tableViewElem .find(ELEM_MAIN) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs); tableViewElem .find(ELEM_FIXL) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs_fixed); tableViewElem .find(ELEM_FIXR) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs_fixed_r); // 初始化新增的节点中的内容 treeTableThat.renderTreeTable(str2Obj.trs, dataLevelNew); if (sonSign && !isToggle) { // 非状态切换的情况下 // 级联展开/关闭子节点 layui.each(childNodes, function (i1, item1) { expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); }); } } } } else { treeTableThat.isExpandAll = false; // 关闭 if (sonSign && !isToggle) { // 非状态切换的情况下 layui.each(childNodes, function (i1, item1) { expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); }); tableViewElem .find( childNodes .map(function (value, index, array) { // 只隐藏直接子节点,其他由递归的处理 return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .addClass(HIDE); } else { var childNodesFlat = treeTableThat.treeToFlat( childNodes, trData[customName.id], dataIndex ); tableViewElem .find( childNodesFlat .map(function (value, index, array) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .addClass(HIDE); } } debounceFn( 'resize-' + tableId, function () { treeTable.resize(tableId); }, 0 )(); if (callbackFlag && trData[LAY_ASYNC_STATUS] !== 'loading') { var onExpand = treeOptions.callback.onExpand; layui.type(onExpand) === 'function' && onExpand(tableId, trData, trExpand); } if ( layui.type(done) === 'function' && trData[LAY_ASYNC_STATUS] !== 'loading' ) { done(tableId, trData, trExpand); } return retValue; }; /** * 展开或关闭一个节点 * @param {String} id 树表id * @param {Object} opts * @param {Number|String} opts.index 展开行的数据下标 * @param {Boolean} [opts.expandFlag] 展开、关闭、切换 * @param {Boolean} [opts.inherit] 是否级联子节点 * @param {Boolean} [opts.callbackFlag] 是否触发 tree.callback 事件 * @param {Boolean} [opts.done] 节点操作完成后的回调函数 * @return [{Boolean}] 状态结果 * */ treeTable.expandNode = function (id, opts) { var that = getThisTable(id); if (!that) return; opts = opts || {}; var index = opts.index; var expandFlag = opts.expandFlag; var sonSign = opts.inherit; var callbackFlag = opts.callbackFlag; var options = that.getOptions(); var tableViewElem = options.elem.next(); return expandNode( { trElem: tableViewElem.find('tr[lay-data-index="' + index + '"]').first() }, expandFlag, sonSign, null, callbackFlag, opts.done ); }; /** * 展开或关闭全部节点 * @param {String} id 树表id * @param {Boolean} expandFlag 展开或关闭 * */ treeTable.expandAll = function (id, expandFlag) { if (layui.type(expandFlag) !== 'boolean') { return hint.error( 'treeTable.expandAll param "expandFlag" must be a boolean value.' ); } var that = getThisTable(id); if (!that) return; that.isExpandAll = expandFlag; var options = that.getOptions(); var treeOptions = options.tree; var tableView = options.elem.next(); var isParentKey = treeOptions.customName.isParent; var idKey = treeOptions.customName.id; var showFlexIconIfNotParent = treeOptions.view.showFlexIconIfNotParent; if (!expandFlag) { // 关闭所有 // 将所有已经打开的节点的状态设置为关闭, that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = false; d[idKey] !== undefined && (that.status.expand[d[idKey]] = false); } }); // 只处理当前页,如果需要处理全部表格,需要用treeTable.updateStatus // 隐藏所有非顶层的节点 tableView .find('.layui-table-box tbody tr[data-level!="0"]') .addClass(HIDE); tableView .find('.layui-table-tree-flexIcon') .html(treeOptions.view.flexIconClose); treeOptions.view.showIcon && tableView .find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)' ) .html(treeOptions.view.iconClose); } else { var tableDataFlat = treeTable.getData(id, true); // 展开所有 // 存在异步加载 if (treeOptions.async.enable) { // 判断是否有未加载过的节点 var isAllAsyncDone = true; layui.each(tableDataFlat, function (i1, item1) { if (item1[isParentKey] && !item1[LAY_ASYNC_STATUS]) { isAllAsyncDone = false; return true; } }); // 有未加载过的节点 if (!isAllAsyncDone) { // 逐个展开 layui.each(treeTable.getData(id), function (i1, item1) { treeTable.expandNode(id, { index: item1[LAY_DATA_INDEX], expandFlag: true, inherit: true }); }); return; } } // 先判断是否全部打开过了 var isAllExpanded = true; layui.each(tableDataFlat, function (i1, item1) { if (item1[isParentKey] && !item1[LAY_HAS_EXPANDED]) { isAllExpanded = false; return true; } }); // 如果全部节点已经都打开过,就可以简单处理跟隐藏所有节点反操作 if (isAllExpanded) { that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = true; d[idKey] !== undefined && (that.status.expand[d[idKey]] = true); } }); // 显示所有子节点 tableView.find('tbody tr[data-level!="0"]').removeClass(HIDE); // 处理节点的图标 tableView .find('.layui-table-tree-flexIcon') .html(treeOptions.view.flexIconOpen); treeOptions.view.showIcon && tableView .find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)' ) .html(treeOptions.view.iconOpen); } else { // 如果有未打开过的父节点,将 tr 内容全部重新生成 that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = true; d[LAY_HAS_EXPANDED] = true; d[idKey] !== undefined && (that.status.expand[d[idKey]] = true); } }); if (options.initSort && options.initSort.type && options.autoSort) { return treeTable.sort(id); } var trAll = table.getTrHtml(id, tableDataFlat); var trAllObj = { trs: $(trAll.trs.join('')), trs_fixed: $(trAll.trs_fixed.join('')), trs_fixed_r: $(trAll.trs_fixed_r.join('')) }; var props; layui.each(tableDataFlat, function (dataIndex, dataItem) { var dataLevel = dataItem[LAY_DATA_INDEX].split('-').length - 1; props = { 'data-index': dataItem[LAY_DATA_INDEX], 'lay-data-index': dataItem[LAY_DATA_INDEX], 'data-level': dataLevel }; trAllObj.trs .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); trAllObj.trs_fixed .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); trAllObj.trs_fixed_r .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); }); layui.each(['main', 'fixed-l', 'fixed-r'], function (i, item) { tableView .find('.layui-table-' + item + ' tbody') .html(trAllObj[['trs', 'trs_fixed', 'trs_fixed_r'][i]]); }); that.renderTreeTable(tableView, 0, false); } } treeTable.resize(id); }; /** * @typedef updateNodeIconOptions * @prop {JQuery} scopeEl - tr 元素 * @prop {boolean} isExpand - 是否是展开图标 * @prop {boolean} isParent - 是否是父节点图标 */ /** * 更新节点图标 * @param {updateNodeIconOptions} opts */ Class.prototype.updateNodeIcon = function (opts) { var that = this; var options = that.getOptions(); var treeOptions = options.tree || {}; var scopeEl = opts.scopeEl; var isExpand = opts.isExpand; var isParent = opts.isParent; // 处理折叠按钮图标 var flexIconElem = scopeEl.find('.layui-table-tree-flexIcon'); flexIconElem .css( 'visibility', isParent || treeOptions.view.showFlexIconIfNotParent ? 'visible' : 'hidden' ) .html( isExpand ? treeOptions.view.flexIconOpen : treeOptions.view.flexIconClose ); // 处理节点图标 if (treeOptions.view.showIcon) { var nodeIconElem = scopeEl.find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom)' ); var nodeIcon = isParent ? isExpand ? treeOptions.view.iconOpen : treeOptions.view.iconClose : treeOptions.view.iconLeaf; nodeIconElem .toggleClass('layui-table-tree-iconLeaf', !isParent) .html(nodeIcon); } }; Class.prototype.renderTreeTable = function (tableView, level, sonSign) { var that = this; var options = that.getOptions(); var tableViewElem = options.elem.next(); !tableViewElem.hasClass(TABLE_TREE) && tableViewElem.addClass(TABLE_TREE); var tableId = options.id; var treeOptions = options.tree || {}; // var treeOptionsData = treeOptions.data || {}; var treeOptionsView = treeOptions.view || {}; var customName = treeOptions.customName || {}; var isParentKey = customName.isParent; // var tableFilterId = tableViewElem.attr('lay-filter'); var treeTableThat = that; var existsData = options.data.length; // 是否直接赋值 data // var tableData = treeTableThat.getTableData(); level = level || 0; if (!level) { // 初始化的表格里面没有level信息,可以作为顶层节点的判断 tableViewElem .find('.layui-table-body tr:not([data-level])') .attr('data-level', level); layui.each(table.cache[tableId], function (dataIndex, dataItem) { // fix: 修正直接赋值 data 时顶层节点 LAY_DATA_INDEX 值的异常问题 if (existsData) { dataItem[LAY_DATA_INDEX] = String(dataIndex); } var layDataIndex = dataItem[LAY_DATA_INDEX]; tableViewElem .find( '.layui-table-main tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); tableViewElem .find( '.layui-table-fixed-l tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); tableViewElem .find( '.layui-table-fixed-r tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); }); } var dataExpand = null; // 记录需要展开的数据 var nameKey = customName.name; var indent = treeOptionsView.indent || 14; layui.each( tableView.find('td[data-field="' + nameKey + '"]'), function (index, item) { item = $(item); var trElem = item.closest('tr'); var itemCell = item.children('.layui-table-cell'); if (itemCell.hasClass('layui-table-tree-item')) { return; } var trIndex = trElem.attr('lay-data-index'); if (!trIndex) { // 排除在统计行中的节点 return; } trElem = tableViewElem.find('tr[lay-data-index="' + trIndex + '"]'); var trData = treeTableThat.getNodeDataByIndex(trIndex); if (trData[LAY_EXPAND] && trData[isParentKey]) { // 需要展开 dataExpand = dataExpand || {}; dataExpand[trIndex] = true; } if (trData[LAY_CHECKBOX_HALF]) { trElem .find('input[type="checkbox"][name="layTableCheckbox"]') .prop('indeterminate', true); } var htmlTemp = itemCell.html(); itemCell = trElem.find( 'td[data-field="' + nameKey + '"]>div.layui-table-cell' ); itemCell.addClass('layui-table-tree-item'); var flexIconElem = itemCell .html( [ '
', trData[LAY_EXPAND] ? treeOptionsView.flexIconOpen : treeOptionsView.flexIconClose, // 折叠图标 '
', treeOptionsView.showIcon ? '
' + (that.normalizedIcon(trData[customName.icon]) || treeOptionsView.icon || (trData[isParentKey] ? trData[LAY_EXPAND] ? treeOptionsView.iconOpen : treeOptionsView.iconClose : treeOptionsView.iconLeaf) || '') + '
' : '', // 区分父子节点 htmlTemp ].join('') ) // 图标要可定制 .find('.layui-table-tree-flexIcon'); // 添加展开按钮的事件 flexIconElem.on('click', function (event) { layui.stope(event); // 处理数据 // var trElem = item.closest('tr'); expandNode({ trElem: trElem }, null, null, null, true); }); } ); if ( !level && treeOptions.view.expandAllDefault && that.isExpandAll === undefined ) { return treeTable.expandAll(tableId, true); // 默认展开全部 } // 当前层的数据看看是否需要展开 if (sonSign !== false && dataExpand) { layui.each(dataExpand, function (index, item) { var trDefaultExpand = tableViewElem.find( 'tr[lay-data-index="' + index + '"]' ); trDefaultExpand .find('.layui-table-tree-flexIcon') .html(treeOptionsView.flexIconOpen); expandNode({ trElem: trDefaultExpand.first() }, true); }); // #1463 expandNode 中已经展开过的节点不会重新渲染 debounceFn( 'renderTreeTable2-' + tableId, function () { form.render($('.layui-table-tree[' + MOD_ID + '="' + tableId + '"]')); }, 0 )(); } else { debounceFn( 'renderTreeTable-' + tableId, function () { options.hasNumberCol && formatNumber(that); form.render($('.layui-table-tree[' + MOD_ID + '="' + tableId + '"]')); }, 0 )(); } }; var formatNumber = function (that) { var options = that.getOptions(); var tableViewElem = options.elem.next(); var num = 0; var trMain = tableViewElem.find('.layui-table-main tbody tr'); var trFixedL = tableViewElem.find('.layui-table-fixed-l tbody tr'); var trFixedR = tableViewElem.find('.layui-table-fixed-r tbody tr'); layui.each(that.treeToFlat(table.cache[options.id]), function (i1, item1) { if (item1['LAY_HIDE']) return; var itemData = that.getNodeDataByIndex(item1[LAY_DATA_INDEX]); itemData['LAY_NUM'] = ++num; trMain.eq(i1).find('.laytable-cell-numbers').html(num); trFixedL.eq(i1).find('.laytable-cell-numbers').html(num); trFixedR.eq(i1).find('.laytable-cell-numbers').html(num); }); }; // 树表渲染 Class.prototype.render = function (type) { var that = this; that.tableIns = table[type === 'reloadData' ? 'reloadData' : 'reload']( that.tableIns.config.id, $.extend(true, {}, that.config) ); that.config = that.tableIns.config; }; // 表格重载 Class.prototype.reload = function (options, deep, type) { var that = this; options = options || {}; delete that.haveInit; // 防止数组深度合并 layui.each(options, function (key, item) { if (layui.type(item) === 'array') delete that.config[key]; }); // 根据需要处理options中的一些参数 updateOptions(that.getOptions().id, options, type || true); // 对参数进行深度或浅扩展 that.config = $.extend(deep, {}, that.config, options); // 执行渲染 that.render(type); }; // 仅重载数据 treeTable.reloadData = function () { var args = $.extend(true, [], arguments); args[3] = 'reloadData'; return treeTable.reload.apply(null, args); }; var updateStatus = function (data, statusObj, childrenKey, notCascade) { var dataUpdated = []; layui.each(data, function (i1, item1) { if (layui.type(statusObj) === 'function') { statusObj(item1); } else { $.extend(item1, statusObj); } dataUpdated.push($.extend({}, item1)); notCascade || (dataUpdated = dataUpdated.concat( updateStatus(item1[childrenKey], statusObj, childrenKey, notCascade) )); }); return dataUpdated; }; Class.prototype.updateStatus = function (data, statusObj, notCascade) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; data = data || table.cache[options.id]; return updateStatus( data, statusObj, treeOptions.customName.children, notCascade ); }; Class.prototype.getTableData = function () { var that = this; var options = that.getOptions(); // return options.url ? table.cache[options.id] : options.data; return table.cache[options.id]; }; treeTable.updateStatus = function (id, statusObj, data) { var that = getThisTable(id); var options = that.getOptions(); if (!data) { if (options.url) { data = table.cache[options.id]; } else { data = options.data; } } return that.updateStatus(data, statusObj); }; treeTable.sort = function (id) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var treeOptions = options.tree; var tableData = treeTable.getData(id); var customName = treeOptions.customName; var childrenKey = customName.children; // 只和同级节点排序 var sort = function (data, field, type) { layui.sort(data, field, type, true); layui.each(data, function (rowIndex, trData) { sort(trData[childrenKey] || [], field, type); }); }; if (options.autoSort) { var initSort = options.initSort; if (initSort.type) { sort(tableData, initSort.field, initSort.type === 'desc'); } else { // 恢复默认 sort(tableData, table.config.indexName, null); } // 更新缓存中数据的顺序 table.cache[id] = tableData; // 重新初始化缓存数据 that.initData(tableData); treeTable.renderData(id); } }; // 处理事件 var updateObjParams = function (obj) { var tableId = obj.config.id; var tableThat = getThisTable(tableId); var trData = (obj.data = treeTable.getNodeDataByIndex(tableId, obj.index)); // 克隆的 var trIndex = trData[LAY_DATA_INDEX]; obj.dataIndex = trIndex; // 处理update方法 var updateFn = obj.update; obj.update = function () { var updateThat = this; var args = arguments; $.extend(tableThat.getNodeDataByIndex(trIndex), args[0]); var ret = updateFn.apply(updateThat, args); // 主要负责更新节点内容 var nameKey = obj.config.tree.customName.name; nameKey in args[0] && obj.tr .find('td[data-field="' + nameKey + '"]') .children('div.layui-table-cell') .removeClass('layui-table-tree-item'); tableThat.renderTreeTable(obj.tr, obj.tr.attr('data-level'), false); return ret; }; // 处理del方法 obj.del = function () { treeTable.removeNode(tableId, trData); }; // 处理setRowChecked obj.setRowChecked = function (opts) { opts = layui.type(opts) === 'object' ? opts : { checked: opts }; treeTable.setRowChecked( tableId, $.extend( { index: trData }, opts ) ); }; }; // 更新数据 treeTable.updateNode = function (id, index, newNode) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); // var treeOptions = options.tree; var tableView = options.elem.next(); var trElem = tableView.find('tr[lay-data-index="' + index + '"]'); var trIndex = trElem.attr('data-index'); var trLevel = trElem.attr('data-level'); if (!newNode) { return; } // 更新值 var newNodeTemp = that.getNodeDataByIndex(index, false, newNode); // 获取新的tr替换 var trNew = table.getTrHtml(id, [newNodeTemp]); // 重新渲染tr layui.each(['main', 'fixed-l', 'fixed-r'], function (i, item) { tableView .find( '.layui-table-' + item + ' tbody tr[lay-data-index="' + index + '"]' ) .replaceWith( $(trNew[['trs', 'trs_fixed', 'trs_fixed_r'][i]].join('')) .attr({ 'data-index': trIndex, 'lay-data-index': index, 'data-level': trLevel }) .data('index', trIndex) ); }); that.renderTreeTable( tableView.find('tr[lay-data-index="' + index + '"]'), trLevel ); }; // 删除数据 // _keepParent 暂时为私有参数,仅供内部使用 treeTable.removeNode = function (id, node, _keepParent) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var treeOptions = options.tree; var isParentKey = treeOptions.customName.isParent; var childrenKey = treeOptions.customName.children; var tableView = options.elem.next(); var delNode; var indexArr = []; var tableCache = table.cache[id]; delNode = that.getNodeDataByIndex( layui.type(node) === 'string' ? node : node[LAY_DATA_INDEX], false, 'delete' ); var nodeP = that.getNodeDataByIndex(delNode[LAY_PARENT_INDEX]); that.updateCheckStatus(nodeP); var delNodesFlat = that.treeToFlat( [delNode], delNode[treeOptions.customName.pid], delNode[LAY_PARENT_INDEX] ); layui.each(delNodesFlat, function (i2, delNode) { var delNodeDataIndex = delNode[LAY_DATA_INDEX]; indexArr.push('tr[lay-data-index="' + delNodeDataIndex + '"]'); // 删除临时 key if (delNodeDataIndex.indexOf('-') !== -1) { delete tableCache[delNodeDataIndex]; } }); tableView.find(indexArr.join(',')).remove(); // 删除行 var deleteCacheKey = function () { for (var key in tableCache) { // 根节点 getNodeDataByIndex 内部已处理 if (key.indexOf('-') !== -1) { // L93 updateCache() 中,cacheKey 取自 rowData 中的 LAY_DATA_INDEX, // 两者不同说明当前 cacheKey 引用的 rowData 已被更新 if (key !== tableCache[key][LAY_DATA_INDEX]) { delete tableCache[key]; } } } }; // 重新整理数据 var tableData = that.initData(); deleteCacheKey(); // index发生变化需要更新页面tr中对应的lay-data-index 新增和删除都要注意数据结构变动之后的index问题 layui.each(that.treeToFlat(tableData), function (i3, item3) { if ( item3[LAY_DATA_INDEX_HISTORY] && item3[LAY_DATA_INDEX_HISTORY] !== item3[LAY_DATA_INDEX] ) { tableView .find('tr[lay-data-index="' + item3[LAY_DATA_INDEX_HISTORY] + '"]') .attr({ 'data-index': item3[LAY_DATA_INDEX], 'lay-data-index': item3[LAY_DATA_INDEX] }) .data('index', item3[LAY_DATA_INDEX]); // item3[LAY_DATA_INDEX_HISTORY] = item3[LAY_DATA_INDEX] } }); // 重新更新顶层节点的data-index; layui.each(tableCache, function (i4, item4) { tableView .find( 'tr[data-level="0"][lay-data-index="' + item4[LAY_DATA_INDEX] + '"]' ) .attr('data-index', i4) .data('index', i4); }); options.hasNumberCol && formatNumber(that); // 更新父节点状态 if (nodeP) { var trEl = tableView.find( 'tr[lay-data-index="' + nodeP[LAY_DATA_INDEX] + '"]' ); if (!_keepParent) { nodeP[isParentKey] = !!( nodeP[childrenKey] && nodeP[childrenKey].length ); } that.updateNodeIcon({ scopeEl: trEl, isExpand: nodeP[LAY_EXPAND], isParent: nodeP[isParentKey] }); } // 重新适配尺寸 treeTable.resize(id); }; /** * 新增数据节点 * @param {String} id 树表id * @param {Object} opts * @param {String|Number} opts.parentIndex 指定的父节点,如果增加根节点,请设置 parentIndex 为 null 即可 * @param {Number} opts.index 新节点插入的位置(从 0 开始)index = -1(默认) 时,插入到最后 * @param {Object|Array} opts.data 新增的节点,单个或者多个 * @param {Boolean} opts.focus 新增的节点,单个或者多个 * @return {Array} 新增的节点 * */ treeTable.addNodes = function (id, opts) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var treeOptions = options.tree; var tableViewElem = options.elem.next(); var checkName = table.config.checkName; opts = opts || {}; var parentIndex = opts.parentIndex; var index = opts.index; var newNodes = opts.data; var focus = opts.focus; parentIndex = layui.type(parentIndex) === 'number' ? parentIndex.toString() : parentIndex; var parentNode = parentIndex ? that.getNodeDataByIndex(parentIndex) : null; index = layui.type(index) === 'number' ? index : -1; // 添加数据 newNodes = $.extend( true, [], layui.isArray(newNodes) ? newNodes : [newNodes] ); // 若未传入 LAY_CHECKED 属性,则继承父节点的 checked 状态 layui.each(newNodes, function (i, item) { if (!(checkName in item) && parentNode) { item[checkName] = parentNode[checkName]; } }); // var tableData = that.getTableData(); var dataAfter; if (!parentNode) { // 添加到根节点 dataAfter = table.cache[id].splice( index === -1 ? table.cache[id].length : index ); table.cache[id] = table.cache[id].concat(newNodes, dataAfter); if (!options.url) { // 静态data模式 if (!options.page) { options.data = table.cache[id]; } else { var pageOptions = options.page; options.data.splice.apply( options.data, [ pageOptions.limit * (pageOptions.curr - 1), pageOptions.limit ].concat(table.cache[id]) ); } } // 将新节点添加到页面 // tableData = that.initData(); if (tableViewElem.find('.layui-none').length) { table.renderData(id); return newNodes; } var newNodesHtml = table.getTrHtml(id, newNodes); var newNodesHtmlObj = { trs: $(newNodesHtml.trs.join('')), trs_fixed: $(newNodesHtml.trs_fixed.join('')), trs_fixed_r: $(newNodesHtml.trs_fixed_r.join('')) }; var attrs = {}; layui.each(newNodes, function (newNodeIndex, newNodeItem) { attrs = { 'data-index': newNodeItem[LAY_DATA_INDEX], 'lay-data-index': newNodeItem[LAY_DATA_INDEX], 'data-level': '0' }; newNodesHtmlObj.trs .eq(newNodeIndex) .attr(attrs) .data('index', newNodeItem[LAY_DATA_INDEX]); newNodesHtmlObj.trs_fixed .eq(newNodeIndex) .attr(attrs) .data('index', newNodeItem[LAY_DATA_INDEX]); newNodesHtmlObj.trs_fixed_r .eq(newNodeIndex) .attr(attrs) .data('index', newNodeItem[LAY_DATA_INDEX]); }); var trIndexPrev = parseInt(newNodes[0][LAY_DATA_INDEX]) - 1; var tableViewElemMAIN = tableViewElem.find(ELEM_MAIN); var tableViewElemFIXL = tableViewElem.find(ELEM_FIXL); var tableViewElemFIXR = tableViewElem.find(ELEM_FIXR); if (trIndexPrev === -1) { // 插入到开头 var hasTr = tableViewElemMAIN.find( 'tr[data-level="0"][data-index="0"]' )[0]; if (hasTr) { tableViewElemMAIN .find('tr[data-level="0"][data-index="0"]') .before(newNodesHtmlObj.trs); tableViewElemFIXL .find('tr[data-level="0"][data-index="0"]') .before(newNodesHtmlObj.trs_fixed); tableViewElemFIXR .find('tr[data-level="0"][data-index="0"]') .before(newNodesHtmlObj.trs_fixed_r); } else { tableViewElemMAIN.find('tbody').prepend(newNodesHtmlObj.trs); tableViewElemFIXL.find('tbody').prepend(newNodesHtmlObj.trs_fixed); tableViewElemFIXR.find('tbody').prepend(newNodesHtmlObj.trs_fixed_r); } } else { if (index === -1) { // 追加到最后 tableViewElemMAIN.find('tbody').append(newNodesHtmlObj.trs); tableViewElemFIXL.find('tbody').append(newNodesHtmlObj.trs_fixed); tableViewElemFIXR.find('tbody').append(newNodesHtmlObj.trs_fixed_r); } else { var trIndexNext = dataAfter[0][LAY_DATA_INDEX_HISTORY]; tableViewElemMAIN .find('tr[data-level="0"][data-index="' + trIndexNext + '"]') .before(newNodesHtmlObj.trs); tableViewElemFIXL .find('tr[data-level="0"][data-index="' + trIndexNext + '"]') .before(newNodesHtmlObj.trs_fixed); tableViewElemFIXR .find('tr[data-level="0"][data-index="' + trIndexNext + '"]') .before(newNodesHtmlObj.trs_fixed_r); } } // 重新更新顶层节点的data-index; layui.each(table.cache[id], function (i4, item4) { tableViewElem .find( 'tr[data-level="0"][lay-data-index="' + item4[LAY_DATA_INDEX] + '"]' ) .attr('data-index', i4) .data('index', i4); }); that.renderTreeTable( tableViewElem.find( newNodes .map(function (value, index, array) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) ); } else { var isParentKey = treeOptions.customName.isParent; var childKey = treeOptions.customName.children; parentNode[isParentKey] = true; var childrenNodes = parentNode[childKey]; if (!childrenNodes) { childrenNodes = parentNode[childKey] = newNodes; } else { dataAfter = childrenNodes.splice( index === -1 ? childrenNodes.length : index ); childrenNodes = parentNode[childKey] = childrenNodes.concat( newNodes, dataAfter ); } // 删除已经存在的同级节点以及他们的子节点,并且把中间节点的已展开过的状态设置为false that.updateStatus(childrenNodes, function (d) { if (d[isParentKey] || treeOptions.view.showFlexIconIfNotParent) { d[LAY_HAS_EXPANDED] = false; } }); var childrenNodesFlat = that.treeToFlat(childrenNodes); tableViewElem .find( childrenNodesFlat .map(function (value) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .remove(); // tableData = that.initData(); // 去掉父节点的已经展开过的状态,重新执行一次展开的方法 parentNode[LAY_HAS_EXPANDED] = false; parentNode[LAY_ASYNC_STATUS] = 'local'; // 转为本地数据,应该规定异步加载子节点的时候addNodes的规则 expandNode( { trElem: tableViewElem.find('tr[lay-data-index="' + parentIndex + '"]') }, true ); } that.updateCheckStatus(parentNode); // 更新父节点图标状态 if (parentNode) { var trEl = tableViewElem.find( 'tr[lay-data-index="' + parentNode[LAY_DATA_INDEX] + '"]' ); that.updateNodeIcon({ scopeEl: trEl, isExpand: parentNode[LAY_EXPAND], isParent: parentNode[isParentKey] }); } treeTable.resize(id); if (focus) { // 滚动到第一个新增的节点 tableViewElem .find(ELEM_MAIN) .find('tr[lay-data-index="' + newNodes[0][LAY_DATA_INDEX] + '"]') .get(0) .scrollIntoViewIfNeeded(); } return newNodes; }; // 获取表格选中状态 treeTable.checkStatus = function (id, includeHalfCheck) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var treeOptions = options.tree; var checkName = table.config.checkName; // 需要区分单双选 var tableData = treeTable.getData(id, true); var checkedData = tableData.filter(function (value, index, array) { return value[checkName] || (includeHalfCheck && value[LAY_CHECKBOX_HALF]); }); var isAll = true; layui.each( treeOptions.data.cascade === 'all' ? table.cache[id] : treeTable.getData(id, true), function (i1, item1) { if (!item1[checkName]) { isAll = false; return true; } } ); return { data: checkedData, isAll: isAll }; }; // 排序之后重新渲染成树表 treeTable.on('sort', function (obj) { var options = obj.config; var tableView = options.elem.next(); var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { treeTable.sort(tableId); } }); // 行点击 treeTable.on('row', function (obj) { var options = obj.config; var tableView = options.elem.next(); if (tableView.hasClass(TABLE_TREE)) { updateObjParams(obj); } }); // 行双击 treeTable.on('rowDouble', function (obj) { var options = obj.config; var tableView = options.elem.next(); // var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { updateObjParams(obj); var treeOptions = options.tree || {}; if (treeOptions.view.dblClickExpand) { expandNode({ trElem: obj.tr.first() }, null, null, null, true); } } }); // 菜单 treeTable.on('rowContextmenu', function (obj) { var options = obj.config; var tableView = options.elem.next(); // var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { updateObjParams(obj); } }); // tr中带lay-event节点点击 treeTable.on('tool', function (obj) { var options = obj.config; var tableView = options.elem.next(); // var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { updateObjParams(obj); } }); // 行内编辑 treeTable.on('edit', function (obj) { // 如果编辑涉及到关键的name字段需要重新更新一下tr节点 var options = obj.config; var tableView = options.elem.next(); // var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { updateObjParams(obj); if (obj.field === options.tree.customName.name) { var updateData = {}; updateData[obj.field] = obj.value; obj.update(updateData); // 通过update调用执行tr节点的更新 } } }); // 单选 treeTable.on('radio', function (obj) { var options = obj.config; var tableView = options.elem.next(); var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { var that = getThisTable(tableId); updateObjParams(obj); checkNode.call(that, obj.tr, obj.checked); } }); /** * 同步设置行选中样式(主体、左固定列、右固定列) * @param {jQuery} tr 行节点集合 * @param {boolean} checked 是否选中 */ Class.prototype.setRowCheckedClass = function (tr, checked) { var that = this; var options = that.getOptions(); var tableViewElem = options.elem.next(); var method = checked ? 'addClass' : 'removeClass'; tr[method](ELEM_CHECKED); // 主体行 // 固定列行(左、右) tr.each(function () { var index = $(this).data('index'); var fixedTr = tableViewElem.find( '.layui-table-fixed-l tbody tr[data-index="' + index + '"], .layui-table-fixed-r tbody tr[data-index="' + index + '"]' ); fixedTr[method](ELEM_CHECKED); }); }; // 更新表格的复选框状态 Class.prototype.updateCheckStatus = function (dataP, checked) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var tableView = options.elem.next(); var checkName = table.config.checkName; var cascade = treeOptions.data.cascade; var isCascadeParent = cascade === 'all' || cascade === 'parent'; // 如有必要更新父节点们的状态 if (isCascadeParent && dataP) { var trsP = that.updateParentCheckStatus( dataP, layui.type(checked) === 'boolean' ? checked : null ); layui.each(trsP, function (indexP, itemP) { var checkboxElem = tableView.find( 'tr[lay-data-index="' + itemP[LAY_DATA_INDEX] + '"] input[name="layTableCheckbox"]:not(:disabled)' ); var trElem = checkboxElem.length ? checkboxElem.closest('tr') : tableView.find( 'tr[lay-data-index="' + itemP[LAY_DATA_INDEX] + '"]' ); var checked = itemP[checkName]; // 标记父节点行背景色 that.setRowCheckedClass(trElem, checked); // 设置原始复选框 checked 属性值并渲染 checkboxElem.prop({ checked: checked, indeterminate: itemP[LAY_CHECKBOX_HALF] }); }); } // 更新全选的状态 var isAll = true; var isIndeterminate = false; var data = treeOptions.data.cascade === 'all' ? table.cache[tableId] : treeTable.getData(tableId, true); data = data.filter(function (item) { return !item[options.disabledName]; }); if (data.length > 0) { layui.each(data, function (i1, item1) { if (item1[checkName] || item1[LAY_CHECKBOX_HALF]) { isIndeterminate = true; } if (!item1[checkName]) { isAll = false; } if (isIndeterminate && !isAll) { return true; } }); } else { isAll = false; } isIndeterminate = isIndeterminate && !isAll; tableView .find('input[name="layTableCheckbox"][lay-filter="layTableAllChoose"]') .prop({ checked: isAll, indeterminate: isIndeterminate }); return isAll; }; // 更新父节点的选中状态 Class.prototype.updateParentCheckStatus = function (dataP, checked) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var checkName = table.config.checkName; var childrenKey = treeOptions.customName.children; var dataRet = []; dataP[LAY_CHECKBOX_HALF] = false; // 先设置为非半选,是否为半选又下面逻辑判断 if (checked === true) { // 为真需要判断子节点的情况 if (!dataP[childrenKey].length) { checked = false; } else { layui.each(dataP[childrenKey], function (index, item) { if (!item[checkName]) { // 只要有一个子节点为false checked = false; dataP[LAY_CHECKBOX_HALF] = true; return true; // 跳出循环 } }); } } else if (checked === false) { // 判断是否为半选 layui.each(dataP[childrenKey], function (index, item) { if (item[checkName] || item[LAY_CHECKBOX_HALF]) { // 只要有一个子节点为选中或者半选状态 dataP[LAY_CHECKBOX_HALF] = true; return true; } }); } else { // 状态不确定的情况下根据子节点的信息 checked = false; var checkedNum = 0; layui.each(dataP[childrenKey], function (index, item) { if (item[checkName]) { checkedNum++; } }); checked = dataP[childrenKey].length ? dataP[childrenKey].length === checkedNum : dataP[checkName]; // 如果没有子节点保留原来的状态; dataP[LAY_CHECKBOX_HALF] = checked ? false : checkedNum > 0; } dataP[checkName] = checked; dataRet.push($.extend({}, dataP)); if (dataP[LAY_PARENT_INDEX]) { dataRet = dataRet.concat( that.updateParentCheckStatus( table.cache[tableId][dataP[LAY_PARENT_INDEX]], checked ) ); } return dataRet; }; var checkNode = function (trElem, checked, callbackFlag, type) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var tableView = options.elem.next(); var inputElem = (trElem.length ? trElem : tableView) .find('.laytable-cell-radio, .laytable-cell-checkbox') .children('input') .last(); // 判断是单选还是多选,不应该同时存在 radio 列和 checkbox 列 // 当未配置选择列时,允许通过 type 参数显式指定模式以兼容 table 的写法 var isRadio = type ? type === 'radio' : inputElem.attr('type') === 'radio'; checked = isRadio && layui.type(checked) !== 'boolean' ? true : checked; if (callbackFlag) { var triggerEvent = function () { var fn = function (event) { layui.stope(event); }; inputElem.parent().on('click', fn); // 添加临时的阻止冒泡事件 inputElem.next().click(); inputElem.parent().off('click', fn); }; // 如果需要触发事件可以简单的触发对应节点的click事件 if (isRadio) { // 单选只能选中或者切换其他的不能取消选中 后续看是否有支持的必要 todo if (checked && !inputElem.prop('checked')) { triggerEvent(); } } else { if (layui.type(checked) === 'boolean') { if (inputElem.prop('checked') !== checked) { // 如果当前已经是想要修改的状态则不做处理 triggerEvent(); } } else { // 切换 triggerEvent(); } } } else { var trData = that.getNodeDataByIndex(trElem.attr('data-index')); var checkName = table.config.checkName; // 如果不触发事件应该有一个方法可以更新数据以及页面的节点 if (isRadio) { if (!trData) { // 单选必须是一个存在的行 return; } var statusChecked = {}; statusChecked[checkName] = false; // that.updateStatus(null, statusChecked); // 取消其他的选中状态 that.updateStatus(null, function (d) { if (d[checkName]) { var radioElem = tableView.find( 'tr[lay-data-index="' + d[LAY_DATA_INDEX] + '"] input[type="radio"][lay-type="layTableRadio"]' ); d[checkName] = false; // 取消当前选中行背景色 that.setRowCheckedClass(radioElem.closest('tr'), false); radioElem.prop('checked', false); } }); // 取消其他的选中状态 trData[checkName] = checked; that.setRowCheckedClass(trElem, checked); // 标记当前选中行背景色 that.setRowCheckedClass(trElem.siblings(), false); // 取消其他行背景色 trElem .find('input[type="radio"][lay-type="layTableRadio"]') .prop('checked', checked); } else { // 切换只能用到单条,全选到这一步的时候应该是一个确定的状态 checked = layui.type(checked) === 'boolean' ? checked : !trData[checkName]; // 状态切换,如果遇到不可操作的节点待处理 todo // 全选或者是一个父节点,将子节点的状态同步为当前节点的状态 // 处理不可操作的信息 var checkedStatusFn = function (d) { if (!d[table.config.disabledName]) { // 节点不可操作的不处理 d[checkName] = checked; d[LAY_CHECKBOX_HALF] = false; } }; var trs = that.updateStatus( trData ? [trData] : table.cache[tableId], checkedStatusFn, trData && ['parent', 'none'].indexOf(treeOptions.data.cascade) !== -1 ); var checkboxElem = tableView.find( trs .map(function (value) { return ( 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"] input[name="layTableCheckbox"]:not(:disabled)' ); }) .join(',') ); var trsElem = checkboxElem.length ? checkboxElem.closest('tr') : tableView.find( trs .map(function (value) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ); that.setRowCheckedClass(trsElem, checked); // 标记当前选中行背景色 checkboxElem.prop({ checked: checked, indeterminate: false }); var trDataP; // 更新父节点以及更上层节点的状态 if (trData && trData[LAY_PARENT_INDEX]) { // 找到父节点,然后判断父节点的子节点是否全部选中 trDataP = that.getNodeDataByIndex(trData[LAY_PARENT_INDEX]); } return that.updateCheckStatus(trDataP, checked); } } }; // 多选 treeTable.on('checkbox', function (obj) { var options = obj.config; var tableView = options.elem.next(); var tableId = options.id; if (tableView.hasClass(TABLE_TREE)) { var that = getThisTable(tableId); var checked = obj.checked; updateObjParams(obj); obj.isAll = checkNode.call(that, obj.tr, checked); } }); /** * 设置行选中状态 * @param {String} id 树表id * @param {Object} opts * @param {Object|String} opts.index 节点下标 * @param {Boolean} opts.checked 选中或取消 * @param {String} [opts.type] 选中方式,支持 radio / checkbox * @param {Boolean} [opts.callbackFlag] 是否触发事件回调 * */ treeTable.setRowChecked = function (id, opts) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var tableView = options.elem.next(); opts = opts || {}; var node = opts.index; var checked = opts.checked; var callbackFlag = opts.callbackFlag; var type = opts.type; var dataIndex = layui.type(node) === 'string' ? node : node[LAY_DATA_INDEX]; // 判断是否在当前页面中 var nodeData = that.getNodeDataByIndex(dataIndex); if (!nodeData) { // 目前只能处理当前页的数据 return; } var collectNeedExpandNodeIndex = function (index) { needExpandIndex.push(index); var trElem = tableView.find('tr[lay-data-index="' + index + '"]'); if (!trElem.length) { var nodeData = that.getNodeDataByIndex(index); var parentIndex = nodeData[LAY_PARENT_INDEX]; parentIndex && collectNeedExpandNodeIndex(parentIndex); } }; // 判断是否展开过 var trElem = tableView.find('tr[lay-data-index="' + dataIndex + '"]'); if (!trElem.length) { var parentIndex = nodeData[LAY_PARENT_INDEX]; var needExpandIndex = []; collectNeedExpandNodeIndex(parentIndex); // 如果还没有展开没有渲染的要先渲染出来 layui.each(needExpandIndex.reverse(), function (index, nodeIndex) { treeTable.expandNode(id, { index: nodeIndex, expandFlag: true }); }); trElem = tableView.find('tr[lay-data-index="' + dataIndex + '"]'); } checkNode.call(that, trElem, checked, callbackFlag, type); }; treeTable.checkAllNodes = function (id, checked) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); var tableView = options.elem.next(); checkNode.call(that, tableView.find('tr[data-index="NONE"]'), !!checked); }; /** * 获得数据 * @param {String} id 表格id * @param {Boolean} [isSimpleData] 是否返回平铺结构的数据 * @return {Array} 表格数据 * */ treeTable.getData = function (id, isSimpleData) { var that = getThisTable(id); if (!that) return; var tableData = []; layui.each( $.extend(true, [], table.cache[id] || []), function (index, item) { // 遍历排除掉临时的数据 tableData.push(item); } ); return isSimpleData ? that.treeToFlat(tableData) : tableData; }; /** * 重新加载子节点 * @param {String} id 表格id * @param {String} dataIndex 父节点的dataIndex * */ treeTable.reloadAsyncNode = function (id, dataIndex) { var that = getThisTable(id); if (!that) { return; } var options = that.getOptions(); var treeOptions = options.tree; if (!treeOptions.async || !treeOptions.async.enable) { return; } var dataP = that.getNodeDataByIndex(dataIndex); if (!dataP) { return; } dataP[LAY_HAS_EXPANDED] = false; dataP[LAY_EXPAND] = false; dataP[LAY_ASYNC_STATUS] = false; layui.each( that.treeToFlat(dataP[treeOptions.customName.children]).reverse(), function (i1, item1) { treeTable.removeNode(id, item1[LAY_DATA_INDEX], true); } ); // 重新展开 treeTable.expandNode(id, { index: dataIndex, expandFlag: true, callbackFlag: true }); }; /** * 通过数据id获取节点对象 * */ treeTable.getNodeById = function (id, dataId) { var that = getThisTable(id); if (!that) return; return that.getNodeById(dataId); }; /** * 根据自定义规则搜索节点数据 * @param {String} id 树表id * @param {Function} filter 自定义过滤器函数 * @param {Object} [opts] * @param {Boolean} [opts.isSingle] 是否只找到第一个 * @param {Object} [opts.parentNode] 在指定在某个父节点下的子节点中搜索 * @return {Object} 节点对象 * */ treeTable.getNodesByFilter = function (id, filter, opts) { var that = getThisTable(id); if (!that) return; var options = that.getOptions(); opts = opts || {}; var isSingle = opts.isSingle; var parentNode = opts.parentNode; var dataP = parentNode && parentNode.data; // dataP = dataP || table.cache[id]; var nodes = that .treeToFlat( dataP ? dataP[options.tree.customName.children] || [] : table.cache[id] ) .filter(filter); var nodesResult = []; layui.each(nodes, function (i1, item1) { nodesResult.push(that.getNodeByIndex(item1[LAY_DATA_INDEX])); if (isSingle) { return true; } }); return nodesResult; }; // 记录所有实例 thisTreeTable.that = {}; // 记录所有实例对象 // thisTreeTable.config = {}; // 记录所有实例配置项 // 重载 treeTable.reload = function (id, options, deep, type) { // deep = deep !== false; // 默认采用深拷贝 var that = getThisTable(id); if (!that) return; that.reload(options, deep, type); return thisTreeTable.call(that); }; // 核心入口 treeTable.render = function (options) { var inst = new Class(options); return thisTreeTable.call(inst); }; exports(MOD_NAME, treeTable); });