7
7
ref ,
8
8
PropType ,
9
9
CSSProperties ,
10
+ nextTick ,
10
11
} from 'vue' ;
11
12
import TreeNode , { treeNodePropsPass , NodeDataType } from 'src/components/TreeNode' ;
12
13
import { emitError , jsonFlatten , cloneDeep } from 'src/utils' ;
@@ -40,11 +41,21 @@ export default defineComponent({
40
41
type : Number ,
41
42
default : 400 ,
42
43
} ,
43
- // When using virtual scroll, define the height of each row.
44
+ // When using virtual scroll without dynamicHeight , define the height of each row.
44
45
itemHeight : {
45
46
type : Number ,
46
47
default : 20 ,
47
48
} ,
49
+ // Enable dynamic row heights for virtual scroll.
50
+ dynamicHeight : {
51
+ type : Boolean ,
52
+ default : false ,
53
+ } ,
54
+ // Estimated item height used before measurement in dynamic mode.
55
+ estimatedItemHeight : {
56
+ type : Number ,
57
+ default : 20 ,
58
+ } ,
48
59
// When there is a selection function, define the selected path.
49
60
// For multiple selections, it is an array ['root.a','root.b'], for single selection, it is a string of 'root.a'.
50
61
selectedValue : {
@@ -104,8 +115,73 @@ export default defineComponent({
104
115
translateY : 0 ,
105
116
visibleData : null as NodeDataType [ ] | null ,
106
117
hiddenPaths : initHiddenPaths ( props . deep , props . collapsedNodeLength ) ,
118
+ startIndex : 0 ,
119
+ endIndex : 0 ,
107
120
} ) ;
108
121
122
+ // Dynamic height bookkeeping
123
+ // heights[i] is the measured height of row i in the current flatData (or estimated if not measured yet)
124
+ // offsets[i] is the cumulative offset before row i (offsets[0] = 0, offsets[length] = totalHeight)
125
+ let heights : number [ ] = [ ] ;
126
+ let offsets : number [ ] = [ ] ;
127
+ let totalHeight = 0 ;
128
+ const rowRefs : Record < number , HTMLElement | null > = { } ;
129
+ const OVERSCAN_COUNT = 5 ;
130
+
131
+ const initDynamicHeights = ( length : number ) => {
132
+ heights = Array ( length )
133
+ . fill ( 0 )
134
+ . map ( ( ) => props . estimatedItemHeight || props . itemHeight || 20 ) ;
135
+ offsets = new Array ( length + 1 ) ;
136
+ offsets [ 0 ] = 0 ;
137
+ for ( let i = 0 ; i < length ; i ++ ) {
138
+ offsets [ i + 1 ] = offsets [ i ] + heights [ i ] ;
139
+ }
140
+ totalHeight = offsets [ length ] || 0 ;
141
+ } ;
142
+
143
+ const recomputeOffsetsFrom = ( start : number ) => {
144
+ const length = heights . length ;
145
+ if ( start < 0 ) start = 0 ;
146
+ if ( start > length ) start = length ;
147
+ for ( let i = start ; i < length ; i ++ ) {
148
+ offsets [ i + 1 ] = offsets [ i ] + heights [ i ] ;
149
+ }
150
+ totalHeight = offsets [ length ] || 0 ;
151
+ } ;
152
+
153
+ const setRowRef = ( index : number , el : HTMLElement | null ) => {
154
+ if ( el ) {
155
+ rowRefs [ index ] = el ;
156
+ } else {
157
+ delete rowRefs [ index ] ;
158
+ }
159
+ } ;
160
+
161
+ const lowerBound = ( arr : number [ ] , target : number ) => {
162
+ // first index i where arr[i] >= target
163
+ let lo = 0 ;
164
+ let hi = arr . length - 1 ;
165
+ while ( lo < hi ) {
166
+ const mid = ( lo + hi ) >>> 1 ;
167
+ if ( arr [ mid ] < target ) lo = mid + 1 ;
168
+ else hi = mid ;
169
+ }
170
+ return lo ;
171
+ } ;
172
+
173
+ const findStartIndexByScrollTop = ( scrollTop : number ) => {
174
+ // largest i such that offsets[i] <= scrollTop
175
+ const i = lowerBound ( offsets , scrollTop + 0.0001 ) ; // epsilon to handle exact matches
176
+ return Math . max ( 0 , Math . min ( i - 1 , heights . length - 1 ) ) ;
177
+ } ;
178
+
179
+ const findEndIndexByViewport = ( scrollTop : number , viewportHeight : number ) => {
180
+ const target = scrollTop + viewportHeight ;
181
+ const i = lowerBound ( offsets , target ) ;
182
+ return Math . max ( 0 , Math . min ( i + 1 , heights . length ) ) ;
183
+ } ;
184
+
109
185
const flatData = computed ( ( ) => {
110
186
let startHiddenItem : null | NodeDataType = null ;
111
187
const data = [ ] ;
@@ -154,31 +230,89 @@ export default defineComponent({
154
230
: '' ;
155
231
} ) ;
156
232
233
+ const listHeight = computed ( ( ) => {
234
+ if ( props . dynamicHeight ) {
235
+ return totalHeight || 0 ;
236
+ }
237
+ return flatData . value . length * props . itemHeight ;
238
+ } ) ;
239
+
157
240
const updateVisibleData = ( ) => {
158
241
const flatDataValue = flatData . value ;
242
+ if ( ! flatDataValue ) return ;
159
243
if ( props . virtual ) {
160
- const visibleCount = props . height / props . itemHeight ;
161
244
const scrollTop = treeRef . value ?. scrollTop || 0 ;
162
- const scrollCount = Math . floor ( scrollTop / props . itemHeight ) ;
163
- let start =
164
- scrollCount < 0
165
- ? 0
166
- : scrollCount + visibleCount > flatDataValue . length
167
- ? flatDataValue . length - visibleCount
168
- : scrollCount ;
169
- if ( start < 0 ) {
170
- start = 0 ;
245
+
246
+ if ( props . dynamicHeight ) {
247
+ // Ensure dynamic arrays are initialized and consistent with data length
248
+ if ( heights . length !== flatDataValue . length ) {
249
+ initDynamicHeights ( flatDataValue . length ) ;
250
+ }
251
+
252
+ const start = findStartIndexByScrollTop ( scrollTop ) ;
253
+ const endNoOverscan = findEndIndexByViewport ( scrollTop , props . height ) ;
254
+ const startWithOverscan = Math . max ( 0 , start - OVERSCAN_COUNT ) ;
255
+ const endWithOverscan = Math . min ( flatDataValue . length , endNoOverscan + OVERSCAN_COUNT ) ;
256
+
257
+ state . startIndex = startWithOverscan ;
258
+ state . endIndex = endWithOverscan ;
259
+ state . translateY = offsets [ startWithOverscan ] || 0 ;
260
+ state . visibleData = flatDataValue . slice ( startWithOverscan , endWithOverscan ) ;
261
+
262
+ // Measure after render and update heights/offets if needed
263
+ nextTick ( ) . then ( ( ) => {
264
+ let changed = false ;
265
+ for ( let i = state . startIndex ; i < state . endIndex ; i ++ ) {
266
+ const el = rowRefs [ i ] ;
267
+ if ( ! el ) continue ;
268
+ const h = el . offsetHeight ;
269
+ if ( h && heights [ i ] !== h ) {
270
+ heights [ i ] = h ;
271
+ // Update offsets from i forward
272
+ offsets [ i + 1 ] = offsets [ i ] + heights [ i ] ;
273
+ recomputeOffsetsFrom ( i + 1 ) ;
274
+ changed = true ;
275
+ }
276
+ }
277
+ if ( changed ) {
278
+ // Recalculate slice based on new offsets
279
+ updateVisibleData ( ) ;
280
+ }
281
+ } ) ;
282
+ } else {
283
+ const visibleCount = props . height / props . itemHeight ;
284
+ const scrollCount = Math . floor ( scrollTop / props . itemHeight ) ;
285
+ let start =
286
+ scrollCount < 0
287
+ ? 0
288
+ : scrollCount + visibleCount > flatDataValue . length
289
+ ? flatDataValue . length - visibleCount
290
+ : scrollCount ;
291
+ if ( start < 0 ) {
292
+ start = 0 ;
293
+ }
294
+ const end = start + visibleCount ;
295
+ state . translateY = start * props . itemHeight ;
296
+ state . startIndex = start ;
297
+ state . endIndex = end ;
298
+ state . visibleData = flatDataValue . slice ( start , end ) ;
171
299
}
172
- const end = start + visibleCount ;
173
- state . translateY = start * props . itemHeight ;
174
- state . visibleData = flatDataValue . filter ( ( item , index ) => index >= start && index < end ) ;
175
300
} else {
301
+ state . translateY = 0 ;
302
+ state . startIndex = 0 ;
303
+ state . endIndex = flatDataValue . length ;
176
304
state . visibleData = flatDataValue ;
177
305
}
178
306
} ;
179
307
308
+ let rafId : number | null = null ;
180
309
const handleTreeScroll = ( ) => {
181
- updateVisibleData ( ) ;
310
+ if ( rafId ) {
311
+ cancelAnimationFrame ( rafId ) ;
312
+ }
313
+ rafId = requestAnimationFrame ( ( ) => {
314
+ updateVisibleData ( ) ;
315
+ } ) ;
182
316
} ;
183
317
184
318
const handleSelectedChange = ( { path } : NodeDataType ) => {
@@ -251,10 +385,26 @@ export default defineComponent({
251
385
252
386
watchEffect ( ( ) => {
253
387
if ( flatData . value ) {
388
+ if ( props . virtual && props . dynamicHeight ) {
389
+ if ( heights . length !== flatData . value . length ) {
390
+ initDynamicHeights ( flatData . value . length ) ;
391
+ }
392
+ }
254
393
updateVisibleData ( ) ;
255
394
}
256
395
} ) ;
257
396
397
+ // Re-initialize dynamic height arrays when data shape changes significantly
398
+ watch (
399
+ ( ) => [ props . dynamicHeight , props . estimatedItemHeight , originFlatData . value . length ] ,
400
+ ( ) => {
401
+ if ( props . virtual && props . dynamicHeight ) {
402
+ initDynamicHeights ( flatData . value . length ) ;
403
+ nextTick ( updateVisibleData ) ;
404
+ }
405
+ } ,
406
+ ) ;
407
+
258
408
watch (
259
409
( ) => props . deep ,
260
410
val => {
@@ -274,47 +424,52 @@ export default defineComponent({
274
424
const renderNodeValue = props . renderNodeValue ?? slots . renderNodeValue ;
275
425
const renderNodeActions = props . renderNodeActions ?? slots . renderNodeActions ?? false ;
276
426
277
- const nodeContent =
278
- state . visibleData &&
279
- state . visibleData . map ( item => (
280
- < TreeNode
281
- key = { item . id }
282
- data = { props . data }
283
- rootPath = { props . rootPath }
284
- indent = { props . indent }
285
- node = { item }
286
- collapsed = { ! ! state . hiddenPaths [ item . path ] }
287
- theme = { props . theme }
288
- showDoubleQuotes = { props . showDoubleQuotes }
289
- showLength = { props . showLength }
290
- checked = { selectedPaths . value . includes ( item . path ) }
291
- selectableType = { props . selectableType }
292
- showLine = { props . showLine }
293
- showLineNumber = { props . showLineNumber }
294
- showSelectController = { props . showSelectController }
295
- selectOnClickNode = { props . selectOnClickNode }
296
- nodeSelectable = { props . nodeSelectable }
297
- highlightSelectedNode = { props . highlightSelectedNode }
298
- editable = { props . editable }
299
- editableTrigger = { props . editableTrigger }
300
- showIcon = { props . showIcon }
301
- showKeyValueSpace = { props . showKeyValueSpace }
302
- renderNodeKey = { renderNodeKey }
303
- renderNodeValue = { renderNodeValue }
304
- renderNodeActions = { renderNodeActions }
305
- onNodeClick = { handleNodeClick }
306
- onNodeMouseover = { handleNodeMouseover }
307
- onBracketsClick = { handleBracketsClick }
308
- onIconClick = { handleIconClick }
309
- onSelectedChange = { handleSelectedChange }
310
- onValueChange = { handleValueChange }
311
- style = {
312
- props . itemHeight && props . itemHeight !== 20
313
- ? { lineHeight : `${ props . itemHeight } px` }
314
- : { }
315
- }
316
- />
317
- ) ) ;
427
+ const nodeContent = state . visibleData ?. map ( ( item , localIndex ) => {
428
+ const globalIndex = state . startIndex + localIndex ;
429
+ return (
430
+ < div key = { item . id } ref = { el => setRowRef ( globalIndex , ( el as HTMLElement ) || null ) } >
431
+ < TreeNode
432
+ data = { props . data }
433
+ rootPath = { props . rootPath }
434
+ indent = { props . indent }
435
+ node = { item }
436
+ collapsed = { ! ! state . hiddenPaths [ item . path ] }
437
+ theme = { props . theme }
438
+ showDoubleQuotes = { props . showDoubleQuotes }
439
+ showLength = { props . showLength }
440
+ checked = { selectedPaths . value . includes ( item . path ) }
441
+ selectableType = { props . selectableType }
442
+ showLine = { props . showLine }
443
+ showLineNumber = { props . showLineNumber }
444
+ showSelectController = { props . showSelectController }
445
+ selectOnClickNode = { props . selectOnClickNode }
446
+ nodeSelectable = { props . nodeSelectable }
447
+ highlightSelectedNode = { props . highlightSelectedNode }
448
+ editable = { props . editable }
449
+ editableTrigger = { props . editableTrigger }
450
+ showIcon = { props . showIcon }
451
+ showKeyValueSpace = { props . showKeyValueSpace }
452
+ renderNodeKey = { renderNodeKey }
453
+ renderNodeValue = { renderNodeValue }
454
+ renderNodeActions = { renderNodeActions }
455
+ onNodeClick = { handleNodeClick }
456
+ onNodeMouseover = { handleNodeMouseover }
457
+ onBracketsClick = { handleBracketsClick }
458
+ onIconClick = { handleIconClick }
459
+ onSelectedChange = { handleSelectedChange }
460
+ onValueChange = { handleValueChange }
461
+ class = { props . dynamicHeight ? 'dynamic-height' : undefined }
462
+ style = {
463
+ props . dynamicHeight
464
+ ? { }
465
+ : props . itemHeight && props . itemHeight !== 20
466
+ ? { lineHeight : `${ props . itemHeight } px` }
467
+ : { }
468
+ }
469
+ />
470
+ </ div >
471
+ ) ;
472
+ } ) ;
318
473
319
474
return (
320
475
< div
@@ -336,10 +491,7 @@ export default defineComponent({
336
491
>
337
492
{ props . virtual ? (
338
493
< div class = "vjs-tree-list" style = { { height : `${ props . height } px` } } >
339
- < div
340
- class = "vjs-tree-list-holder"
341
- style = { { height : `${ flatData . value . length * props . itemHeight } px` } }
342
- >
494
+ < div class = "vjs-tree-list-holder" style = { { height : `${ listHeight . value } px` } } >
343
495
< div
344
496
class = "vjs-tree-list-holder-inner"
345
497
style = { { transform : `translateY(${ state . translateY } px)` } }
0 commit comments