vxe-table
Version:
一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等...
1,549 lines (1,532 loc) • 143 kB
JavaScript
import XEUtils from 'xe-utils/ctor'
import GlobalConfig from '../../conf'
import Cell from './cell'
import VXETable from '../../v-x-e-table'
import { UtilTools, DomTools } from '../../tools'
import { clearTableAllStatus } from './util'
const { getRowid, getRowkey, setCellValue, getCellLabel, hasChildrenList, getColumnList } = UtilTools
const { browse, calcHeight, hasClass, addClass, removeClass, getEventTargetNode } = DomTools
const isWebkit = browse['-webkit'] && !browse.edge
const debounceScrollYDuration = browse.msie ? 40 : 20
const resizableStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_WIDTH'
const visibleStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_VISIBLE'
/**
* 生成行的唯一主键
*/
function getRowUniqueId () {
return XEUtils.uniqueId('row_')
}
/**
* 单元格的值为:'' | null | undefined 时都属于空值
*/
function eqCellNull (cellValue) {
return cellValue === '' || XEUtils.eqNull(cellValue)
}
function eqCellValue (row1, row2, field) {
const val1 = XEUtils.get(row1, field)
const val2 = XEUtils.get(row2, field)
if (eqCellNull(val1) && eqCellNull(val2)) {
return true
}
if (XEUtils.isString(val1) || XEUtils.isNumber(val1)) {
/* eslint-disable eqeqeq */
return val1 == val2
}
return XEUtils.isEqual(val1, val2)
}
function getNextSortOrder (_vm, column) {
const orders = _vm.sortOpts.orders
const currOrder = column.order || null
const oIndex = orders.indexOf(currOrder) + 1
return orders[oIndex < orders.length ? oIndex : 0]
}
function getCustomStorageMap (key) {
const version = GlobalConfig.version
const rest = XEUtils.toStringJSON(localStorage.getItem(key))
return rest && rest._v === version ? rest : { _v: version }
}
function getRecoverRow (_vm, list) {
const { fullAllDataRowMap } = _vm
return list.filter(row => fullAllDataRowMap.has(row))
}
function handleReserveRow (_vm, reserveRowMap) {
const { fullDataRowIdData } = _vm
const reserveList = []
XEUtils.each(reserveRowMap, (item, rowid) => {
if (fullDataRowIdData[rowid] && reserveList.indexOf(fullDataRowIdData[rowid].row) === -1) {
reserveList.push(fullDataRowIdData[rowid].row)
}
})
return reserveList
}
function computeVirtualX (_vm) {
const { $refs, visibleColumn } = _vm
const { tableBody } = $refs
const tableBodyElem = tableBody ? tableBody.$el : null
if (tableBodyElem) {
const { scrollLeft, clientWidth } = tableBodyElem
const endWidth = scrollLeft + clientWidth
let toVisibleIndex = -1
let cWidth = 0
let visibleSize = 0
for (let colIndex = 0, colLen = visibleColumn.length; colIndex < colLen; colIndex++) {
cWidth += visibleColumn[colIndex].renderWidth
if (toVisibleIndex === -1 && scrollLeft < cWidth) {
toVisibleIndex = colIndex
}
if (toVisibleIndex >= 0) {
visibleSize++
if (cWidth > endWidth) {
break
}
}
}
return { toVisibleIndex: Math.max(0, toVisibleIndex), visibleSize: Math.max(8, visibleSize) }
}
return { toVisibleIndex: 0, visibleSize: 8 }
}
function computeVirtualY (_vm) {
const { $refs, vSize, rowHeightMaps } = _vm
const { tableHeader, tableBody } = $refs
const tableBodyElem = tableBody ? tableBody.$el : null
if (tableBodyElem) {
const tableHeaderElem = tableHeader ? tableHeader.$el : null
let rowHeight = 0
let firstTrElem
firstTrElem = tableBodyElem.querySelector('tr')
if (!firstTrElem && tableHeaderElem) {
firstTrElem = tableHeaderElem.querySelector('tr')
}
if (firstTrElem) {
rowHeight = firstTrElem.clientHeight
}
if (!rowHeight) {
rowHeight = rowHeightMaps[vSize || 'default']
}
const visibleSize = Math.max(8, Math.ceil(tableBodyElem.clientHeight / rowHeight) + 2)
return { rowHeight, visibleSize }
}
return { rowHeight: 0, visibleSize: 8 }
}
function calculateMergerOffserIndex (list, offsetItem, type) {
for (let mcIndex = 0, len = list.length; mcIndex < len; mcIndex++) {
const mergeItem = list[mcIndex]
const { startIndex, endIndex } = offsetItem
const mergeStartIndex = mergeItem[type]
const mergeSpanNumber = mergeItem[type + 'span']
const mergeEndIndex = mergeStartIndex + mergeSpanNumber
if (mergeStartIndex < startIndex && startIndex < mergeEndIndex) {
offsetItem.startIndex = mergeStartIndex
}
if (mergeStartIndex < endIndex && endIndex < mergeEndIndex) {
offsetItem.endIndex = mergeEndIndex
}
if (offsetItem.startIndex !== startIndex || offsetItem.endIndex !== endIndex) {
mcIndex = -1
}
}
}
function setMerges (_vm, merges, mList, rowList) {
if (merges) {
const { treeConfig, visibleColumn } = _vm
if (treeConfig) {
throw new Error(UtilTools.getLog('vxe.error.noTree', ['merge-footer-items']))
}
if (!XEUtils.isArray(merges)) {
merges = [merges]
}
merges.forEach(item => {
let { row, col, rowspan, colspan } = item
if (rowList && XEUtils.isNumber(row)) {
row = rowList[row]
}
if (XEUtils.isNumber(col)) {
col = visibleColumn[col]
}
if ((rowList ? row : XEUtils.isNumber(row)) && col && (rowspan || colspan)) {
rowspan = XEUtils.toNumber(rowspan) || 1
colspan = XEUtils.toNumber(colspan) || 1
if (rowspan > 1 || colspan > 1) {
const mcIndex = XEUtils.findIndexOf(mList, item => item._row === row && item._col === col)
const mergeItem = mList[mcIndex]
if (mergeItem) {
mergeItem.rowspan = rowspan
mergeItem.colspan = colspan
mergeItem._rowspan = rowspan
mergeItem._colspan = colspan
} else {
const mergeRowIndex = rowList ? rowList.indexOf(row) : row
const mergeColIndex = visibleColumn.indexOf(col)
mList.push({
row: mergeRowIndex,
col: mergeColIndex,
rowspan,
colspan,
_row: row,
_col: col,
_rowspan: rowspan,
_colspan: colspan
})
}
}
}
})
}
}
function removeMerges (_vm, merges, mList, rowList) {
const rest = []
if (merges) {
const { treeConfig, visibleColumn } = _vm
if (treeConfig) {
throw new Error(UtilTools.getLog('vxe.error.noTree', ['merge-cells']))
}
if (!XEUtils.isArray(merges)) {
merges = [merges]
}
merges.forEach(item => {
let { row, col } = item
if (rowList && XEUtils.isNumber(row)) {
row = rowList[row]
}
if (XEUtils.isNumber(col)) {
col = visibleColumn[col]
}
const mcIndex = XEUtils.findIndexOf(mList, item => item._row === row && item._col === col)
if (mcIndex > -1) {
const rItems = mList.splice(mcIndex, 1)
rest.push(rItems[0])
}
})
}
return rest
}
function clearAllSort (_vm) {
_vm.tableFullColumn.forEach((column) => {
column.order = null
})
}
const Methods = {
/**
* 获取父容器元素
*/
getParentElem () {
return this.$xegrid ? this.$xegrid.$el.parentNode : this.$el.parentNode
},
/**
* 获取父容器的高度
*/
getParentHeight () {
return this.$xegrid ? this.$xegrid.getParentHeight() : this.getParentElem().clientHeight
},
/**
* 获取需要排除的高度
* 但渲染表格高度时,需要排除工具栏或分页等相关组件的高度
* 如果存在表尾合计滚动条,则需要排除滚动条高度
*/
getExcludeHeight () {
return this.$xegrid ? this.$xegrid.getExcludeHeight() : 0
},
/**
* 重置表格的一切数据状态
*/
clearAll () {
return clearTableAllStatus(this)
},
/**
* 同步 data 数据
* 如果用了该方法,那么组件将不再记录增删改的状态,只能自行实现对应逻辑
* 对于某些特殊的场景,比如深层树节点元素发生变动时可能会用到
*/
syncData () {
return this.$nextTick().then(() => {
this.tableData = []
return this.$nextTick().then(() => this.loadTableData(this.tableFullData))
})
},
/**
* 手动处理数据
* 对于手动更改了排序、筛选...等条件后需要重新处理数据时可能会用到
*/
updateData () {
return this.handleTableData(true).then(this.updateFooter).then(this.recalculate)
},
handleTableData (force) {
const { scrollYLoad, scrollYStore } = this
const fullData = force ? this.updateAfterFullData() : this.afterFullData
this.tableData = scrollYLoad ? fullData.slice(scrollYStore.startIndex, scrollYStore.endIndex) : fullData.slice(0)
return this.$nextTick()
},
/**
* 加载表格数据
* @param {Array} datas 数据
*/
loadTableData (datas) {
const { keepSource, treeConfig, editStore, sYOpts, scrollYStore, scrollXStore } = this
const tableFullData = datas ? datas.slice(0) : []
const scrollYLoad = !treeConfig && sYOpts.gt > -1 && sYOpts.gt < tableFullData.length
scrollYStore.startIndex = 0
scrollYStore.endIndex = 1
scrollXStore.startIndex = 0
scrollXStore.endIndex = 1
editStore.insertList = []
editStore.removeList = []
// 全量数据
this.tableFullData = tableFullData
// 缓存数据
this.updateCache(true)
// 原始数据
this.tableSynchData = datas
if (keepSource) {
this.tableSourceData = XEUtils.clone(tableFullData, true)
}
this.scrollYLoad = scrollYLoad
if (scrollYLoad) {
if (!(this.height || this.maxHeight)) {
UtilTools.error('vxe.error.reqProp', ['height | max-height'])
}
if (!this.showOverflow) {
UtilTools.warn('vxe.error.reqProp', ['show-overflow'])
}
if (this.spanMethod) {
UtilTools.warn('vxe.error.scrollErrProp', ['span-method'])
}
}
this.clearMergeCells()
this.clearMergeFooterItems()
this.handleTableData(true)
this.updateFooter()
return this.computeScrollLoad().then(() => {
// 是否加载了数据
if (scrollYLoad) {
scrollYStore.endIndex = scrollYStore.visibleSize
}
this.handleReserveStatus()
this.checkSelectionStatus()
return this.$nextTick().then(() => this.recalculate()).then(() => this.refreshScroll())
})
},
/**
* 重新加载数据,不会清空表格状态
* @param {Array} datas 数据
*/
loadData (datas) {
return this.loadTableData(datas).then(() => {
if (!this.inited) {
this.inited = true
this.handleDefaults()
}
this.recalculate()
})
},
/**
* 重新加载数据,会清空表格状态
* @param {Array} datas 数据
*/
reloadData (datas) {
return this.clearAll()
.then(() => {
this.inited = true
return this.loadTableData(datas)
})
.then(this.handleDefaults)
},
/**
* 局部加载行数据并恢复到初始状态
* 对于行数据需要局部更改的场景中可能会用到
* @param {Row} row 行对象
* @param {Object} record 新数据
* @param {String} field 字段名
*/
reloadRow (row, record, field) {
const { keepSource, tableSourceData, tableData } = this
if (keepSource) {
const rowIndex = this.getRowIndex(row)
const oRow = tableSourceData[rowIndex]
if (oRow && row) {
if (field) {
XEUtils.set(oRow, field, XEUtils.get(record || row, field))
} else {
if (record) {
tableSourceData[rowIndex] = record
XEUtils.clear(row, undefined)
Object.assign(row, this.defineField(Object.assign({}, record)))
this.updateCache(true)
} else {
XEUtils.destructuring(oRow, XEUtils.clone(row, true))
}
}
}
this.tableData = tableData.slice(0)
} else {
UtilTools.warn('vxe.error.reqProp', ['keep-source'])
}
return this.$nextTick()
},
/**
* 加载列配置
* 对于表格列需要重载、局部递增场景下可能会用到
* @param {ColumnInfo} columns 列配置
*/
loadColumn (columns) {
const collectColumn = XEUtils.mapTree(columns, column => Cell.createColumn(this, column))
this.handleColumn(collectColumn)
return this.$nextTick()
},
/**
* 加载列配置并恢复到初始状态
* 对于表格列需要重载、局部递增场景下可能会用到
* @param {ColumnInfo} columns 列配置
*/
reloadColumn (columns) {
this.clearAll()
return this.loadColumn(columns)
},
handleColumn (collectColumn) {
this.collectColumn = collectColumn
const tableFullColumn = getColumnList(collectColumn)
this.tableFullColumn = tableFullColumn
this.cacheColumnMap()
this.restoreCustomStorage()
this.refreshColumn().then(() => {
if (this.scrollXLoad) {
this.loadScrollXData(true)
}
})
this.clearMergeCells()
this.clearMergeFooterItems()
this.handleTableData(true)
if ((this.scrollXLoad || this.scrollYLoad) && this.expandColumn) {
UtilTools.warn('vxe.error.scrollErrProp', ['column.type=expand'])
}
this.$nextTick(() => {
if (this.$toolbar) {
this.$toolbar.syncUpdate({ collectColumn, $table: this })
}
})
},
/**
* 更新数据行的 Map
* 牺牲数据组装的耗时,用来换取使用过程中的流畅
*/
updateCache (source) {
const { treeConfig, treeOpts, tableFullData, fullDataRowMap, fullAllDataRowMap } = this
let { fullDataRowIdData, fullAllDataRowIdData } = this
const rowkey = getRowkey(this)
const isLazy = treeConfig && treeOpts.lazy
const handleCache = (row, index, items, path, parent) => {
let rowid = getRowid(this, row)
if (!rowid) {
rowid = getRowUniqueId()
XEUtils.set(row, rowkey, rowid)
}
if (isLazy && row[treeOpts.hasChild] && XEUtils.isUndefined(row[treeOpts.children])) {
row[treeOpts.children] = null
}
const rest = { row, rowid, index: treeConfig && parent ? -1 : index, items, parent }
if (source) {
fullDataRowIdData[rowid] = rest
fullDataRowMap.set(row, rest)
}
fullAllDataRowIdData[rowid] = rest
fullAllDataRowMap.set(row, rest)
}
if (source) {
fullDataRowIdData = this.fullDataRowIdData = {}
fullDataRowMap.clear()
}
fullAllDataRowIdData = this.fullAllDataRowIdData = {}
fullAllDataRowMap.clear()
if (treeConfig) {
XEUtils.eachTree(tableFullData, handleCache, treeOpts)
} else {
tableFullData.forEach(handleCache)
}
},
appendTreeCache (row, childs) {
const { keepSource, tableSourceData, treeOpts, fullDataRowIdData, fullDataRowMap, fullAllDataRowMap, fullAllDataRowIdData } = this
const { children, hasChild } = treeOpts
const rowkey = getRowkey(this)
const rowid = getRowid(this, row)
let matchObj
if (keepSource) {
matchObj = XEUtils.findTree(tableSourceData, item => rowid === getRowid(this, item), treeOpts)
}
XEUtils.eachTree(childs, (row, index, items, path, parent) => {
let rowid = getRowid(this, row)
if (!rowid) {
rowid = getRowUniqueId()
XEUtils.set(row, rowkey, rowid)
}
if (row[hasChild] && XEUtils.isUndefined(row[children])) {
row[children] = null
}
const rest = { row, rowid, index: -1, items, parent }
fullDataRowIdData[rowid] = rest
fullDataRowMap.set(row, rest)
fullAllDataRowIdData[rowid] = rest
fullAllDataRowMap.set(row, rest)
}, treeOpts)
if (matchObj) {
matchObj.item[children] = XEUtils.clone(childs, true)
}
},
/**
* 更新数据列的 Map
* 牺牲数据组装的耗时,用来换取使用过程中的流畅
*/
cacheColumnMap () {
const { tableFullColumn, collectColumn, fullColumnMap } = this
const fullColumnIdData = this.fullColumnIdData = {}
const fullColumnFieldData = this.fullColumnFieldData = {}
const isGroup = collectColumn.some(hasChildrenList)
let expandColumn
let treeNodeColumn
let checkboxColumn
let radioColumn
let hasFixed
const handleFunc = (column, index, items, path, parent) => {
const { id: colid, property, fixed, type, treeNode } = column
const rest = { column, colid, index, items, parent }
if (property) {
if (fullColumnFieldData[property]) {
UtilTools.error('vxe.error.colRepet', ['field', property])
}
fullColumnFieldData[property] = rest
}
if (!hasFixed && fixed) {
hasFixed = fixed
}
if (treeNode) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (treeNodeColumn) {
UtilTools.warn('vxe.error.colRepet', ['tree-node', treeNode])
}
}
if (!treeNodeColumn) {
treeNodeColumn = column
}
} else if (type === 'expand') {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (expandColumn) {
UtilTools.warn('vxe.error.colRepet', ['type', type])
}
}
if (!expandColumn) {
expandColumn = column
}
}
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (type === 'checkbox') {
if (checkboxColumn) {
UtilTools.warn('vxe.error.colRepet', ['type', type])
}
if (!checkboxColumn) {
checkboxColumn = column
}
} else if (type === 'radio') {
if (radioColumn) {
UtilTools.warn('vxe.error.colRepet', ['type', type])
}
if (!radioColumn) {
radioColumn = column
}
}
}
if (fullColumnIdData[colid]) {
UtilTools.error('vxe.error.colRepet', ['colId', colid])
}
fullColumnIdData[colid] = rest
fullColumnMap.set(column, rest)
}
fullColumnMap.clear()
if (isGroup) {
XEUtils.eachTree(collectColumn, (column, index, items, path, parent, nodes) => {
column.level = nodes.length
handleFunc(column, index, items, path, parent)
})
} else {
tableFullColumn.forEach(handleFunc)
}
if (expandColumn && hasFixed) {
UtilTools.warn('vxe.error.errConflicts', ['column.fixed', 'column.type=expand'])
}
if (expandColumn && this.mouseOpts.area) {
UtilTools.error('vxe.error.errConflicts', ['mouse-config.area', 'column.type=expand'])
}
this.isGroup = isGroup
this.treeNodeColumn = treeNodeColumn
this.expandColumn = expandColumn
},
/**
* 根据 tr 元素获取对应的 row 信息
* @param {Element} tr 元素
*/
getRowNode (tr) {
if (tr) {
const { fullAllDataRowIdData } = this
const rowid = tr.getAttribute('data-rowid')
const rest = fullAllDataRowIdData[rowid]
if (rest) {
return { rowid: rest.rowid, item: rest.row, index: rest.index, items: rest.items, parent: rest.parent }
}
}
return null
},
/**
* 根据 th/td 元素获取对应的 column 信息
* @param {Element} cell 元素
*/
getColumnNode (cell) {
if (cell) {
const { fullColumnIdData } = this
const colid = cell.getAttribute('data-colid')
const rest = fullColumnIdData[colid]
if (rest) {
return { colid: rest.colid, item: rest.column, index: rest.index, items: rest.items, parent: rest.parent }
}
}
return null
},
/**
* 根据 row 获取相对于 data 中的索引
* @param {Row} row 行对象
*/
getRowIndex (row) {
return this.fullDataRowMap.has(row) ? this.fullDataRowMap.get(row).index : -1
},
/**
* 根据 row 获取相对于当前数据中的索引
* @param {Row} row 行对象
*/
getVTRowIndex (row) {
return this.afterFullData.indexOf(row)
},
// 在 v3 中废弃
_getRowIndex (row) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
UtilTools.warn('vxe.error.delFunc', ['_getRowIndex', 'getVTRowIndex'])
}
return this.getVTRowIndex(row)
},
/**
* 根据 row 获取渲染中的虚拟索引
* @param {Row} row 行对象
*/
getVMRowIndex (row) {
return this.tableData.indexOf(row)
},
// 在 v3 中废弃
$getRowIndex (row) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
UtilTools.warn('vxe.error.delFunc', ['$getRowIndex', 'getVMRowIndex'])
}
return this.getVMRowIndex(row)
},
/**
* 根据 column 获取相对于 columns 中的索引
* @param {ColumnInfo} column 列配置
*/
getColumnIndex (column) {
return this.fullColumnMap.has(column) ? this.fullColumnMap.get(column).index : -1
},
/**
* 根据 column 获取相对于当前表格列中的索引
* @param {ColumnInfo} column 列配置
*/
getVTColumnIndex (column) {
return this.visibleColumn.indexOf(column)
},
// 在 v3 中废弃
_getColumnIndex (column) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
UtilTools.warn('vxe.error.delFunc', ['_getColumnIndex', 'getVTColumnIndex'])
}
return this.getVTColumnIndex(column)
},
/**
* 根据 column 获取渲染中的虚拟索引
* @param {ColumnInfo} column 列配置
*/
getVMColumnIndex (column) {
return this.tableColumn.indexOf(column)
},
// 在 v3 中废弃
$getColumnIndex (column) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
UtilTools.warn('vxe.error.delFunc', ['$getColumnIndex', 'getVMColumnIndex'])
}
return this.getVMColumnIndex(column)
},
/**
* 判断是否为索引列
* @param {ColumnInfo} column 列配置
*/
isSeqColumn (column) {
return column && column.type === 'seq'
},
/**
* 定义行数据中的列属性,如果不存在则定义
* @param {Row} record 行数据
*/
defineField (record) {
const { radioOpts, checkboxOpts, treeConfig, treeOpts, expandOpts } = this
const rowkey = getRowkey(this)
this.visibleColumn.forEach(({ property, editRender }) => {
if (property && !XEUtils.has(record, property)) {
XEUtils.set(record, property, editRender && !XEUtils.isUndefined(editRender.defaultValue) ? editRender.defaultValue : null)
}
})
const ohterFields = [radioOpts.labelField, checkboxOpts.checkField, checkboxOpts.labelField, expandOpts.labelField]
ohterFields.forEach((key) => {
if (key && !XEUtils.get(record, key)) {
XEUtils.set(record, key, null)
}
})
if (treeConfig && treeOpts.lazy && XEUtils.isUndefined(record[treeOpts.children])) {
record[treeOpts.children] = null
}
// 必须有行数据的唯一主键,可以自行设置;也可以默认生成一个随机数
if (!XEUtils.get(record, rowkey)) {
XEUtils.set(record, rowkey, getRowUniqueId())
}
return record
},
/**
* 创建 data 对象
* 对于某些特殊场景可能会用到,会自动对数据的字段名进行检测,如果不存在就自动定义
* @param {Array} records 新数据
*/
createData (records) {
const rowkey = getRowkey(this)
const rows = records.map(record => this.defineField(Object.assign({}, record, { [rowkey]: null })))
return this.$nextTick().then(() => rows)
},
/**
* 创建 Row|Rows 对象
* 对于某些特殊场景需要对数据进行手动插入时可能会用到
* @param {Array/Object} records 新数据
*/
createRow (records) {
const isArr = XEUtils.isArray(records)
if (!isArr) {
records = [records]
}
return this.$nextTick().then(() => this.createData(records).then(rows => isArr ? rows : rows[0]))
},
/**
* 还原数据
* 如果不传任何参数,则还原整个表格
* 如果传 row 则还原一行
* 如果传 rows 则还原多行
* 如果还额外传了 field 则还原指定的单元格数据
*/
revertData (rows, field) {
const { keepSource, tableSourceData, treeConfig } = this
if (keepSource) {
if (arguments.length) {
if (rows && !XEUtils.isArray(rows)) {
rows = [rows]
}
rows.forEach(row => {
if (!this.isInsertByRow(row)) {
const rowIndex = this.getRowIndex(row)
if (treeConfig && rowIndex === -1) {
throw new Error(UtilTools.getLog('vxe.error.noTree', ['revertData']))
}
const oRow = tableSourceData[rowIndex]
if (oRow && row) {
if (field) {
XEUtils.set(row, field, XEUtils.clone(XEUtils.get(oRow, field), true))
} else {
XEUtils.destructuring(row, XEUtils.clone(oRow, true))
}
}
}
})
return this.$nextTick()
}
return this.reloadData(tableSourceData)
} else {
UtilTools.warn('vxe.error.reqProp', ['keep-source'])
}
return this.$nextTick()
},
/**
* 清空单元格内容
* 如果不创参数,则清空整个表格内容
* 如果传 row 则清空一行内容
* 如果传 rows 则清空多行内容
* 如果还额外传了 field 则清空指定单元格内容
* @param {Array/Row} rows 行数据
* @param {String} field 字段名
*/
clearData (rows, field) {
const { tableFullData, visibleColumn } = this
if (!arguments.length) {
rows = tableFullData
} else if (rows && !XEUtils.isArray(rows)) {
rows = [rows]
}
if (field) {
rows.forEach(row => XEUtils.set(row, field, null))
} else {
rows.forEach(row => {
visibleColumn.forEach(column => {
if (column.property) {
setCellValue(row, column, null)
}
})
})
}
return this.$nextTick()
},
/**
* 检查是否为临时行数据
* @param {Row} row 行对象
*/
isInsertByRow (row) {
return this.editStore.insertList.indexOf(row) > -1
},
/**
* 检查行或列数据是否发生改变
* @param {Row} row 行对象
* @param {String} field 字段名
*/
isUpdateByRow (row, field) {
const { visibleColumn, keepSource, treeConfig, treeOpts, tableSourceData, fullDataRowIdData } = this
if (keepSource) {
let oRow, property
const rowid = getRowid(this, row)
// 新增的数据不需要检测
if (!fullDataRowIdData[rowid]) {
return false
}
if (treeConfig) {
const children = treeOpts.children
const matchObj = XEUtils.findTree(tableSourceData, item => rowid === getRowid(this, item), treeOpts)
row = Object.assign({}, row, { [children]: null })
if (matchObj) {
oRow = Object.assign({}, matchObj.item, { [children]: null })
}
} else {
const oRowIndex = fullDataRowIdData[rowid].index
oRow = tableSourceData[oRowIndex]
}
if (oRow) {
if (arguments.length > 1) {
return !eqCellValue(oRow, row, field)
}
for (let index = 0, len = visibleColumn.length; index < len; index++) {
property = visibleColumn[index].property
if (property && !eqCellValue(oRow, row, property)) {
return true
}
}
}
}
return false
},
/**
* 获取表格的可视列,也可以指定索引获取列
* @param {Number} columnIndex 索引
*/
getColumns (columnIndex) {
const columns = this.visibleColumn
return arguments.length ? columns[columnIndex] : columns.slice(0)
},
/**
* 根据列的唯一主键获取列
* @param {String} colid 列主键
*/
getColumnById (colid) {
const fullColumnIdData = this.fullColumnIdData
return fullColumnIdData[colid] ? fullColumnIdData[colid].column : null
},
/**
* 根据列的字段名获取列
* @param {String} field 字段名
*/
getColumnByField (field) {
const fullColumnFieldData = this.fullColumnFieldData
return fullColumnFieldData[field] ? fullColumnFieldData[field].column : null
},
/**
* 获取当前表格的列
* 收集到的全量列、全量表头列、处理条件之后的全量表头列、当前渲染中的表头列
*/
getTableColumn () {
return {
collectColumn: this.collectColumn.slice(0),
fullColumn: this.tableFullColumn.slice(0),
visibleColumn: this.visibleColumn.slice(0),
tableColumn: this.tableColumn.slice(0)
}
},
/**
* 获取数据,和 data 的行为一致,也可以指定索引获取数据
*/
getData (rowIndex) {
const tableSynchData = this.data || this.tableSynchData
return arguments.length ? tableSynchData[rowIndex] : tableSynchData.slice(0)
},
/**
* 用于多选行,获取已选中的数据
*/
getCheckboxRecords () {
const { tableFullData, treeConfig, treeOpts, checkboxOpts } = this
const { checkField: property } = checkboxOpts
let rowList = []
if (property) {
if (treeConfig) {
rowList = XEUtils.filterTree(tableFullData, row => XEUtils.get(row, property), treeOpts)
} else {
rowList = tableFullData.filter(row => XEUtils.get(row, property))
}
} else {
const { selection } = this
if (treeConfig) {
rowList = XEUtils.filterTree(tableFullData, row => selection.indexOf(row) > -1, treeOpts)
} else {
rowList = tableFullData.filter(row => selection.indexOf(row) > -1)
}
}
return rowList
},
/**
* 获取处理后全量的表格数据
* 如果存在筛选条件,继续处理
*/
updateAfterFullData () {
const { visibleColumn, tableFullData, filterOpts, sortOpts } = this
const { remote: allRemoteFilter, filterMethod: allFilterMethod } = filterOpts
const { remote: allRemoteSort, sortMethod: allSortMethod, multiple: sortMultiple } = sortOpts
let tableData = tableFullData.slice(0)
const filterColumns = []
const orderColumns = []
visibleColumn.forEach(column => {
const { sortable, order, filters } = column
if (!allRemoteFilter && filters && filters.length) {
const valueList = []
const itemList = []
filters.forEach((item) => {
if (item.checked) {
itemList.push(item)
valueList.push(item.value)
}
})
if (itemList.length) {
filterColumns.push({ column, valueList, itemList })
}
}
if (!allRemoteSort && sortable && order) {
orderColumns.push({ column, sortBy: column.sortBy, property: column.property, order })
}
})
if (filterColumns.length) {
tableData = tableData.filter(row => {
return filterColumns.every(({ column, valueList, itemList }) => {
if (valueList.length && !allRemoteFilter) {
const { filterRender, property } = column
let { filterMethod } = column
const compConf = filterRender ? VXETable.renderer.get(filterRender.name) : null
if (!filterMethod && compConf && compConf.renderFilter) {
filterMethod = compConf.filterMethod
}
if (allFilterMethod && !filterMethod) {
return allFilterMethod({ options: itemList, values: valueList, row, column })
}
return filterMethod ? itemList.some(item => filterMethod({ value: item.value, option: item, row, column })) : valueList.indexOf(XEUtils.get(row, property)) > -1
}
return true
})
})
}
const firstOrderColumn = orderColumns[0]
if (!allRemoteSort && firstOrderColumn) {
if (allSortMethod) {
const sortRests = allSortMethod({ data: tableData, column: firstOrderColumn.column, property: firstOrderColumn.property, order: firstOrderColumn.order, sortList: orderColumns, $table: this })
tableData = XEUtils.isArray(sortRests) ? sortRests : tableData
} else {
const params = { $table: this }
// 兼容 v4
if (sortMultiple) {
tableData = XEUtils.orderBy(tableData, orderColumns.map(({ column, property, order }) => {
return {
field: (column.sortBy ? (XEUtils.isArray(column.sortBy) ? column.sortBy[0] : column.sortBy) : null) || (column.formatter ? (row) => getCellLabel(row, column, params) : property),
order
}
}))
} else {
// 兼容 v2,在 v4 中废弃, sortBy 不能为数组
let sortByConfs
if (firstOrderColumn.sortBy) {
sortByConfs = (XEUtils.isArray(firstOrderColumn.sortBy) ? firstOrderColumn.sortBy : [firstOrderColumn.sortBy]).map(item => {
return {
field: item,
order: firstOrderColumn.order
}
})
}
tableData = XEUtils.orderBy(tableData, sortByConfs || [firstOrderColumn].map(({ column, property, order }) => {
return {
field: column.formatter ? (row) => getCellLabel(row, column, params) : property,
order
}
}))
}
}
}
this.afterFullData = tableData
return tableData
},
/**
* 根据行的唯一主键获取行
* @param {String/Number} rowid 行主键
*/
getRowById (rowid) {
const fullDataRowIdData = this.fullDataRowIdData
return fullDataRowIdData[rowid] ? fullDataRowIdData[rowid].row : null
},
/**
* 根据行获取行的唯一主键
* @param {Row} row 行对象
*/
getRowid (row) {
const fullAllDataRowMap = this.fullAllDataRowMap
return fullAllDataRowMap.has(row) ? fullAllDataRowMap.get(row).rowid : null
},
/**
* 获取处理后的表格数据
* 如果存在筛选条件,继续处理
* 如果存在排序,继续处理
*/
getTableData () {
const { tableFullData, afterFullData, tableData, footerData } = this
return {
fullData: tableFullData.slice(0),
visibleData: afterFullData.slice(0),
tableData: tableData.slice(0),
footerData: footerData.slice(0)
}
},
/**
* 默认行为只允许执行一次,除非被重置
*/
handleDefaults () {
if (this.checkboxConfig) {
this.handleDefaultSelectionChecked()
}
if (this.radioConfig) {
this.handleDefaultRadioChecked()
}
if (this.sortConfig) {
this.handleDefaultSort()
}
if (this.expandConfig) {
this.handleDefaultRowExpand()
}
if (this.treeConfig) {
this.handleDefaultTreeExpand()
}
if (this.mergeCells) {
this.handleDefaultMergeCells()
}
if (this.mergeFooterItems) {
this.handleDefaultMergeFooterItems()
}
this.$nextTick(() => requestAnimationFrame(this.recalculate))
},
/**
* 隐藏指定列
* @param {ColumnInfo} column 列配置
*/
hideColumn (column) {
column.visible = false
return this.handleCustom()
},
/**
* 显示指定列
* @param {ColumnInfo} column 列配置
*/
showColumn (column) {
column.visible = true
return this.handleCustom()
},
/**
* 手动重置列的显示隐藏、列宽拖动的状态;
* 如果为 true 则重置所有状态
* 如果已关联工具栏,则会同步更新
*/
resetColumn (options) {
const { customOpts } = this
const { checkMethod } = customOpts
const opts = Object.assign({ visible: true, resizable: options === true }, options)
this.tableFullColumn.forEach(column => {
if (opts.resizable) {
column.resizeWidth = 0
}
if (!checkMethod || checkMethod({ column })) {
column.visible = column.defaultVisible
}
})
if (opts.resizable) {
this.saveCustomResizable(true)
}
return this.handleCustom()
},
handleCustom () {
this.saveCustomVisible()
this.analyColumnWidth()
return this.refreshColumn()
},
/**
* 还原自定义列操作状态
*/
restoreCustomStorage () {
const { id, collectColumn, customConfig, customOpts } = this
const { storage } = customOpts
const isAllStorage = customOpts.storage === true
const isResizable = isAllStorage || (storage && storage.resizable)
const isVisible = isAllStorage || (storage && storage.visible)
if (customConfig && (isResizable || isVisible)) {
const customMap = {}
if (!id) {
UtilTools.error('vxe.error.reqProp', ['id'])
return
}
if (isResizable) {
const columnWidthStorage = getCustomStorageMap(resizableStorageKey)[id]
if (columnWidthStorage) {
XEUtils.each(columnWidthStorage, (resizeWidth, field) => {
customMap[field] = { field, resizeWidth }
})
}
}
if (isVisible) {
const columnVisibleStorage = getCustomStorageMap(visibleStorageKey)[id]
if (columnVisibleStorage) {
const colVisibles = columnVisibleStorage.split('|')
const colHides = colVisibles[0] ? colVisibles[0].split(',') : []
const colShows = colVisibles[1] ? colVisibles[1].split(',') : []
colHides.forEach(field => {
if (customMap[field]) {
customMap[field].visible = false
} else {
customMap[field] = { field, visible: false }
}
})
colShows.forEach(field => {
if (customMap[field]) {
customMap[field].visible = true
} else {
customMap[field] = { field, visible: true }
}
})
}
}
const keyMap = {}
XEUtils.eachTree(collectColumn, column => {
const colKey = column.getKey()
if (colKey) {
keyMap[colKey] = column
}
})
XEUtils.each(customMap, ({ visible, resizeWidth }, field) => {
const column = keyMap[field]
if (column) {
if (XEUtils.isNumber(resizeWidth)) {
column.resizeWidth = resizeWidth
}
if (XEUtils.isBoolean(visible)) {
column.visible = visible
}
}
})
}
},
saveCustomVisible () {
const { id, collectColumn, customConfig, customOpts } = this
const { checkMethod, storage } = customOpts
const isAllStorage = customOpts.storage === true
const isVisible = isAllStorage || (storage && storage.visible)
if (customConfig && isVisible) {
const columnVisibleStorageMap = getCustomStorageMap(visibleStorageKey)
const colHides = []
const colShows = []
if (!id) {
UtilTools.error('vxe.error.reqProp', ['id'])
return
}
XEUtils.eachTree(collectColumn, column => {
if (!checkMethod || checkMethod({ column })) {
if (!column.visible && column.defaultVisible) {
const colKey = column.getKey()
if (colKey) {
colHides.push(colKey)
}
} else if (column.visible && !column.defaultVisible) {
const colKey = column.getKey()
if (colKey) {
colShows.push(colKey)
}
}
}
})
columnVisibleStorageMap[id] = [colHides.join(',')].concat(colShows.length ? [colShows.join(',')] : []).join('|') || undefined
localStorage.setItem(visibleStorageKey, XEUtils.toJSONString(columnVisibleStorageMap))
}
},
saveCustomResizable (isReset) {
const { id, collectColumn, customConfig, customOpts } = this
const { storage } = customOpts
const isAllStorage = customOpts.storage === true
const isResizable = isAllStorage || (storage && storage.resizable)
if (customConfig && isResizable) {
const columnWidthStorageMap = getCustomStorageMap(resizableStorageKey)
let columnWidthStorage
if (!id) {
UtilTools.error('vxe.error.reqProp', ['id'])
return
}
if (!isReset) {
columnWidthStorage = XEUtils.isPlainObject(columnWidthStorageMap[id]) ? columnWidthStorageMap[id] : {}
XEUtils.eachTree(collectColumn, column => {
if (column.resizeWidth) {
const colKey = column.getKey()
if (colKey) {
columnWidthStorage[colKey] = column.renderWidth
}
}
})
}
columnWidthStorageMap[id] = XEUtils.isEmpty(columnWidthStorage) ? undefined : columnWidthStorage
localStorage.setItem(resizableStorageKey, XEUtils.toJSONString(columnWidthStorageMap))
}
},
/**
* 刷新列信息
* 将固定的列左边、右边分别靠边
*/
refreshColumn () {
const leftList = []
const centerList = []
const rightList = []
const { collectColumn, tableFullColumn, isGroup, columnStore, sXOpts, scrollXStore } = this
// 如果是分组表头,如果子列全部被隐藏,则根列也隐藏
if (isGroup) {
const leftGroupList = []
const centerGroupList = []
const rightGroupList = []
XEUtils.eachTree(collectColumn, (column, index, items, path, parent) => {
const isColGroup = hasChildrenList(column)
// 如果是分组,必须按组设置固定列,不允许给子列设置固定
if (parent && parent.fixed) {
column.fixed = parent.fixed
}
if (parent && column.fixed !== parent.fixed) {
UtilTools.error('vxe.error.groupFixed')
}
if (isColGroup) {
column.visible = !!XEUtils.findTree(column.children, subColumn => hasChildrenList(subColumn) ? null : subColumn.visible)
} else if (column.visible) {
if (column.fixed === 'left') {
leftList.push(column)
} else if (column.fixed === 'right') {
rightList.push(column)
} else {
centerList.push(column)
}
}
})
collectColumn.forEach((column) => {
if (column.visible) {
if (column.fixed === 'left') {
leftGroupList.push(column)
} else if (column.fixed === 'right') {
rightGroupList.push(column)
} else {
centerGroupList.push(column)
}
}
})
this.tableGroupColumn = leftGroupList.concat(centerGroupList).concat(rightGroupList)
} else {
// 重新分配列
tableFullColumn.forEach((column) => {
if (column.visible) {
if (column.fixed === 'left') {
leftList.push(column)
} else if (column.fixed === 'right') {
rightList.push(column)
} else {
centerList.push(column)
}
}
})
}
const visibleColumn = leftList.concat(centerList).concat(rightList)
let scrollXLoad = sXOpts.gt > -1 && sXOpts.gt < tableFullColumn.length
Object.assign(columnStore, { leftList, centerList, rightList })
if (scrollXLoad && isGroup) {
scrollXLoad = false
UtilTools.warn('vxe.error.scrollXNotGroup')
}
if (scrollXLoad) {
if (this.showHeader && !this.showHeaderOverflow) {
UtilTools.warn('vxe.error.reqProp', ['show-header-overflow'])
}
if (this.showFooter && !this.showFooterOverflow) {
UtilTools.warn('vxe.error.reqProp', ['show-footer-overflow'])
}
if (this.spanMethod) {
UtilTools.warn('vxe.error.scrollErrProp', ['span-method'])
}
if (this.footerSpanMethod) {
UtilTools.warn('vxe.error.scrollErrProp', ['footer-span-method'])
}
const { visibleSize } = computeVirtualX(this)
scrollXStore.startIndex = 0
scrollXStore.endIndex = visibleSize
scrollXStore.visibleSize = visibleSize
}
// 如果列被显示/隐藏,则清除合并状态
// 如果列被设置为固定,则清除合并状态
if (visibleColumn.length !== this.visibleColumn.length || !this.visibleColumn.every((column, index) => column === visibleColumn[index])) {
this.clearMergeCells()
this.clearMergeFooterItems()
}
this.scrollXLoad = scrollXLoad
this.visibleColumn = visibleColumn
this.handleTableColumn()
return this.$nextTick().then(() => {
this.updateFooter()
return this.recalculate(true)
}).then(() => {
this.updateCellAreas()
return this.$nextTick().then(() => this.recalculate())
})
},
/**
* 指定列宽的列进行拆分
*/
analyColumnWidth () {
const { columnOpts } = this
const { width: defaultWidth, minWidth: defaultMinWidth } = columnOpts
const resizeList = []
const pxList = []
const pxMinList = []
const scaleList = []
const scaleMinList = []
const autoList = []
this.tableFullColumn.forEach(column => {
if (defaultWidth && !column.width) {
column.width = defaultWidth
}
if (defaultMinWidth && !column.minWidth) {
column.minWidth = defaultMinWidth
}
if (column.visible) {
if (column.resizeWidth) {
resizeList.push(column)
} else if (DomTools.isPx(column.width)) {
pxList.push(column)
} else if (DomTools.isScale(column.width)) {
scaleList.push(column)
} else if (DomTools.isPx(column.minWidth)) {
pxMinList.push(column)
} else if (DomTools.isScale(column.minWidth)) {
scaleMinList.push(column)
} else {
autoList.push(column)
}
}
})
Object.assign(this.columnStore, { resizeList, pxList, pxMinList, scaleList, scaleMinList, autoList })
},
/**
* 刷新滚动操作,手动同步滚动相关位置(对于某些特殊的操作,比如滚动条错位、固定列不同步)
*/
refreshScroll () {
const { lastScrollLeft, lastScrollTop } = this
return this.clearScroll().then(() => {
if (lastScrollLeft || lastScrollTop) {
// 重置最后滚动状态
this.lastScrollLeft = 0
this.lastScrollTop = 0
// 还原滚动状态
return this.scrollTo(lastScrollLeft, lastScrollTop)
}
})
},
/**
* 计算单元格列宽,动态分配可用剩余空间
* 支持 width=? width=?px width=?% min-width=? min-width=?px min-width=?%
*/
recalculate (refull) {
const { $refs } = this
const { tableBody, tableHeader, tableFooter } = $refs
const bodyElem = tableBody ? tableBody.$el : null
const headerElem = tableHeader ? tableHeader.$el : null
const footerElem = tableFooter ? tableFooter.$el : null
if (bodyElem) {
this.autoCellWidth(headerElem, bodyElem, footerElem)
if (refull === true) {
// 初始化时需要在列计算之后再执行优化运算,达到最优显示效果
return this.computeScrollLoad().then(() => {
this.autoCellWidth(headerElem, bodyElem, footerElem)
this.computeScrollLoad()
})
}
}
return this.computeScrollLoad()
},
/**
* 列宽算法
* 支持 px、%、固定 混合分配
* 支持动态列表调整分配
* 支持自动分配偏移量
* @param {Element} headerElem
* @param {Element} bodyElem
* @param {Element} footerElem
* @param {Number} bodyWidth
*/
autoCellWidth (headerElem, bodyElem, footerElem) {
let tableWidth = 0
const minCellWidth = 40 // 列宽最少限制 40px
const bodyWidth = bodyElem.clientWidth
let remainWidth = bodyWidth
let meanWidth = remainWidth / 100
const { fit, columnStore } = this
const { resizeList, pxMinList, pxList, scaleList, scaleMinList, autoList } = columnStore
// 最小宽
pxMinList.forEach(column => {
const minWidth = parseInt(column.minWidth)
tableWidth += minWidth
column.renderWidth = minWidth
})
// 最小百分比
scaleMinList.forEach(column => {
const scaleWidth = Math.floor(parseInt(column.minWidth) * meanWidth)
tableWidth += scaleWidth
column.renderWidth = scaleWidth
})
// 固定百分比
scaleList.forEach(column => {
const scaleWidth = Math.floor(parseInt(column.width) * meanWidth)
tableWidth += scaleWidth
column.renderWidth = scaleWidth
})
// 固定宽
pxList.forEach(column => {
const width = parseInt(column.width)
tableWidth += width
column.renderWidth = width
})
// 调整了列宽
resizeList.forEach(column => {
const width = parseInt(column.resizeWidth)
tableWidth += width
column.renderWidth = width
})
remainWidth -= tableWidth
meanWidth = remainWidth > 0 ? Math.floor(remainWidth / (scaleMinList.length + pxMinList.length + autoList.length)) : 0
if (fit) {
if (remainWidth > 0) {
scaleMinList.concat(pxMinList).forEach(column => {
tableWidth += meanWidth
column.renderWidth += meanWidth
})
}
} else {
meanWidth = minCellWidth
}
// 自适应
autoList.forEach(column => {
const width = Math.max(meanWidth, minCellWidth)
column.renderWidth = width
tableWidth += width
})
if (fit) {
/**
* 偏移量算法
* 如果所有列足够放的情况下,从最后动态列开始分配
*/
const dynamicList = scaleList.concat(scaleMinList).concat(pxMinList).concat(autoList)
let dynamicSize = dynamicList.length - 1
if (dynamicSize > 0) {
let odiffer = bodyWidth - tableWidth
if (odiffer > 0) {
while (odiffer > 0 && dynamicSize >= 0) {
odiffer--
dynamicList[dynamicSize--].renderWidth++
}
tableWidth = bodyWidth
}
}
}
const tableHeight = bodyElem.offsetHeight
const overflowY = bodyElem.scrollHeight > bodyElem.clientHeight
this.scrollbarWidth = overflowY ? bodyElem.offsetWidth - bodyWidth : 0
this.overflowY = overflowY
this.tableWidth = tableWidth
this.tableHeight = tableHeight
if (headerElem) {
this.headerHeight = headerElem.clientHeight
// 检测是否同步滚动
if (headerElem.scrollLeft !== bodyElem.scrollLeft) {
headerElem.scrollLeft = bodyElem.scrollLeft
}
} else {
this.headerHeight = 0
}
if (footerElem) {
const footerHeight = footerElem.offsetHeight
this.scrollbarHeight = Math.max(footerHeight - footerElem.clientHeight, 0)
this.overflowX = tableWidth > footerElem.clientWidth
this.footerHeight = footerHeight
} else {
this.footerHeight = 0
this.scrollbarHeight = Math.max(tableHeight - bodyElem.clientHeight, 0)
this.overflowX = tableWidth > bodyWidth
}
this.customHeight = calcHeight(this, 'height')
this.customMaxHeight = calcHeight(this, 'maxHeight')
this.parentHeight = Math.max(this.headerHeight + this.footerHeight + 20, this.getParentHeight())
if (this.overflowX) {
this.checkScrolling()
}
},
updateStyle () {
let {
$refs,
isGroup,
fullColumnIdData,
tableColumn,
customHeight,
customMaxHeight,
border,
headerHeight,
showFooter,
showOverflow: allColumnOverflow,
showHeaderOverflow: allColumnHeaderOverflow,
showFooterOverflow: allColumnFooterOverflow,
footerHeight,
tableHeight,
tableWidth,
scrollbarHeight,
scrollbarWidth,
scrollXLoad,
scrollYLoad,
cellOffsetWidth,
columnStore,
elemStore,
editStore,
currentRow,
mouseConfig
} = this
const containerList = ['main', 'left', 'right']
const emptyPlaceholderElem = $refs.emptyPlaceholder
const bodyWrapperElem = elemStore['main-body-wrapper']
if (emptyPlaceholderElem) {
emptyPlaceholderElem.style.top = `${headerHeight}px`
em