vxe-pc-ui
Version:
A vue based PC component library
1,446 lines (1,354 loc) • 98.5 kB
text/typescript
import { ref, h, reactive, PropType, computed, VNode, watch, onBeforeUnmount, nextTick, onMounted, provide } from 'vue'
import { defineVxeComponent } from '../../ui/src/comp'
import { VxeUI, createEvent, useSize, globalEvents, globalResize, renderEmptyElement } from '../../ui'
import { calcTreeLine, enNodeValue, deNodeValue } from './util'
import { errLog } from '../../ui/src/log'
import { getCrossTreeDragNodeInfo } from './store'
import XEUtils from 'xe-utils'
import { getSlotVNs } from '../../ui/src/vn'
import { toCssUnit, isScale, getPaddingTopBottomSize, addClass, removeClass, getTpImg, hasControlKey, getEventTargetNode } from '../../ui/src/dom'
import { isEnableConf } from '../../ui/src/utils'
import { moveRowAnimateToTb, clearRowAnimate } from '../../ui/src/anime'
import VxeLoadingComponent from '../../loading'
import type { TreeReactData, VxeTreeEmits, VxeTreePropTypes, TreeInternalData, TreePrivateRef, VxeTreeDefines, VxeTreePrivateComputed, TreePrivateMethods, TreeMethods, ValueOf, VxeTreeConstructor, VxeTreePrivateMethods, VxeComponentStyleType } from '../../../types'
const { menus, getConfig, getI18n, getIcon } = VxeUI
/**
* 生成节点的唯一主键
*/
function getNodeUniqueId () {
return XEUtils.uniqueId('node_')
}
function createInternalData (): TreeInternalData {
return {
// initialized: false,
// lastFilterValue: '',
treeFullData: [],
afterTreeList: [],
afterVisibleList: [],
nodeMaps: {},
selectCheckboxMaps: {},
indeterminateRowMaps: {},
treeExpandedMaps: {},
treeExpandLazyLoadedMaps: {},
lastScrollLeft: 0,
lastScrollTop: 0,
scrollYStore: {
startIndex: 0,
endIndex: 0,
visibleSize: 0,
offsetSize: 0,
rowHeight: 0
},
// prevDragNode: null,
// prevDragToChild: false,
// prevDragPos: ''
lastScrollTime: 0
// hpTimeout: undefined
}
}
function createReactData ():TreeReactData {
return {
parentHeight: 0,
customHeight: 0,
customMinHeight: 0,
customMaxHeight: 0,
currentNode: null,
scrollYLoad: false,
bodyHeight: 0,
topSpaceHeight: 0,
selectRadioKey: null,
treeList: [],
updateExpandedFlag: 1,
updateCheckboxFlag: 1,
dragNode: null,
dragTipText: ''
}
}
// let crossTreeDragNodeObj: {
// $oldTree: VxeTreeConstructor & VxeTreePrivateMethods
// $newTree: (VxeTreeConstructor & VxeTreePrivateMethods) | null
// } | null = null
export default defineVxeComponent({
name: 'VxeTree',
props: {
data: Array as PropType<VxeTreePropTypes.Data>,
autoResize: {
type: Boolean as PropType<VxeTreePropTypes.AutoResize>,
default: () => getConfig().tree.autoResize
},
height: [String, Number] as PropType<VxeTreePropTypes.Height>,
maxHeight: {
type: [String, Number] as PropType<VxeTreePropTypes.MaxHeight>,
default: () => getConfig().tree.maxHeight
},
minHeight: {
type: [String, Number] as PropType<VxeTreePropTypes.MinHeight>,
default: () => getConfig().tree.minHeight
},
loading: Boolean as PropType<VxeTreePropTypes.Loading>,
loadingConfig: Object as PropType<VxeTreePropTypes.LoadingConfig>,
accordion: {
type: Boolean as PropType<VxeTreePropTypes.Accordion>,
default: () => getConfig().tree.accordion
},
childrenField: {
type: String as PropType<VxeTreePropTypes.ChildrenField>,
default: () => getConfig().tree.childrenField
},
valueField: {
type: String as PropType<VxeTreePropTypes.ValueField>,
default: () => getConfig().tree.valueField
},
keyField: {
type: String as PropType<VxeTreePropTypes.KeyField>,
default: () => getConfig().tree.keyField
},
parentField: {
type: String as PropType<VxeTreePropTypes.ParentField>,
default: () => getConfig().tree.parentField
},
titleField: {
type: String as PropType<VxeTreePropTypes.TitleField>,
default: () => getConfig().tree.titleField
},
hasChildField: {
type: String as PropType<VxeTreePropTypes.HasChildField>,
default: () => getConfig().tree.hasChildField
},
mapChildrenField: {
type: String as PropType<VxeTreePropTypes.MapChildrenField>,
default: () => getConfig().tree.mapChildrenField
},
transform: Boolean as PropType<VxeTreePropTypes.Transform>,
// 已废弃
isCurrent: Boolean as PropType<VxeTreePropTypes.IsCurrent>,
// 已废弃
isHover: Boolean as PropType<VxeTreePropTypes.IsHover>,
expandAll: Boolean as PropType<VxeTreePropTypes.ExpandAll>,
expandNodeKeys: Array as PropType<VxeTreePropTypes.ExpandNodeKeys>,
showLine: {
type: Boolean as PropType<VxeTreePropTypes.ShowLine>,
default: () => getConfig().tree.showLine
},
trigger: String as PropType<VxeTreePropTypes.Trigger>,
indent: {
type: Number as PropType<VxeTreePropTypes.Indent>,
default: () => getConfig().tree.indent
},
showRadio: {
type: Boolean as PropType<VxeTreePropTypes.ShowRadio>,
default: () => getConfig().tree.showRadio
},
checkNodeKey: {
type: [String, Number] as PropType<VxeTreePropTypes.CheckNodeKey>,
default: () => getConfig().tree.checkNodeKey
},
radioConfig: Object as PropType<VxeTreePropTypes.RadioConfig>,
showCheckbox: {
type: Boolean as PropType<VxeTreePropTypes.ShowCheckbox>,
default: () => getConfig().tree.showCheckbox
},
checkNodeKeys: {
type: Array as PropType<VxeTreePropTypes.CheckNodeKeys>,
default: () => getConfig().tree.checkNodeKeys
},
checkboxConfig: Object as PropType<VxeTreePropTypes.CheckboxConfig>,
nodeConfig: Object as PropType<VxeTreePropTypes.NodeConfig>,
lazy: Boolean as PropType<VxeTreePropTypes.Lazy>,
toggleMethod: Function as PropType<VxeTreePropTypes.ToggleMethod>,
loadMethod: Function as PropType<VxeTreePropTypes.LoadMethod>,
drag: {
type: Boolean as PropType<VxeTreePropTypes.Drag>,
default: () => getConfig().tree.drag
},
dragConfig: Object as PropType<VxeTreePropTypes.DragConfig>,
menuConfig: Object as PropType<VxeTreePropTypes.MenuConfig>,
showIcon: {
type: Boolean as PropType<VxeTreePropTypes.ShowIcon>,
default: true
},
iconOpen: {
type: String as PropType<VxeTreePropTypes.IconOpen>,
default: () => getConfig().tree.iconOpen
},
iconClose: {
type: String as PropType<VxeTreePropTypes.IconClose>,
default: () => getConfig().tree.iconClose
},
iconLoaded: {
type: String as PropType<VxeTreePropTypes.IconLoaded>,
default: () => getConfig().tree.iconLoaded
},
filterValue: [String, Number] as PropType<VxeTreePropTypes.FilterValue>,
filterConfig: Object as PropType<VxeTreePropTypes.FilterConfig>,
size: {
type: String as PropType<VxeTreePropTypes.Size>,
default: () => getConfig().tree.size || getConfig().size
},
virtualYConfig: Object as PropType<VxeTreePropTypes.VirtualYConfig>
},
emits: [
'update:modelValue',
'update:checkNodeKey',
'update:checkNodeKeys',
'node-click',
'node-dblclick',
'current-change',
'radio-change',
'checkbox-change',
'load-success',
'load-error',
'scroll',
'node-dragstart',
'node-dragover',
'node-dragend',
'node-expand',
'node-menu',
'menu-click'
] as VxeTreeEmits,
setup (props, context) {
const { emit, slots } = context
const xID = XEUtils.uniqueId()
const { computeSize } = useSize(props)
const refElem = ref<HTMLDivElement>()
const refHeaderWrapperElem = ref<HTMLDivElement>()
const refFooterWrapperElem = ref<HTMLDivElement>()
const refVirtualWrapper = ref<HTMLDivElement>()
const refVirtualBody = ref<HTMLDivElement>()
const refDragNodeLineElem = ref<HTMLDivElement>()
const refDragTipElem = ref<HTMLDivElement>()
const crossTreeDragNodeInfo = getCrossTreeDragNodeInfo()
const internalData = createInternalData()
const reactData = reactive(createReactData())
const refMaps: TreePrivateRef = {
refElem
}
const computeTitleField = computed(() => {
return props.titleField || 'title'
})
const computeKeyField = computed(() => {
return props.keyField || 'id'
})
const computeValueField = computed(() => {
const keyField = computeKeyField.value
return props.valueField || keyField
})
const computeParentField = computed(() => {
return props.parentField || 'parentId'
})
const computeChildrenField = computed(() => {
return props.childrenField || 'children'
})
const computeMapChildrenField = computed(() => {
return props.mapChildrenField || 'mapChildren'
})
const computeHasChildField = computed(() => {
return props.hasChildField || 'hasChild'
})
const computeVirtualYOpts = computed(() => {
return Object.assign({} as { gt: number }, getConfig().tree.virtualYConfig, props.virtualYConfig)
})
const computeIsRowCurrent = computed(() => {
const nodeOpts = computeNodeOpts.value
const { isCurrent } = nodeOpts
if (XEUtils.isBoolean(isCurrent)) {
return isCurrent
}
return props.isCurrent
})
const computeIsRowHover = computed(() => {
const nodeOpts = computeNodeOpts.value
const { isHover } = nodeOpts
if (XEUtils.isBoolean(isHover)) {
return isHover
}
return props.isHover
})
const computeRadioOpts = computed(() => {
return Object.assign({ showIcon: true }, getConfig().tree.radioConfig, props.radioConfig)
})
const computeCheckboxOpts = computed(() => {
return Object.assign({ showIcon: true }, getConfig().tree.checkboxConfig, props.checkboxConfig)
})
const computeNodeOpts = computed(() => {
return Object.assign({}, getConfig().tree.nodeConfig, props.nodeConfig)
})
const computeLoadingOpts = computed(() => {
return Object.assign({}, getConfig().tree.loadingConfig, props.loadingConfig)
})
const computeDragOpts = computed(() => {
return Object.assign({}, getConfig().tree.dragConfig, props.dragConfig)
})
const computeMenuOpts = computed(() => {
return Object.assign({}, getConfig().tree.menuConfig, props.menuConfig)
})
const computeTreeStyle = computed(() => {
const { indent } = props
const { customHeight, customMinHeight, customMaxHeight } = reactData
const stys: VxeComponentStyleType = {}
if (customHeight) {
stys.height = toCssUnit(customHeight)
}
if (customMinHeight) {
stys.minHeight = toCssUnit(customMinHeight)
}
if (customMaxHeight) {
stys.maxHeight = toCssUnit(customMaxHeight)
}
if (indent) {
stys['--vxe-ui-tree-node-indent'] = toCssUnit(indent)
}
return stys
})
const computeFilterOpts = computed(() => {
return Object.assign({}, getConfig().tree.filterConfig, props.filterConfig)
})
const computeMaps: VxeTreePrivateComputed = {
computeKeyField,
computeParentField,
computeChildrenField,
computeMapChildrenField,
computeRadioOpts,
computeCheckboxOpts,
computeNodeOpts,
computeDragOpts
}
const $xeTree = {
xID,
props,
context,
internalData,
reactData,
getRefMaps: () => refMaps,
getComputeMaps: () => computeMaps
} as unknown as VxeTreeConstructor & VxeTreePrivateMethods
const getNodeId = (node: any) => {
const valueField = computeValueField.value
const nodeKey = XEUtils.get(node, valueField)
return enNodeValue(nodeKey)
}
const isExpandByNode = (node: any) => {
const { updateExpandedFlag } = reactData
const { treeExpandedMaps } = internalData
const nodeid = getNodeId(node)
return !!(updateExpandedFlag && treeExpandedMaps[nodeid])
}
const isCheckedByRadioNodeId = (nodeid: any) => {
const { selectRadioKey } = reactData
return selectRadioKey === nodeid
}
const isCheckedByRadioNode = (node: any) => {
return isCheckedByRadioNodeId(getNodeId(node))
}
const isCheckedByCheckboxNodeId = (nodeid: any) => {
const { updateCheckboxFlag } = reactData
const { selectCheckboxMaps } = internalData
return !!(updateCheckboxFlag && selectCheckboxMaps[nodeid])
}
const isCheckedByCheckboxNode = (node: any) => {
return isCheckedByCheckboxNodeId(getNodeId(node))
}
const isIndeterminateByCheckboxNodeid = (nodeid: any) => {
const { updateCheckboxFlag } = reactData
const { indeterminateRowMaps } = internalData
return !!(updateCheckboxFlag && indeterminateRowMaps[nodeid])
}
const isIndeterminateByCheckboxNode = (node: any) => {
return isIndeterminateByCheckboxNodeid(getNodeId(node))
}
const emitCheckboxMode = (value: VxeTreePropTypes.CheckNodeKeys) => {
emit('update:checkNodeKeys', value)
}
const emitRadioMode = (value: VxeTreePropTypes.CheckNodeKey) => {
emit('update:checkNodeKey', value)
}
const handleSetCheckboxByNodeId = (nodeKeys: any | any[], checked: boolean) => {
const { nodeMaps } = internalData
if (nodeKeys) {
if (!XEUtils.isArray(nodeKeys)) {
nodeKeys = [nodeKeys]
}
const nodeList: any[] = []
nodeKeys.forEach((nodeKey: string) => {
const nodeid = enNodeValue(nodeKey)
const nodeItem = nodeMaps[nodeid]
if (nodeItem) {
nodeList.push(nodeItem.item)
}
})
handleCheckedCheckboxNode(nodeList, checked)
}
return nextTick()
}
const handleCheckedCheckboxNode = (nodeList: any[], checked: boolean) => {
const { transform } = props
const { selectCheckboxMaps } = internalData
const mapChildrenField = computeMapChildrenField.value
const childrenField = computeChildrenField.value
const checkboxOpts = computeCheckboxOpts.value
const { checkStrictly } = checkboxOpts
const handleSelect = (node: any) => {
const nodeid = getNodeId(node)
if (checked) {
if (!selectCheckboxMaps[nodeid]) {
selectCheckboxMaps[nodeid] = node
}
} else {
if (selectCheckboxMaps[nodeid]) {
delete selectCheckboxMaps[nodeid]
}
}
}
if (checkStrictly) {
nodeList.forEach(handleSelect)
} else {
XEUtils.eachTree(nodeList, handleSelect, { children: transform ? mapChildrenField : childrenField })
}
reactData.updateCheckboxFlag++
updateCheckboxStatus()
}
const updateCheckboxChecked = (nodeKeys: VxeTreePropTypes.CheckNodeKeys) => {
internalData.selectCheckboxMaps = {}
handleSetCheckboxByNodeId(nodeKeys, true)
}
const handleSetExpand = (nodeid: string | number, expanded: boolean, expandedMaps: Record<string, boolean>) => {
if (expanded) {
if (!expandedMaps[nodeid]) {
expandedMaps[nodeid] = true
}
} else {
if (expandedMaps[nodeid]) {
delete expandedMaps[nodeid]
}
}
}
const dispatchEvent = (type: ValueOf<VxeTreeEmits>, params: Record<string, any>, evnt: Event | null) => {
emit(type, createEvent(evnt, { $tree: $xeTree }, params))
}
const getParentElem = () => {
const el = refElem.value
return el ? el.parentElement : null
}
const calcTreeHeight = (key: 'height' | 'minHeight' | 'maxHeight') => {
const { parentHeight } = reactData
const val = props[key]
let num = 0
if (val) {
if (val === '100%' || val === 'auto') {
num = parentHeight
} else {
if (isScale(val)) {
num = Math.floor((XEUtils.toInteger(val) || 1) / 100 * parentHeight)
} else {
num = XEUtils.toNumber(val)
}
num = Math.max(40, num)
}
}
return num
}
const updateHeight = () => {
reactData.customHeight = calcTreeHeight('height')
reactData.customMinHeight = calcTreeHeight('minHeight')
reactData.customMaxHeight = calcTreeHeight('maxHeight')
// 如果启用虚拟滚动,默认高度
if (reactData.scrollYLoad && !(reactData.customHeight || reactData.customMinHeight)) {
reactData.customHeight = 300
}
}
const createNode = (records: any[]) => {
const valueField = computeValueField.value
return Promise.resolve(
records.map(obj => {
const item = { ...obj }
let nodeid = getNodeId(item)
if (!nodeid) {
nodeid = getNodeUniqueId()
XEUtils.set(item, valueField, nodeid)
}
return item
})
)
}
const cacheNodeMap = () => {
const { treeFullData } = internalData
const valueField = computeValueField.value
const childrenField = computeChildrenField.value
const keyMaps: Record<string, VxeTreeDefines.NodeCacheItem> = {}
XEUtils.eachTree(treeFullData, (item, index, items, path, parent, nodes) => {
let nodeid = getNodeId(item)
if (!nodeid) {
nodeid = getNodeUniqueId()
XEUtils.set(item, valueField, nodeid)
}
keyMaps[nodeid] = {
item,
index,
$index: -1,
_index: -1,
items,
parent,
nodes,
level: nodes.length - 1,
treeIndex: index,
lineCount: 0,
treeLoaded: false
}
}, { children: childrenField })
internalData.nodeMaps = keyMaps
}
const updateAfterDataIndex = () => {
const { transform } = props
const { afterTreeList, nodeMaps } = internalData
const childrenField = computeChildrenField.value
const mapChildrenField = computeMapChildrenField.value
let vtIndex = 0
XEUtils.eachTree(afterTreeList, (item, index, items) => {
const nodeid = getNodeId(item)
const nodeItem = nodeMaps[nodeid]
if (nodeItem) {
nodeItem.items = items
nodeItem.treeIndex = index
nodeItem._index = vtIndex
} else {
const rest = {
item,
index,
$index: -1,
_index: vtIndex,
items,
parent,
nodes: [],
level: 0,
treeIndex: index,
lineCount: 0,
treeLoaded: false
}
nodeMaps[nodeid] = rest
}
vtIndex++
}, { children: transform ? mapChildrenField : childrenField })
}
const updateAfterFullData = () => {
const { transform, filterValue } = props
const { treeFullData, lastFilterValue } = internalData
const titleField = computeTitleField.value
const childrenField = computeChildrenField.value
const mapChildrenField = computeMapChildrenField.value
const filterOpts = computeFilterOpts.value
const { autoExpandAll, beforeFilterMethod, filterMethod, afterFilterMethod } = filterOpts
let fullList = treeFullData
let treeList = fullList
let filterStr = ''
if (filterValue || filterValue === 0) {
filterStr = `${filterValue}`
const handleSearch = filterMethod
? (item: any) => {
return filterMethod({
$tree: $xeTree,
node: item,
filterValue: filterStr
})
}
: (item: any) => {
return String(item[titleField]).toLowerCase().indexOf(filterStr.toLowerCase()) > -1
}
const bafParams = { $tree: $xeTree, filterValue: filterStr }
if (beforeFilterMethod) {
beforeFilterMethod(bafParams)
}
if (transform) {
treeList = XEUtils.searchTree(treeFullData, handleSearch, {
original: true,
isEvery: true,
children: childrenField,
mapChildren: mapChildrenField
})
fullList = treeList
} else {
fullList = treeFullData.filter(handleSearch)
}
internalData.lastFilterValue = filterStr
nextTick(() => {
// 筛选时自动展开
if (autoExpandAll) {
$xeTree.setAllExpandNode(true).then(() => {
if (afterFilterMethod) {
afterFilterMethod(bafParams)
}
})
} else {
if (afterFilterMethod) {
afterFilterMethod(bafParams)
}
}
})
} else {
if (transform) {
treeList = XEUtils.searchTree(treeFullData, () => true, {
original: true,
isEvery: true,
children: childrenField,
mapChildren: mapChildrenField
})
fullList = treeList
if (lastFilterValue) {
const bafParams = { $tree: $xeTree, filterValue: filterStr }
if (beforeFilterMethod) {
beforeFilterMethod(bafParams)
}
// 取消筛选时自动收起
nextTick(() => {
if (autoExpandAll) {
$xeTree.clearAllExpandNode().then(() => {
if (afterFilterMethod) {
afterFilterMethod(bafParams)
}
})
} else {
if (afterFilterMethod) {
afterFilterMethod(bafParams)
}
}
})
}
}
internalData.lastFilterValue = ''
}
internalData.afterVisibleList = fullList
internalData.afterTreeList = treeList
updateAfterDataIndex()
}
/**
* 如果为虚拟树、则将树结构拍平
*/
const handleTreeToList = () => {
const { transform } = props
const { afterTreeList, treeExpandedMaps } = internalData
const mapChildrenField = computeMapChildrenField.value
const expandMaps: {
[key: string]: number
} = {}
if (transform) {
const fullData: any[] = []
XEUtils.eachTree(afterTreeList, (item, index, items, path, parentRow) => {
const nodeid = getNodeId(item)
const parentNodeid = getNodeId(parentRow)
if (!parentRow || (expandMaps[parentNodeid] && treeExpandedMaps[parentNodeid])) {
expandMaps[nodeid] = 1
fullData.push(item)
}
}, { children: mapChildrenField })
updateScrollYStatus(fullData)
internalData.afterVisibleList = fullData
return fullData
}
return internalData.afterVisibleList
}
const handleData = (force?: boolean) => {
const { scrollYLoad } = reactData
const { scrollYStore, nodeMaps } = internalData
let fullList: any[] = internalData.afterVisibleList
if (force) {
// 更新数据,处理筛选和排序
updateAfterFullData()
// 如果为虚拟树,将树结构拍平
fullList = handleTreeToList()
}
const treeList = scrollYLoad ? fullList.slice(scrollYStore.startIndex, scrollYStore.endIndex) : fullList.slice(0)
treeList.forEach((item, $index) => {
const nodeid = getNodeId(item)
const itemRest = nodeMaps[nodeid]
if (itemRest) {
itemRest.$index = $index
}
})
reactData.treeList = treeList
}
const triggerSearchEvent = XEUtils.debounce(() => handleData(true), 350, { trailing: true })
const loadData = (list: any[]) => {
const { expandAll, expandNodeKeys, transform } = props
const { initialized, scrollYStore } = internalData
const keyField = computeKeyField.value
const parentField = computeParentField.value
const childrenField = computeChildrenField.value
const fullData = transform ? XEUtils.toArrayTree(list, { key: keyField, parentKey: parentField, mapChildren: childrenField }) : list ? list.slice(0) : []
internalData.treeFullData = fullData
Object.assign(scrollYStore, {
startIndex: 0,
endIndex: 1,
visibleSize: 0
})
const sYLoad = updateScrollYStatus(fullData)
cacheNodeMap()
handleData(true)
if (sYLoad) {
if (!(props.height || props.maxHeight)) {
errLog('vxe.error.reqProp', ['[tree] height | max-height | virtual-y-config.enabled=false'])
}
}
return computeScrollLoad().then(() => {
if (!initialized) {
if (list && list.length) {
internalData.initialized = true
if (expandAll) {
$xeTree.setAllExpandNode(true)
} else if (expandNodeKeys && expandNodeKeys.length) {
$xeTree.setExpandByNodeId(expandNodeKeys, true)
}
handleSetCheckboxByNodeId(props.checkNodeKeys || [], true)
}
}
updateHeight()
refreshScroll()
})
}
const updateScrollYStatus = (fullData?: any[]) => {
const { transform } = props
const virtualYOpts = computeVirtualYOpts.value
const allList = fullData || internalData.treeFullData
// 如果gt为0,则总是启用
const scrollYLoad = !!transform && !!virtualYOpts.enabled && virtualYOpts.gt > -1 && (virtualYOpts.gt === 0 || virtualYOpts.gt < allList.length)
reactData.scrollYLoad = scrollYLoad
return scrollYLoad
}
const updateYSpace = () => {
const { scrollYLoad } = reactData
const { scrollYStore, afterVisibleList } = internalData
reactData.bodyHeight = scrollYLoad ? afterVisibleList.length * scrollYStore.rowHeight : 0
reactData.topSpaceHeight = scrollYLoad ? Math.max(scrollYStore.startIndex * scrollYStore.rowHeight, 0) : 0
}
const updateYData = () => {
handleData()
updateYSpace()
}
const computeScrollLoad = () => {
return nextTick().then(() => {
const { scrollYLoad } = reactData
const { scrollYStore } = internalData
const virtualBodyElem = refVirtualBody.value
const virtualYOpts = computeVirtualYOpts.value
let rowHeight = 0
let firstItemElem: HTMLElement | undefined
if (virtualBodyElem) {
if (!firstItemElem) {
firstItemElem = virtualBodyElem.children[0] as HTMLElement
}
}
if (firstItemElem) {
rowHeight = firstItemElem.offsetHeight
}
rowHeight = Math.max(20, rowHeight)
scrollYStore.rowHeight = rowHeight
// 计算 Y 逻辑
if (scrollYLoad) {
const scrollBodyElem = refVirtualWrapper.value
const visibleYSize = Math.max(8, scrollBodyElem ? Math.ceil(scrollBodyElem.clientHeight / rowHeight) : 0)
const offsetYSize = Math.max(0, Math.min(2, XEUtils.toNumber(virtualYOpts.oSize)))
scrollYStore.offsetSize = offsetYSize
scrollYStore.visibleSize = visibleYSize
scrollYStore.endIndex = Math.max(scrollYStore.startIndex, visibleYSize + offsetYSize, scrollYStore.endIndex)
updateYData()
} else {
updateYSpace()
}
})
}
/**
* 如果有滚动条,则滚动到对应的位置
*/
const handleScrollTo = (scrollLeft: { top?: number | null; left?: number | null; } | number | null | undefined, scrollTop?: number | null) => {
const scrollBodyElem = refVirtualWrapper.value
if (scrollLeft) {
if (!XEUtils.isNumber(scrollLeft)) {
scrollTop = scrollLeft.top
scrollLeft = scrollLeft.left
}
}
if (scrollBodyElem) {
if (XEUtils.isNumber(scrollLeft)) {
scrollBodyElem.scrollLeft = scrollLeft
}
if (XEUtils.isNumber(scrollTop)) {
scrollBodyElem.scrollTop = scrollTop
}
}
if (reactData.scrollYLoad) {
return new Promise<void>(resolve => {
setTimeout(() => {
nextTick(() => {
resolve()
})
}, 50)
})
}
return nextTick()
}
/**
* 刷新滚动条
*/
const refreshScroll = () => {
const { lastScrollLeft, lastScrollTop } = internalData
return clearScroll().then(() => {
if (lastScrollLeft || lastScrollTop) {
internalData.lastScrollLeft = 0
internalData.lastScrollTop = 0
return scrollTo(lastScrollLeft, lastScrollTop)
}
})
}
/**
* 重新计算列表
*/
const recalculate = () => {
const { scrollYStore } = internalData
const { rowHeight } = scrollYStore
const el = refElem.value
if (el && el.clientWidth && el.clientHeight) {
const parentEl = getParentElem()
const headerWrapperEl = refHeaderWrapperElem.value
const footerWrapperEl = refFooterWrapperElem.value
const headHeight = headerWrapperEl ? headerWrapperEl.clientHeight : 0
const footHeight = footerWrapperEl ? footerWrapperEl.clientHeight : 0
if (parentEl) {
const parentPaddingSize = getPaddingTopBottomSize(parentEl)
reactData.parentHeight = Math.max(headHeight + footHeight + rowHeight, parentEl.clientHeight - parentPaddingSize - headHeight - footHeight)
}
updateHeight()
return computeScrollLoad().then(() => {
updateHeight()
updateYSpace()
})
}
return nextTick()
}
const loadYData = () => {
const { scrollYStore } = internalData
const { startIndex, endIndex, visibleSize, offsetSize, rowHeight } = scrollYStore
const scrollBodyElem = refVirtualWrapper.value
if (!scrollBodyElem) {
return
}
const scrollTop = scrollBodyElem.scrollTop
const toVisibleIndex = Math.floor(scrollTop / rowHeight)
const offsetStartIndex = Math.max(0, toVisibleIndex - 1 - offsetSize)
const offsetEndIndex = toVisibleIndex + visibleSize + offsetSize
if (toVisibleIndex <= startIndex || toVisibleIndex >= endIndex - visibleSize - 1) {
if (startIndex !== offsetStartIndex || endIndex !== offsetEndIndex) {
scrollYStore.startIndex = offsetStartIndex
scrollYStore.endIndex = offsetEndIndex
updateYData()
}
}
}
const scrollEvent = (evnt: Event) => {
const scrollBodyElem = evnt.target as HTMLDivElement
const scrollTop = scrollBodyElem.scrollTop
const scrollLeft = scrollBodyElem.scrollLeft
const isX = scrollLeft !== internalData.lastScrollLeft
const isY = scrollTop !== internalData.lastScrollTop
internalData.lastScrollTop = scrollTop
internalData.lastScrollLeft = scrollLeft
if (reactData.scrollYLoad) {
loadYData()
}
internalData.lastScrollTime = Date.now()
dispatchEvent('scroll', { scrollLeft, scrollTop, isX, isY }, evnt)
}
const clearScroll = () => {
const scrollBodyElem = refVirtualWrapper.value
if (scrollBodyElem) {
scrollBodyElem.scrollTop = 0
scrollBodyElem.scrollLeft = 0
}
internalData.lastScrollTop = 0
internalData.lastScrollLeft = 0
return nextTick()
}
const handleNodeMousedownEvent = (evnt: MouseEvent, node: any) => {
const { drag } = props
const { nodeMaps } = internalData
const targetEl = evnt.currentTarget
const dragConfig = computeDragOpts.value
const { trigger, isCrossDrag, isPeerDrag, disabledMethod } = dragConfig
const nodeid = getNodeId(node)
const triggerTreeNode = getEventTargetNode(evnt, targetEl, 'vxe-tree--node-item-switcher').flag
let isNodeDrag = false
if (drag) {
isNodeDrag = trigger === 'node'
}
if (!triggerTreeNode) {
const params = { node, $tree: $xeTree }
const itemRest = nodeMaps[nodeid]
if (isNodeDrag && (isCrossDrag || isPeerDrag || (itemRest && !itemRest.level)) && !(disabledMethod && disabledMethod(params))) {
handleNodeDragMousedownEvent(evnt, { node })
}
}
}
const handleNodeClickEvent = (evnt: MouseEvent, node: any) => {
const { showRadio, showCheckbox, trigger } = props
const radioOpts = computeRadioOpts.value
const checkboxOpts = computeCheckboxOpts.value
const isRowCurrent = computeIsRowCurrent.value
let triggerCurrent = false
let triggerRadio = false
let triggerCheckbox = false
let triggerExpand = false
if (isRowCurrent) {
triggerCurrent = true
changeCurrentEvent(evnt, node)
} else if (reactData.currentNode) {
reactData.currentNode = null
}
if (trigger === 'node') {
triggerExpand = true
toggleExpandEvent(evnt, node)
}
if (showRadio && radioOpts.trigger === 'node') {
triggerRadio = true
changeRadioEvent(evnt, node)
}
if (showCheckbox && checkboxOpts.trigger === 'node') {
triggerCheckbox = true
changeCheckboxEvent(evnt, node)
}
dispatchEvent('node-click', { node, triggerCurrent, triggerRadio, triggerCheckbox, triggerExpand }, evnt)
}
const handleNodeDblclickEvent = (evnt: MouseEvent, node: any) => {
dispatchEvent('node-dblclick', { node }, evnt)
}
const handleContextmenuEvent = (evnt: MouseEvent, node: any) => {
const { menuConfig } = props
const isRowCurrent = computeIsRowCurrent.value
const menuOpts = computeMenuOpts.value
if (menuConfig ? isEnableConf(menuOpts) : menuOpts.enabled) {
const { options, visibleMethod } = menuOpts
if (!visibleMethod || visibleMethod({ $tree: $xeTree, options, node })) {
if (isRowCurrent) {
changeCurrentEvent(evnt, node)
} else if (reactData.currentNode) {
reactData.currentNode = null
}
if (VxeUI.contextMenu) {
VxeUI.contextMenu.openByEvent(evnt, {
options,
events: {
optionClick (eventParams) {
const { option } = eventParams
const gMenuOpts = menus.get(option.code)
const tmMethod = gMenuOpts ? gMenuOpts.treeMenuMethod : null
const params = {
menu: option,
node,
$event: evnt,
$tree: $xeTree,
/**
* @@deprecated
*/
option
}
if (tmMethod) {
tmMethod(params, evnt)
}
dispatchEvent('menu-click', params, eventParams.$event)
}
}
})
}
}
}
dispatchEvent('node-menu', { node }, evnt)
}
const handleAsyncTreeExpandChilds = (node: any) => {
const checkboxOpts = computeCheckboxOpts.value
const { loadMethod } = props
const { checkStrictly } = checkboxOpts
return new Promise<void>(resolve => {
if (loadMethod) {
const { nodeMaps } = internalData
const nodeid = getNodeId(node)
const nodeItem = nodeMaps[nodeid]
internalData.treeExpandLazyLoadedMaps[nodeid] = true
Promise.resolve(
loadMethod({ $tree: $xeTree, node })
).then((childRecords: any) => {
const { treeExpandLazyLoadedMaps } = internalData
nodeItem.treeLoaded = true
if (treeExpandLazyLoadedMaps[nodeid]) {
treeExpandLazyLoadedMaps[nodeid] = false
}
if (!XEUtils.isArray(childRecords)) {
childRecords = []
}
if (childRecords) {
return $xeTree.loadChildrenNode(node, childRecords).then(childRows => {
const { treeExpandedMaps } = internalData
if (childRows.length && !treeExpandedMaps[nodeid]) {
treeExpandedMaps[nodeid] = true
}
reactData.updateExpandedFlag++
// 如果当前节点已选中,则展开后子节点也被选中
if (!checkStrictly && $xeTree.isCheckedByCheckboxNodeId(nodeid)) {
handleCheckedCheckboxNode(childRows, true)
}
dispatchEvent('load-success', { node, data: childRecords }, new Event('load-success'))
return nextTick()
})
} else {
dispatchEvent('load-success', { node, data: childRecords }, new Event('load-success'))
}
}).catch((e) => {
const { treeExpandLazyLoadedMaps } = internalData
nodeItem.treeLoaded = false
if (treeExpandLazyLoadedMaps[nodeid]) {
treeExpandLazyLoadedMaps[nodeid] = false
}
dispatchEvent('load-error', { node, data: e }, new Event('load-error'))
}).finally(() => {
handleTreeToList()
handleData()
return recalculate()
})
} else {
resolve()
}
})
}
/**
* 展开与收起树节点
* @param nodeList
* @param expanded
* @returns
*/
const handleBaseTreeExpand = (nodeList: any[], expanded: boolean) => {
const { lazy, accordion, toggleMethod } = props
const { treeExpandLazyLoadedMaps, treeExpandedMaps } = internalData
const { nodeMaps } = internalData
const childrenField = computeChildrenField.value
const hasChildField = computeHasChildField.value
const result: any[] = []
let validNodes = toggleMethod ? nodeList.filter((node: any) => toggleMethod({ $tree: $xeTree, expanded, node })) : nodeList
if (accordion) {
validNodes = validNodes.length ? [validNodes[validNodes.length - 1]] : []
// 同一级只能展开一个
const nodeid = getNodeId(validNodes[0])
const nodeItem = nodeMaps[nodeid]
if (nodeItem) {
nodeItem.items.forEach(item => {
const itemNodeId = getNodeId(item)
if (treeExpandedMaps[itemNodeId]) {
delete treeExpandedMaps[itemNodeId]
}
})
}
}
const expandNodes: any[] = []
if (expanded) {
validNodes.forEach((item) => {
const itemNodeId = getNodeId(item)
if (!treeExpandedMaps[itemNodeId]) {
const nodeItem = nodeMaps[itemNodeId]
const isLoad = lazy && item[hasChildField] && !nodeItem.treeLoaded && !treeExpandLazyLoadedMaps[itemNodeId]
// 是否使用懒加载
if (isLoad) {
result.push(handleAsyncTreeExpandChilds(item))
} else {
if (item[childrenField] && item[childrenField].length) {
treeExpandedMaps[itemNodeId] = true
expandNodes.push(item)
}
}
}
})
} else {
validNodes.forEach(item => {
const itemNodeId = getNodeId(item)
if (treeExpandedMaps[itemNodeId]) {
delete treeExpandedMaps[itemNodeId]
expandNodes.push(item)
}
})
}
reactData.updateExpandedFlag++
handleTreeToList()
handleData()
return Promise.all(result).then(() => recalculate())
}
const toggleExpandEvent = (evnt: MouseEvent, node: any) => {
const { lazy } = props
const { treeExpandedMaps, treeExpandLazyLoadedMaps } = internalData
const nodeid = getNodeId(node)
const expanded = !treeExpandedMaps[nodeid]
evnt.stopPropagation()
if (!lazy || !treeExpandLazyLoadedMaps[nodeid]) {
handleBaseTreeExpand([node], expanded)
}
dispatchEvent('node-expand', { node, expanded }, evnt)
}
const updateCheckboxStatus = () => {
const { transform } = props
const { selectCheckboxMaps, indeterminateRowMaps, afterTreeList } = internalData
const childrenField = computeChildrenField.value
const mapChildrenField = computeMapChildrenField.value
const checkboxOpts = computeCheckboxOpts.value
const { checkStrictly, checkMethod } = checkboxOpts
if (!checkStrictly) {
const childRowMaps: Record<string, number> = {}
const childRowList: any[][] = []
XEUtils.eachTree(afterTreeList, (node) => {
const nodeid = getNodeId(node)
const childList = node[childrenField]
if (childList && childList.length && !childRowMaps[nodeid]) {
childRowMaps[nodeid] = 1
childRowList.unshift([node, nodeid, childList])
}
}, { children: transform ? mapChildrenField : childrenField })
childRowList.forEach(vals => {
const node: string = vals[0]
const nodeid: string = vals[1]
const childList: any[] = vals[2]
let sLen = 0 // 已选
let hLen = 0 // 半选
let vLen = 0 // 有效子行
const cLen = childList.length // 子行
childList.forEach(
checkMethod
? (item) => {
const childNodeid = getNodeId(item)
const isSelect = selectCheckboxMaps[childNodeid]
if (checkMethod({ $tree: $xeTree, node: item })) {
if (isSelect) {
sLen++
} else if (indeterminateRowMaps[childNodeid]) {
hLen++
}
vLen++
} else {
if (isSelect) {
sLen++
} else if (indeterminateRowMaps[childNodeid]) {
hLen++
}
}
}
: item => {
const childNodeid = getNodeId(item)
const isSelect = selectCheckboxMaps[childNodeid]
if (isSelect) {
sLen++
} else if (indeterminateRowMaps[childNodeid]) {
hLen++
}
vLen++
}
)
let isSelected = false
if (cLen > 0) {
if (vLen > 0) {
isSelected = (sLen > 0 || hLen > 0) && sLen >= vLen
} else {
// 如果存在子项禁用
if ((sLen > 0 && sLen >= vLen)) {
isSelected = true
} else if (selectCheckboxMaps[nodeid]) {
isSelected = true
} else {
isSelected = false
}
}
} else {
// 如果无子项
isSelected = selectCheckboxMaps[nodeid]
}
const halfSelect = !isSelected && (sLen > 0 || hLen > 0)
if (isSelected) {
selectCheckboxMaps[nodeid] = node
if (indeterminateRowMaps[nodeid]) {
delete indeterminateRowMaps[nodeid]
}
} else {
if (selectCheckboxMaps[nodeid]) {
delete selectCheckboxMaps[nodeid]
}
if (halfSelect) {
indeterminateRowMaps[nodeid] = node
} else {
if (indeterminateRowMaps[nodeid]) {
delete indeterminateRowMaps[nodeid]
}
}
}
})
reactData.updateCheckboxFlag++
}
}
const changeCheckboxEvent = (evnt: MouseEvent, node: any) => {
evnt.preventDefault()
evnt.stopPropagation()
const { transform } = props
const { selectCheckboxMaps } = internalData
const childrenField = computeChildrenField.value
const mapChildrenField = computeMapChildrenField.value
const checkboxOpts = computeCheckboxOpts.value
const { checkStrictly, checkMethod } = checkboxOpts
let isDisabled = !!checkMethod
if (checkMethod) {
isDisabled = !checkMethod({ $tree: $xeTree, node })
}
if (isDisabled) {
return
}
const nodeid = getNodeId(node)
let isChecked = false
if (selectCheckboxMaps[nodeid]) {
delete selectCheckboxMaps[nodeid]
} else {
isChecked = true
selectCheckboxMaps[nodeid] = node
}
if (!checkStrictly) {
XEUtils.eachTree(XEUtils.get(node, transform ? mapChildrenField : childrenField), (childNode) => {
const childNodeid = getNodeId(childNode)
if (isChecked) {
if (!selectCheckboxMaps[childNodeid]) {
selectCheckboxMaps[childNodeid] = true
}
} else {
if (selectCheckboxMaps[childNodeid]) {
delete selectCheckboxMaps[childNodeid]
}
}
}, { children: transform ? mapChildrenField : childrenField })
}
reactData.updateCheckboxFlag++
updateCheckboxStatus()
const nodeids = XEUtils.keys(selectCheckboxMaps)
const value = nodeids.map(deNodeValue)
emitCheckboxMode(value)
dispatchEvent('checkbox-change', { node, value, checked: isChecked }, evnt)
}
const changeCurrentEvent = (evnt: MouseEvent, node: any) => {
evnt.preventDefault()
const nodeOpts = computeNodeOpts.value
const { currentMethod, trigger } = nodeOpts
const childrenField = computeChildrenField.value
const childList: any[] = XEUtils.get(node, childrenField)
const hasChild = childList && childList.length
let isDisabled = !!currentMethod
if (trigger === 'child') {
if (hasChild) {
return
}
} else if (trigger === 'parent') {
if (!hasChild) {
return
}
}
if (currentMethod) {
isDisabled = !currentMethod({ node })
}
if (isDisabled) {
return
}
const isChecked = true
reactData.currentNode = node
dispatchEvent('current-change', { node, checked: isChecked }, evnt)
}
const changeRadioEvent = (evnt: MouseEvent, node: any) => {
evnt.preventDefault()
evnt.stopPropagation()
const radioOpts = computeRadioOpts.value
const { checkMethod } = radioOpts
let isDisabled = !!checkMethod
if (checkMethod) {
isDisabled = !checkMethod({ $tree: $xeTree, node })
}
if (isDisabled) {
return
}
const isChecked = true
const nodeid = getNodeId(node)
const value = deNodeValue(nodeid)
reactData.selectRadioKey = nodeid
emitRadioMode(value)
dispatchEvent('radio-change', { node, value, checked: isChecked }, evnt)
}
const handleGlobalResizeEvent = () => {
const el = refElem.value
if (!el || !el.clientWidth) {
return
}
recalculate()
}
const treeMethods: TreeMethods = {
dispatchEvent,
getNodeId,
getNodeById (nodeid) {
const { nodeMaps } = internalData
if (nodeid) {
const nodeItem = nodeMaps[nodeid]
if (nodeItem) {
return nodeItem.item
}
}
return null
},
loadData (data) {
return loadData(data || [])
},
reloadData (data) {
return loadData(data || [])
},
clearCurrentNode () {
reactData.currentNode = null
return nextTick()
},
getCurrentNodeId () {
const { currentNode } = reactData
if (currentNode) {
return deNodeValue(getNodeId(currentNode))
}
return null
},
getCurrentNode () {
const { currentNode } = reactData
const { nodeMaps } = internalData
if (currentNode) {
const nodeItem = nodeMaps[getNodeId(currentNode)]
if (nodeItem) {
return nodeItem.item
}
}
return null
},
setCurrentNodeId (nodeKey) {
const { nodeMaps } = internalData
const nodeItem = nodeMaps[enNodeValue(nodeKey)]
reactData.currentNode = nodeItem ? nodeItem.item : null
return nextTick()
},
setCurrentNode (node) {
reactData.currentNode = node
return nextTick()
},
clearRadioNode () {
reactData.selectRadioKey = null
emitRadioMode(null)
return nextTick()
},
getRadioNodeId () {
return reactData.selectRadioKey || null
},
getRadioNode () {
const { selectRadioKey } = reactData
const { nodeMaps } = internalData
if (selectRadioKey) {
const nodeItem = nodeMaps[selectRadioKey]
if (nodeItem) {
return nodeItem.item
}
}
return null
},
setRadioNodeId (nodeKey) {
rea