UNPKG

vxe-pc-ui

Version:
1,446 lines (1,354 loc) 98.5 kB
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