diff --git a/src/components/common/TreeExplorer.vue b/src/components/common/TreeExplorer.vue index d5e1309f6..c060c613e 100644 --- a/src/components/common/TreeExplorer.vue +++ b/src/components/common/TreeExplorer.vue @@ -124,39 +124,27 @@ const calculateNodePositions = ( const nodePositions = new Map() let currentPos = 0 - const traverse = (node: RenderedTreeExplorerNode): number => { - const nodeStart = currentPos - nodePositions.set(node.key, nodeStart) + const traverse = (node: RenderedTreeExplorerNode) => { + nodePositions.set(node.key, currentPos) currentPos += DEFAULT_NODE_HEIGHT if (node.children && !node.leaf && expandedKeys.value?.[node.key]) { - for (const child of node.children) { - currentPos = traverse(child) - } - } - - return currentPos - } - - if (root.children) { - for (const child of root.children) { - currentPos = traverse(child) + node.children.forEach(traverse) } } + root.children?.forEach(traverse) return nodePositions } const getFullNodeHeight = (node: RenderedTreeExplorerNode): number => { - let height = DEFAULT_NODE_HEIGHT - - if (node.children && !node.leaf && expandedKeys.value?.[node.key]) { - for (const child of node.children) { - height += getFullNodeHeight(child) - } + if (!node.children || node.leaf || !expandedKeys.value?.[node.key]) { + return DEFAULT_NODE_HEIGHT } - - return height + return ( + DEFAULT_NODE_HEIGHT + + node.children.reduce((sum, child) => sum + getFullNodeHeight(child), 0) + ) } const calculateNodeWindowRange = ( @@ -166,67 +154,68 @@ const calculateNodeWindowRange = ( scrollBottom: number, bufferHeight: number ): WindowRange | null => { - if (!node.children || node.leaf) return null - - const isExpanded = expandedKeys.value?.[node.key] ?? false - if (!isExpanded) return null + if ( + !node.children || + node.leaf || + !expandedKeys.value?.[node.key] || + node.children.length === 0 + ) { + return null + } const totalChildren = node.children.length - if (totalChildren === 0) return null - const nodeStart = nodePositions.get(node.key) ?? 0 const childrenStart = nodeStart + DEFAULT_NODE_HEIGHT - const lastChild = node.children[node.children.length - 1] + const lastChild = node.children[totalChildren - 1] const lastChildStart = nodePositions.get(lastChild.key) ?? childrenStart - const lastChildHeight = getFullNodeHeight(lastChild) - const childrenEnd = lastChildStart + lastChildHeight + const childrenEnd = lastChildStart + getFullNodeHeight(lastChild) - const isAboveView = childrenEnd < scrollTop - bufferHeight - const isBelowView = childrenStart > scrollBottom + bufferHeight + const scrollTopWithBuffer = scrollTop - bufferHeight + const scrollBottomWithBuffer = scrollBottom + bufferHeight - if (isAboveView) { - const endRow = Math.max(0, totalChildren - windowSize.value) - return { start: endRow, end: totalChildren } + // Quick checks for nodes outside viewport + if (childrenEnd < scrollTopWithBuffer) { + return { + start: Math.max(0, totalChildren - windowSize.value), + end: totalChildren + } } - if (isBelowView) { + if (childrenStart > scrollBottomWithBuffer) { return { start: 0, end: Math.min(windowSize.value, totalChildren) } } + // Find visible range by iterating children let startIndex = 0 let endIndex = totalChildren for (let i = 0; i < totalChildren; i++) { const child = node.children[i] const childStart = nodePositions.get(child.key) ?? 0 - const childHeight = getFullNodeHeight(child) - const childEnd = childStart + childHeight + const childEnd = childStart + getFullNodeHeight(child) - if (childEnd < scrollTop - bufferHeight) { + if (childEnd < scrollTopWithBuffer) { startIndex = i + 1 } - if (childStart <= scrollBottom + bufferHeight) { + if (childStart <= scrollBottomWithBuffer) { endIndex = i + 1 } } + // Apply buffer and ensure minimum window size startIndex = Math.max(0, startIndex - bufferRows.value) - endIndex = Math.min(totalChildren, endIndex + bufferRows.value) - - if (endIndex - startIndex < windowSize.value) { - endIndex = Math.min(totalChildren, startIndex + windowSize.value) - } + endIndex = Math.min( + totalChildren, + Math.max(endIndex + bufferRows.value, startIndex + windowSize.value) + ) return { start: startIndex, end: endIndex } } const getTreeNodeIcon = (node: TreeExplorerNode): string => { - if (node.getIcon) { - const icon = node.getIcon() - if (icon) return icon - } - if (node.icon) return node.icon + const customIcon = node.getIcon?.() ?? node.icon + if (customIcon) return customIcon if (node.leaf) return 'pi pi-file' @@ -260,11 +249,7 @@ const nodeKeyMap = computed>(() => { const map: Record = {} const buildMap = (node: RenderedTreeExplorerNode) => { map[node.key] = node - if (node.children) { - for (const child of node.children) { - buildMap(child) - } - } + node.children?.forEach(buildMap) } buildMap(renderedRoot.value) return map @@ -279,30 +264,33 @@ const mergeRanges = ( return { range: calculated, changed: true } } - const needsStartExpansion = calculated.start < existing.start - const hasExcessAtStart = - existing.start > 0 && calculated.start > existing.start + bufferRows.value - const needsEndExpansion = calculated.end > existing.end - const hasExcessAtEnd = - existing.end < totalChildren && - calculated.end < existing.end - bufferRows.value + const buffer = bufferRows.value + const maxWindow = windowSize.value * 2 - const updateStart = needsStartExpansion || hasExcessAtStart - const updateEnd = needsEndExpansion || hasExcessAtEnd + const updateStart = + calculated.start < existing.start || + (existing.start > 0 && calculated.start > existing.start + buffer) + const updateEnd = + calculated.end > existing.end || + (existing.end < totalChildren && calculated.end < existing.end - buffer) let newStart = updateStart ? calculated.start : existing.start let newEnd = updateEnd ? calculated.end : existing.end - if (newEnd - newStart > windowSize.value * 2) { - if (needsStartExpansion) { - newEnd = Math.min(totalChildren, newStart + windowSize.value * 2) + // Limit maximum window size + if (newEnd - newStart > maxWindow) { + if (updateStart) { + newEnd = Math.min(totalChildren, newStart + maxWindow) } else { - newStart = Math.max(0, newEnd - windowSize.value * 2) + newStart = Math.max(0, newEnd - maxWindow) } } const changed = - updateStart || updateEnd || newStart !== existing.start || newEnd !== existing.end + updateStart || + updateEnd || + newStart !== existing.start || + newEnd !== existing.end return { range: { start: newStart, end: newEnd }, changed } } @@ -316,16 +304,18 @@ const updateVisibleParentRanges = () => { const scrollBottom = scrollTop + containerHeight.value const bufferHeight = bufferRows.value * DEFAULT_NODE_HEIGHT const nodePositions = calculateNodePositions(renderedRoot.value) - const currentRanges = parentNodeWindowRanges.value const newRanges: Record = {} let hasChanges = false const processNode = (node: RenderedTreeExplorerNode) => { - if (!node.children || node.leaf) return - - const isExpanded = expandedKeys.value?.[node.key] ?? false - if (!isExpanded) return + if ( + !node.children || + node.leaf || + !expandedKeys.value?.[node.key] + ) { + return + } const calculated = calculateNodeWindowRange( node, @@ -336,41 +326,30 @@ const updateVisibleParentRanges = () => { ) if (calculated) { - const existing = currentRanges[node.key] const { range, changed } = mergeRanges( - existing, + currentRanges[node.key], calculated, node.children.length ) newRanges[node.key] = range - if (changed) { - hasChanges = true - } + if (changed) hasChanges = true } - for (const child of node.children) { - processNode(child) - } + node.children.forEach(processNode) } - for (const child of renderedRoot.value.children) { - processNode(child) - } + renderedRoot.value.children.forEach(processNode) - if (hasChanges || Object.keys(newRanges).length !== Object.keys(currentRanges).length) { + if ( + hasChanges || + Object.keys(newRanges).length !== Object.keys(currentRanges).length + ) { parentNodeWindowRanges.value = newRanges } } watch([scrollY, containerHeight], updateVisibleParentRanges, { immediate: true }) - -watch( - expandedKeys, - () => { - updateVisibleParentRanges() - }, - { deep: true } -) +watch(expandedKeys, updateVisibleParentRanges, { deep: true }) const displayRoot = computed(() => ({ ...renderedRoot.value, @@ -383,33 +362,31 @@ const calculateRealSpacerHeights = ( originalChildren: RenderedTreeExplorerNode[], range: WindowRange ): { topSpacer: number; bottomSpacer: number } => { - let topSpacer = 0 - let bottomSpacer = 0 + const topSpacer = originalChildren + .slice(0, range.start) + .reduce((sum, child) => sum + getFullNodeHeight(child), 0) - for (let i = 0; i < range.start; i++) { - topSpacer += getFullNodeHeight(originalChildren[i]) - } - - for (let i = range.end; i < originalChildren.length; i++) { - bottomSpacer += getFullNodeHeight(originalChildren[i]) - } + const bottomSpacer = originalChildren + .slice(range.end) + .reduce((sum, child) => sum + getFullNodeHeight(child), 0) return { topSpacer, bottomSpacer } } const getNodeChildrenStyle = (node: RenderedTreeExplorerNode) => { - if (!node || node.leaf) { - return { class: 'virtual-node-children' } + const baseStyle = { class: 'virtual-node-children' } + + if ( + !node || + node.leaf || + !expandedKeys.value?.[node.key] + ) { + return baseStyle } const originalNode = nodeKeyMap.value[node.key] - if (!originalNode?.children || originalNode.children.length === 0) { - return { class: 'virtual-node-children' } - } - - const isExpanded = expandedKeys.value?.[node.key] ?? false - if (!isExpanded) { - return { class: 'virtual-node-children' } + if (!originalNode?.children?.length) { + return baseStyle } const range = @@ -422,7 +399,7 @@ const getNodeChildrenStyle = (node: RenderedTreeExplorerNode) => { ) return { - class: 'virtual-node-children', + ...baseStyle, style: { '--top-spacer': `${topSpacer}px`, '--bottom-spacer': `${bottomSpacer}px` @@ -478,36 +455,29 @@ const handleNodeLabelEdit = async ( provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit) const { t } = useI18n() -const renameCommand = (node: RenderedTreeExplorerNode) => { - renameEditingNode.value = node -} -const deleteCommand = async (node: RenderedTreeExplorerNode) => { - await node.handleDelete?.() - emit('nodeDelete', node) -} -const menuItems = computed(() => - [ - getAddFolderMenuItem(menuTargetNode.value), +const menuItems = computed(() => { + const targetNode = menuTargetNode.value + if (!targetNode) return [] + + return [ + getAddFolderMenuItem(targetNode), { label: t('g.rename'), icon: 'pi pi-file-edit', command: () => { - if (menuTargetNode.value) { - renameCommand(menuTargetNode.value) - } + renameEditingNode.value = targetNode }, - visible: menuTargetNode.value?.handleRename !== undefined + visible: targetNode.handleRename !== undefined }, { label: t('g.delete'), icon: 'pi pi-trash', command: async () => { - if (menuTargetNode.value) { - await deleteCommand(menuTargetNode.value) - } + await targetNode.handleDelete?.() + emit('nodeDelete', targetNode) }, - visible: menuTargetNode.value?.handleDelete !== undefined, - isAsync: true // The delete command can be async + visible: targetNode.handleDelete !== undefined, + isAsync: true }, ...extraMenuItems.value ].map((menuItem: MenuItem) => ({ @@ -518,7 +488,7 @@ const menuItems = computed(() => }) : undefined })) -) +}) const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => { menuTargetNode.value = node