|
42 | 42 | <ContextMenu ref="menu" :model="menuItems" /> |
43 | 43 | </template> |
44 | 44 | <script setup lang="ts"> |
45 | | -import { useElementSize, useScroll, whenever } from '@vueuse/core' |
46 | | -import { clamp } from 'es-toolkit/compat' |
| 45 | +import { useElementSize, useScroll } from '@vueuse/core' |
47 | 46 | import ContextMenu from 'primevue/contextmenu' |
48 | 47 | import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem' |
49 | 48 | import Tree from 'primevue/tree' |
50 | | -import { computed, provide, ref, watch } from 'vue' |
| 49 | +import { computed, provide, ref } from 'vue' |
51 | 50 | import { useI18n } from 'vue-i18n' |
52 | 51 |
|
53 | 52 | import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue' |
@@ -98,125 +97,124 @@ const { |
98 | 97 | } |
99 | 98 | ) |
100 | 99 |
|
101 | | -const BUFFER_ROWS = 10 |
102 | 100 | const DEFAULT_NODE_HEIGHT = 32 |
103 | 101 | const SCROLL_THROTTLE = 64 |
104 | 102 |
|
105 | | -const parentWindowRanges = ref<Record<string, WindowRange>>({}) |
106 | 103 | const treeContainerRef = ref<HTMLDivElement | null>(null) |
107 | 104 | const menu = ref<InstanceType<typeof ContextMenu> | null>(null) |
108 | 105 | const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null) |
109 | 106 | const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null) |
110 | | -const bufferRowsRef = ref(BUFFER_ROWS) |
111 | 107 |
|
112 | 108 | const { height: containerHeight } = useElementSize(treeContainerRef) |
113 | 109 | const { y: scrollY } = useScroll(treeContainerRef, { |
114 | 110 | throttle: SCROLL_THROTTLE, |
115 | 111 | eventListenerOptions: { passive: true } |
116 | 112 | }) |
117 | 113 |
|
118 | | -// Reset window ranges when nodes are collapsed |
119 | | -watch( |
120 | | - expandedKeys, |
121 | | - (newKeys, oldKeys) => { |
122 | | - if (!oldKeys) return |
123 | | - for (const key in oldKeys) { |
124 | | - if (oldKeys[key] && !newKeys[key]) { |
125 | | - delete parentWindowRanges.value[key] |
| 114 | +// Computed values for window calculation |
| 115 | +const viewRows = computed(() => |
| 116 | + containerHeight.value |
| 117 | + ? Math.ceil(containerHeight.value / DEFAULT_NODE_HEIGHT) |
| 118 | + : 0 |
| 119 | +) |
| 120 | +const bufferRows = computed(() => Math.max(1, Math.floor(viewRows.value / 3))) |
| 121 | +const windowSize = computed(() => viewRows.value + bufferRows.value * 2) |
| 122 | +
|
| 123 | +// Compute window ranges for all nodes based on scroll position |
| 124 | +// Each node's window is calculated relative to its children list |
| 125 | +const parentWindowRanges = computed<Record<string, WindowRange>>(() => { |
| 126 | + if (!containerHeight.value || !renderedRoot.value.children) { |
| 127 | + return {} |
| 128 | + } |
| 129 | +
|
| 130 | + const ranges: Record<string, WindowRange> = {} |
| 131 | + const scrollTop = scrollY.value |
| 132 | + const scrollBottom = scrollTop + containerHeight.value |
| 133 | +
|
| 134 | + // Calculate cumulative positions for nodes in the tree |
| 135 | + const nodePositions = new Map<string, number>() |
| 136 | + let currentPos = 0 |
| 137 | +
|
| 138 | + const calculatePositions = (node: RenderedTreeExplorerNode): number => { |
| 139 | + const nodeStart = currentPos |
| 140 | + nodePositions.set(node.key, nodeStart) |
| 141 | + currentPos += DEFAULT_NODE_HEIGHT |
| 142 | +
|
| 143 | + if (node.children && !node.leaf && expandedKeys.value?.[node.key]) { |
| 144 | + for (const child of node.children) { |
| 145 | + currentPos = calculatePositions(child) |
126 | 146 | } |
127 | 147 | } |
128 | | - }, |
129 | | - { deep: true } |
130 | | -) |
131 | 148 |
|
132 | | -// Update windows for all nodes based on current scroll position |
133 | | -const updateWindows = () => { |
134 | | - if (!treeContainerRef.value || !containerHeight.value) return |
| 149 | + return currentPos |
| 150 | + } |
135 | 151 |
|
136 | | - const viewRows = Math.ceil(containerHeight.value / DEFAULT_NODE_HEIGHT) |
137 | | - const offsetRows = Math.floor(scrollY.value / DEFAULT_NODE_HEIGHT) |
138 | | - bufferRowsRef.value = viewRows / 3 |
| 152 | + for (const child of renderedRoot.value.children) { |
| 153 | + currentPos = calculatePositions(child) |
| 154 | + } |
139 | 155 |
|
140 | | - const updateNodeWindow = (node: RenderedTreeExplorerNode) => { |
| 156 | + // Compute windows for each node based on scroll position |
| 157 | + const computeNodeWindow = (node: RenderedTreeExplorerNode) => { |
141 | 158 | if (!node.children || node.leaf) return |
142 | 159 |
|
143 | 160 | const isExpanded = expandedKeys.value?.[node.key] ?? false |
144 | | - if (!isExpanded) { |
145 | | - delete parentWindowRanges.value[node.key] |
146 | | - return |
147 | | - } |
| 161 | + if (!isExpanded) return |
| 162 | +
|
| 163 | + const nodeStart = nodePositions.get(node.key) ?? 0 |
| 164 | + const childrenStart = nodeStart + DEFAULT_NODE_HEIGHT |
| 165 | + const childrenEnd = |
| 166 | + childrenStart + node.children.length * DEFAULT_NODE_HEIGHT |
| 167 | +
|
| 168 | + // Check if this node's children are in the visible range |
| 169 | + const isVisible = |
| 170 | + childrenEnd >= scrollTop - bufferRows.value * DEFAULT_NODE_HEIGHT && |
| 171 | + childrenStart <= scrollBottom + bufferRows.value * DEFAULT_NODE_HEIGHT |
148 | 172 |
|
149 | 173 | const totalChildren = node.children.length |
150 | | - const currentRange = parentWindowRanges.value[node.key] |
151 | | -
|
152 | | - if (currentRange) { |
153 | | - const fromRow = Math.max(0, offsetRows - bufferRowsRef.value) |
154 | | - const toRow = offsetRows + bufferRowsRef.value + viewRows |
155 | | - const newStart = clamp(fromRow, 0, totalChildren) |
156 | | - const newEnd = clamp(toRow, newStart, totalChildren) |
157 | | -
|
158 | | - if ( |
159 | | - Math.abs(currentRange.start - newStart) > bufferRowsRef.value || |
160 | | - Math.abs(currentRange.end - newEnd) > bufferRowsRef.value |
161 | | - ) { |
162 | | - parentWindowRanges.value[node.key] = { |
163 | | - start: newStart, |
164 | | - end: newEnd |
165 | | - } |
| 174 | +
|
| 175 | + if (isVisible && totalChildren > 0) { |
| 176 | + // Calculate which children should be visible based on scroll position |
| 177 | + const relativeScrollTop = Math.max(0, scrollTop - childrenStart) |
| 178 | + const relativeScrollBottom = Math.max(0, scrollBottom - childrenStart) |
| 179 | +
|
| 180 | + const fromRow = Math.max( |
| 181 | + 0, |
| 182 | + Math.floor(relativeScrollTop / DEFAULT_NODE_HEIGHT) - bufferRows.value |
| 183 | + ) |
| 184 | + const toRow = Math.min( |
| 185 | + totalChildren, |
| 186 | + Math.ceil(relativeScrollBottom / DEFAULT_NODE_HEIGHT) + bufferRows.value |
| 187 | + ) |
| 188 | +
|
| 189 | + ranges[node.key] = { |
| 190 | + start: Math.max(0, fromRow), |
| 191 | + end: Math.min( |
| 192 | + totalChildren, |
| 193 | + Math.max(fromRow + windowSize.value, toRow) |
| 194 | + ) |
166 | 195 | } |
167 | 196 | } else { |
168 | | - const windowSize = viewRows + bufferRowsRef.value * 2 |
169 | | - parentWindowRanges.value[node.key] = createInitialWindowRange( |
| 197 | + // Node is outside visible range, use minimal window |
| 198 | + ranges[node.key] = createInitialWindowRange( |
170 | 199 | totalChildren, |
171 | | - windowSize |
| 200 | + windowSize.value |
172 | 201 | ) |
173 | 202 | } |
174 | 203 |
|
175 | | - const range = parentWindowRanges.value[node.key] |
| 204 | + // Recursively compute windows for children |
| 205 | + const range = ranges[node.key] |
176 | 206 | for (let i = range.start; i < range.end && i < node.children.length; i++) { |
177 | | - updateNodeWindow(node.children[i]) |
| 207 | + computeNodeWindow(node.children[i]) |
178 | 208 | } |
179 | 209 | } |
180 | 210 |
|
181 | | - for (const child of renderedRoot.value.children || []) { |
182 | | - updateNodeWindow(child) |
| 211 | + for (const child of renderedRoot.value.children) { |
| 212 | + computeNodeWindow(child) |
183 | 213 | } |
184 | | -} |
185 | 214 |
|
186 | | -// Watch scroll position and update windows reactively |
187 | | -watch([scrollY, containerHeight, expandedKeys], updateWindows, { |
188 | | - immediate: true, |
189 | | - flush: 'post' |
| 215 | + return ranges |
190 | 216 | }) |
191 | 217 |
|
192 | | -// Reset windows to top when scroll reaches top |
193 | | -whenever( |
194 | | - () => scrollY.value === 0, |
195 | | - () => { |
196 | | - const resetNodeWindow = (node: RenderedTreeExplorerNode) => { |
197 | | - if (!node.children || node.leaf) return |
198 | | - const isExpanded = expandedKeys.value?.[node.key] ?? false |
199 | | - if (!isExpanded) return |
200 | | -
|
201 | | - const totalChildren = node.children.length |
202 | | - parentWindowRanges.value[node.key] = createInitialWindowRange( |
203 | | - totalChildren, |
204 | | - Math.ceil((containerHeight.value / DEFAULT_NODE_HEIGHT) * 2) |
205 | | - ) |
206 | | -
|
207 | | - for (const child of node.children) { |
208 | | - if (expandedKeys.value?.[child.key]) { |
209 | | - resetNodeWindow(child) |
210 | | - } |
211 | | - } |
212 | | - } |
213 | | -
|
214 | | - for (const parent of renderedRoot.value.children || []) { |
215 | | - resetNodeWindow(parent) |
216 | | - } |
217 | | - } |
218 | | -) |
219 | | -
|
220 | 218 | const getTreeNodeIcon = (node: TreeExplorerNode): string => { |
221 | 219 | if (node.getIcon) { |
222 | 220 | const icon = node.getIcon() |
@@ -266,11 +264,6 @@ const nodeKeyMap = computed<Record<string, RenderedTreeExplorerNode>>(() => { |
266 | 264 | return map |
267 | 265 | }) |
268 | 266 |
|
269 | | -const windowSize = computed(() => { |
270 | | - if (!containerHeight.value) return 60 |
271 | | - return Math.ceil((containerHeight.value / DEFAULT_NODE_HEIGHT) * 2) |
272 | | -}) |
273 | | -
|
274 | 267 | const displayRoot = computed<RenderedTreeExplorerNode>(() => ({ |
275 | 268 | ...renderedRoot.value, |
276 | 269 | children: (renderedRoot.value.children || []).map((node) => |
|
0 commit comments