UNPKG

vxe-table-plugin-virtual-tree

Version:

基于 vxe-table 表格的增强插件,实现简单的虚拟树表格

580 lines (570 loc) 19.7 kB
/* eslint-disable no-unused-vars */ import { CreateElement, VNodeChildren } from 'vue' import XEUtils from 'xe-utils/methods/xe-utils' import { VXETable } from 'vxe-table/lib/vxe-table' /* eslint-enable no-unused-vars */ function countTreeExpand ($xTree: any, prevRow: any): number { const rowChildren = prevRow[$xTree.treeOpts.children] let count = 1 if ($xTree.isTreeExpandByRow(prevRow)) { for (let index = 0; index < rowChildren.length; index++) { count += countTreeExpand($xTree, rowChildren[index]) } } return count } function getOffsetSize ($xTree: any): number { switch ($xTree.vSize) { case 'mini': return 3 case 'small': return 2 case 'medium': return 1 } return 0 } function calcTreeLine ($table: any, $xTree: any, matchObj: any): number { const { index, items } = matchObj let expandSize = 1 if (index) { expandSize = countTreeExpand($xTree, items[index - 1]) } return $table.rowHeight * expandSize - (index ? 1 : (12 - getOffsetSize($xTree))) } function registerComponent ({ Vue, Table, Grid, setup, t }: any) { const GlobalConfig = setup() const propKeys = Object.keys(Table.props).filter(name => ['data', 'treeConfig'].indexOf(name) === -1) const VirtualTree: any = { name: 'VxeVirtualTree', extends: Grid, data () { return { removeList: [] } }, crested () { if (this.keepSource || this.treeOpts.lazy) { console.error('[plugin-virtual-tree] Unsupported parameters.') } }, computed: { vSize (this: any) { return this.size || this.$parent.size || this.$parent.vSize }, treeOpts (this: any) { return Object.assign({ children: 'children', hasChild: 'hasChild', indent: 20 }, GlobalConfig.treeConfig, this.treeConfig) }, renderClass (this: any) { const { vSize } = this return ['vxe-grid vxe-virtual-tree', { [`size--${vSize}`]: vSize, 't--animat': this.animat, 'has--tree-line': this.treeConfig && this.treeOpts.line, 'is--maximize': this.isMaximized() }] }, tableExtendProps (this: any) { let rest: any = {} propKeys.forEach(key => { rest[key] = this[key] }) return rest } }, watch: { columns (this: any) { this.loadColumn(this.handleColumns()) }, data (this: any, value: any[]) { this.loadData(value) } }, created (this: any) { const { data } = this Object.assign(this, { fullTreeData: [], tableData: [], fullTreeRowMap: new Map() }) this.handleColumns() if (data) { this.reloadData(data) } }, methods: { getTableOns (this: any) { const { $listeners, proxyConfig, proxyOpts } = this const ons: { [key: string]: Function } = {} XEUtils.each($listeners, (cb, type) => { ons[type] = (...args: any[]) => { this.$emit(type, ...args) } }) ons['checkbox-all'] = this.checkboxAllEvent ons['checkbox-change'] = this.checkboxChangeEvent if (proxyConfig) { if (proxyOpts.sort) { ons['sort-change'] = this.sortChangeEvent } if (proxyOpts.filter) { ons['filter-change'] = this.filterChangeEvent } } return ons }, renderTreeLine (this: any, params: any, h: CreateElement) { const { treeConfig, treeOpts, fullTreeRowMap } = this const { $table, row, column } = params const { treeNode } = column if (treeNode && treeConfig && treeOpts.line) { const $xTree = this const rowLevel = row._X_LEVEL const matchObj = fullTreeRowMap.get(row) return [ treeNode && treeOpts.line ? h('div', { class: 'vxe-tree--line-wrapper' }, [ h('div', { class: 'vxe-tree--line', style: { height: `${calcTreeLine($table, $xTree, matchObj)}px`, left: `${rowLevel * (treeOpts.indent || 20) + (rowLevel ? 2 - getOffsetSize($xTree) : 0) + 16}px` } }) ]) : null ] } return [] }, renderTreeIcon (this: any, params: any, h: CreateElement, cellVNodes: VNodeChildren) { let { isHidden } = params let { row } = params let { children, indent, trigger, iconOpen, iconClose } = this.treeOpts let rowChildren = row[children] let isAceived = false let on: any = {} if (!isHidden) { isAceived = row._X_EXPAND } if (!trigger || trigger === 'default') { on.click = () => this.toggleTreeExpand(row) } return [ h('div', { class: ['vxe-cell--tree-node', { 'is--active': isAceived }], style: { paddingLeft: `${row._X_LEVEL * indent}px` } }, [ rowChildren && rowChildren.length ? [ h('div', { class: 'vxe-tree--btn-wrapper', on }, [ h('i', { class: ['vxe-tree--node-btn', isAceived ? (iconOpen || GlobalConfig.icon.TABLE_TREE_OPEN) : (iconClose || GlobalConfig.icon.TABLE_TREE_CLOSE)] }) ]) ] : null, h('div', { class: 'vxe-tree-cell' }, cellVNodes) ]) ] }, _loadTreeData (this: any, data: any) { const selectRow = this.getRadioRecord() return this.$nextTick() .then(() => this.$refs.xTable.loadData(data)) .then(() => { if (selectRow) { this.setRadioRow(selectRow) } }) }, loadData (data: any) { return this._loadTreeData(this.toVirtualTree(data)) }, reloadData (this: any, data: any) { return this.$nextTick() .then(() => this.$refs.xTable.reloadData(this.toVirtualTree(data))) .then(() => this.handleDefaultTreeExpand()) }, isTreeExpandByRow (row: any) { return !!row._X_EXPAND }, setTreeExpansion (rows: any, expanded: any) { return this.setTreeExpand(rows, expanded) }, setTreeExpand (this: any, rows: any, expanded: any) { if (rows) { if (!XEUtils.isArray(rows)) { rows = [rows] } rows.forEach((row: any) => this.virtualExpand(row, !!expanded)) } return this._loadTreeData(this.tableData) }, setAllTreeExpansion (expanded: any) { return this.setAllTreeExpand(expanded) }, setAllTreeExpand (expanded: any) { return this._loadTreeData(this.virtualAllExpand(expanded)) }, toggleTreeExpansion (row: any) { return this.toggleTreeExpand(row) }, toggleTreeExpand (row: any) { return this._loadTreeData(this.virtualExpand(row, !row._X_EXPAND)) }, getTreeExpandRecords (this: any) { const hasChilds = this.hasChilds const treeExpandRecords: any[] = [] XEUtils.eachTree(this.fullTreeData, row => { if (row._X_EXPAND && hasChilds(row)) { treeExpandRecords.push(row) } }, this.treeOpts) return treeExpandRecords }, clearTreeExpand () { return this.setAllTreeExpand(false) }, handleColumns (this: any) { return this.columns.map((conf: any) => { if (conf.treeNode) { let slots = conf.slots || {} slots.icon = this.renderTreeIcon slots.line = this.renderTreeLine conf.slots = slots } return conf }) }, hasChilds (this: any, row: any) { const childList = row[this.treeOpts.children] return childList && childList.length }, /** * 获取表格数据集,包含新增、删除、修改 */ getRecordset (this: any) { return { insertRecords: this.getInsertRecords(), removeRecords: this.getRemoveRecords(), updateRecords: this.getUpdateRecords() } }, isInsertByRow (row: any) { return !!row._X_INSERT }, getInsertRecords (this: any) { const insertRecords: any[] = [] XEUtils.eachTree(this.fullTreeData, row => { if (row._X_INSERT) { insertRecords.push(row) } }, this.treeOpts) return insertRecords }, insert (this: any, records: any) { return this.insertAt(records) }, insertAt (this: any, records: any, row: any) { const { fullTreeData, tableData, treeOpts } = this if (!XEUtils.isArray(records)) { records = [records] } let newRecords = records.map((record: any) => this.defineField(Object.assign({ _X_EXPAND: false, _X_INSERT: true, _X_LEVEL: 0 }, record))) if (!row) { fullTreeData.unshift.apply(fullTreeData, newRecords) tableData.unshift.apply(tableData, newRecords) } else { if (row === -1) { fullTreeData.push.apply(fullTreeData, newRecords) tableData.push.apply(tableData, newRecords) } else { let matchObj = XEUtils.findTree(fullTreeData, item => item === row, treeOpts) if (!matchObj || matchObj.index === -1) { throw new Error(t('vxe.error.unableInsert')) } let { items, index, nodes }: any = matchObj let rowIndex = tableData.indexOf(row) if (rowIndex > -1) { tableData.splice.apply(tableData, [rowIndex, 0].concat(newRecords)) } items.splice.apply(items, [index, 0].concat(newRecords)) newRecords.forEach((item: any) => { item._X_LEVEL = nodes.length - 1 }) } } return this._loadTreeData(tableData).then(() => { return { row: newRecords.length ? newRecords[newRecords.length - 1] : null, rows: newRecords } }) }, /** * 获取已删除的数据 */ getRemoveRecords (this: any) { return this.removeList }, removeSelecteds () { return this.removeCheckboxRow() }, /** * 删除选中数据 */ removeCheckboxRow (this: any) { return this.remove(this.getSelectRecords()).then((params: any) => { this.clearSelection() return params }) }, remove (this: any, rows: any[]) { const { removeList, fullTreeData, treeOpts } = this let rest: any[] = [] if (!rows) { rows = fullTreeData } else if (!XEUtils.isArray(rows)) { rows = [rows] } rows.forEach((row: any) => { let matchObj = XEUtils.findTree(fullTreeData, item => item === row, treeOpts) if (matchObj) { const { item, items, index, parent }: any = matchObj if (!this.isInsertByRow(row)) { removeList.push(row) } if (parent) { let isExpand = this.isTreeExpandByRow(parent) if (isExpand) { this.handleCollapsing(parent) } items.splice(index, 1) if (isExpand) { this.handleExpanding(parent) } } else { this.handleCollapsing(item) items.splice(index, 1) this.tableData.splice(this.tableData.indexOf(item), 1) } rest.push(item) } }) return this._loadTreeData(this.tableData).then(() => { return { row: rest.length ? rest[rest.length - 1] : null, rows: rest } }) }, /** * 处理默认展开树节点 */ handleDefaultTreeExpand (this: any) { let { treeConfig, treeOpts, tableFullData } = this if (treeConfig) { let { children, expandAll, expandRowKeys } = treeOpts if (expandAll) { this.setAllTreeExpand(true) } else if (expandRowKeys) { let rowkey = this.rowId expandRowKeys.forEach((rowid: any) => { let matchObj = XEUtils.findTree(tableFullData, item => rowid === XEUtils.get(item, rowkey), treeOpts) let rowChildren = matchObj ? matchObj.item[children] : 0 if (rowChildren && rowChildren.length) { this.setTreeExpand(matchObj.item, true) } }) } } }, /** * 定义树属性 */ toVirtualTree (this: any, treeData: any[]) { let fullTreeRowMap = this.fullTreeRowMap fullTreeRowMap.clear() XEUtils.eachTree(treeData, (item, index, items, paths, parent, nodes) => { item._X_EXPAND = false item._X_INSERT = false item._X_LEVEL = nodes.length - 1 fullTreeRowMap.set(item, { item, index, items, paths, parent, nodes }) }) this.fullTreeData = treeData.slice(0) this.tableData = treeData.slice(0) return treeData }, /** * 展开/收起树节点 */ virtualExpand (this: any, row: any, expanded: boolean) { if (row._X_EXPAND !== expanded) { if (row._X_EXPAND) { this.handleCollapsing(row) } else { this.handleExpanding(row) } } return this.tableData }, // 展开节点 handleExpanding (this: any, row: any) { if (this.hasChilds(row)) { const { tableData, treeOpts } = this let childRows = row[treeOpts.children] let expandList: any[] = [] let rowIndex = tableData.indexOf(row) if (rowIndex === -1) { throw new Error('错误的操作!') } XEUtils.eachTree(childRows, (item, index, obj, paths, parent, nodes) => { if (!parent || parent._X_EXPAND) { expandList.push(item) } }, treeOpts) row._X_EXPAND = true tableData.splice.apply(tableData, [rowIndex + 1, 0].concat(expandList)) } return this.tableData }, // 收起节点 handleCollapsing (this: any, row: any) { if (this.hasChilds(row)) { const { tableData, treeOpts } = this let childRows = row[treeOpts.children] let nodeChildList: any[] = [] XEUtils.eachTree(childRows, item => { nodeChildList.push(item) }, treeOpts) row._X_EXPAND = false this.tableData = tableData.filter((item: any) => nodeChildList.indexOf(item) === -1) } return this.tableData }, /** * 展开/收起所有树节点 */ virtualAllExpand (this: any, expanded: boolean) { const { treeOpts } = this if (expanded) { const tableList: any[] = [] XEUtils.eachTree(this.fullTreeData, row => { row._X_EXPAND = expanded tableList.push(row) }, treeOpts) this.tableData = tableList } else { XEUtils.eachTree(this.fullTreeData, row => { row._X_EXPAND = expanded }, treeOpts) this.tableData = this.fullTreeData.slice(0) } return this.tableData }, checkboxAllEvent (this: any, params: any) { const { checkboxConfig = {}, treeOpts } = this const { checkField, halfField, checkStrictly } = checkboxConfig const { checked } = params if (checkField && !checkStrictly) { XEUtils.eachTree(this.fullTreeData, row => { row[checkField] = checked if (halfField) { row[halfField] = false } }, treeOpts) } this.$emit('checkbox-all', params) }, checkboxChangeEvent (this: any, params: any) { const { checkboxConfig = {}, treeOpts } = this const { checkField, halfField, checkStrictly } = checkboxConfig const { row, checked } = params if (checkField && !checkStrictly) { XEUtils.eachTree([row], row => { row[checkField] = checked if (halfField) { row[halfField] = false } }, treeOpts) this.checkParentNodeSelection(row) } this.$emit('checkbox-change', params) }, checkParentNodeSelection (this: any, row: any) { const { checkboxConfig = {}, treeOpts } = this const { children } = treeOpts const { checkField, halfField, checkStrictly } = checkboxConfig const matchObj = XEUtils.findTree(this.fullTreeData, item => item === row, treeOpts) if (matchObj && checkField && !checkStrictly) { const parentRow = matchObj.parent if (parentRow) { const isAll = parentRow[children].every((item: any) => item[checkField]) if (halfField && !isAll) { parentRow[halfField] = parentRow[children].some((item: any) => item[checkField] || item[halfField]) } parentRow[checkField] = isAll this.checkParentNodeSelection(parentRow) } else { this.$refs.xTable.checkSelectionStatus() } } }, getCheckboxRecords (this: any) { const { checkboxConfig = {}, treeOpts } = this const { checkField } = checkboxConfig if (checkField) { const records: any[] = [] XEUtils.eachTree(this.fullTreeData, row => { if (row[checkField]) { records.push(row) } }, treeOpts) return records } return this.$refs.xTable.getCheckboxRecords() }, getCheckboxIndeterminateRecords (this: any) { const { checkboxConfig = {}, treeOpts } = this const { halfField } = checkboxConfig if (halfField) { const records: any[] = [] XEUtils.eachTree(this.fullTreeData, row => { if (row[halfField]) { records.push(row) } }, treeOpts) return records } return this.$refs.xTable.getCheckboxIndeterminateRecords() } } } Vue.component(VirtualTree.name, VirtualTree) } /** * 基于 vxe-table 表格的增强插件,实现简单的虚拟树表格 */ export const VXETablePluginVirtualTree = { install (xtable: typeof VXETable) { // 注册组件 registerComponent(xtable) } } if (typeof window !== 'undefined' && window.VXETable) { window.VXETable.use(VXETablePluginVirtualTree) } export default VXETablePluginVirtualTree