vue-tree-kl
Version:
A vue tree component using virtual list.
172 lines (160 loc) • 5.41 kB
text/typescript
import { ref } from "vue"
import { TreeNode } from ".."
import { INonReactiveData, TreeNodeKeyType } from "../types"
import { VerticalPositionType, verticalPositionEnum } from "../constants"
import { TreeProps } from "../components/Tree.vue"
type IUseVirtualListProps = Required<Pick<TreeProps,
'renderNodeAmount' |
'nodeMinHeight' |
'bufferNodeAmount' |
'keyField'
>>
export const useVirtualList = (nonReactive: INonReactiveData, props: IUseVirtualListProps) => {
const scrollArea = ref()
const renderNodes = ref<TreeNode[]>([])
const blockLength = ref(0)
const blockAreaHeight = ref(0)
const topSpaceHeight = ref(0)
const bottomSpaceHeight = ref(0)
const renderAmount = ref(0)
const renderAmountCache = ref(0)
const renderStart = ref(0)
const renderStartCache = ref(0)
const debounceTimer = ref<number | undefined>(undefined)
/**
* 重置空白与滚动高度
*/
const resetSpaceHeights = (): void => {
topSpaceHeight.value = 0
bottomSpaceHeight.value = 0
if (scrollArea.value) scrollArea.value.scrollTop = 0
}
/**
* 计算需要渲染的节点的数量,只要容器高度(clientHeight)不变,这个数量一般就不会变
*/
const updateRenderAmount = (): void => {
const clientHeight = scrollArea.value.clientHeight
renderAmount.value = Math.max(
props.renderNodeAmount,
Math.ceil(clientHeight / props.nodeMinHeight) + props.bufferNodeAmount
)
}
/**
* 计算渲染的节点,基于 scrollTop 计算当前应该渲染哪些节点
*/
const updateRenderNodes = (isScroll: boolean = false): void => {
if (blockLength.value > renderAmount.value) {
const scrollTop = Math.max(scrollArea.value.scrollTop, 0)
/** 当前滚动了多少节点 */
const scrollNodeAmount = Math.floor(scrollTop / props.nodeMinHeight)
renderStart.value =
Math.floor(scrollNodeAmount / props.bufferNodeAmount) *
props.bufferNodeAmount
} else {
renderStart.value = 0
}
if (
isScroll &&
renderAmountCache.value === renderAmount.value &&
renderStartCache.value === renderStart.value
)
return
renderNodes.value = nonReactive.blockNodes
.slice(renderStart.value, renderStart.value + renderAmount.value)
.map(blockNode => {
return Object.assign({}, blockNode, {
_parent: null,
children: []
})
}) as TreeNode[]
topSpaceHeight.value = renderStart.value * props.nodeMinHeight
bottomSpaceHeight.value =
blockAreaHeight.value -
(topSpaceHeight.value + renderNodes.value.length * props.nodeMinHeight)
}
/**
* 计算渲染节点数量,并计算渲染节点
*/
const updateRender = (): void => {
updateRenderAmount()
updateRenderNodes()
}
/**
* 计算可见节点
*/
const updateBlockNodes = (): void => {
nonReactive.blockNodes = nonReactive.store.flatData.filter(
node => node.visible
)
updateBlockData()
updateRender()
}
/**
* 更新 block 数据相关信息
*/
const updateBlockData = (): void => {
blockLength.value = nonReactive.blockNodes.length
blockAreaHeight.value = props.nodeMinHeight * blockLength.value
}
const handleTreeScroll = (): void => {
if (debounceTimer.value) {
window.cancelAnimationFrame(debounceTimer.value)
}
renderAmountCache.value = renderAmount.value
renderStartCache.value = renderStart.value
debounceTimer.value = window.requestAnimationFrame(
updateRenderNodes.bind(null, true)
)
}
/**
* 滚动到指定节点位置
* @param key 要滚动的节点
* @param verticalPosition 滚动的垂直位置,可选为 'top', 'center', 'bottom' 或距离容器可视顶部距离的数字,默认为 'top'
*/
const scrollTo = (
key: TreeNodeKeyType,
verticalPosition: VerticalPositionType | number = verticalPositionEnum.top
): void => {
const node = nonReactive.store.mapData[key]
if (!node || !node.visible) return
let index: number = -1
for (let i = 0; i < blockLength.value; i++) {
if (nonReactive.blockNodes[i][props.keyField] === key) {
index = i
break
}
}
if (index === -1) return
let scrollTop = index * props.nodeMinHeight
if (verticalPosition === verticalPositionEnum.center) {
const clientHeight = scrollArea.value.clientHeight
scrollTop = scrollTop - (clientHeight - props.nodeMinHeight) / 2
} else if (verticalPosition === verticalPositionEnum.bottom) {
const clientHeight = scrollArea.value.clientHeight
scrollTop = scrollTop - (clientHeight - props.nodeMinHeight)
} else if (typeof verticalPosition === 'number') {
scrollTop = scrollTop - verticalPosition
}
if (scrollArea.value) scrollArea.value.scrollTop = scrollTop
}
return {
scrollArea,
renderNodes,
blockLength,
blockAreaHeight,
topSpaceHeight,
bottomSpaceHeight,
renderAmount,
renderAmountCache,
renderStart,
renderStartCache,
resetSpaceHeights,
updateRenderAmount,
updateRenderNodes,
updateRender,
updateBlockNodes,
updateBlockData,
handleTreeScroll,
scrollTo,
}
}