77 ref ,
88 PropType ,
99 CSSProperties ,
10+ nextTick ,
1011} from 'vue' ;
1112import TreeNode , { treeNodePropsPass , NodeDataType } from 'src/components/TreeNode' ;
1213import { emitError , jsonFlatten , cloneDeep } from 'src/utils' ;
@@ -40,11 +41,21 @@ export default defineComponent({
4041 type : Number ,
4142 default : 400 ,
4243 } ,
43- // When using virtual scroll, define the height of each row.
44+ // When using virtual scroll without dynamicHeight , define the height of each row.
4445 itemHeight : {
4546 type : Number ,
4647 default : 20 ,
4748 } ,
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+ } ,
4859 // When there is a selection function, define the selected path.
4960 // For multiple selections, it is an array ['root.a','root.b'], for single selection, it is a string of 'root.a'.
5061 selectedValue : {
@@ -104,8 +115,73 @@ export default defineComponent({
104115 translateY : 0 ,
105116 visibleData : null as NodeDataType [ ] | null ,
106117 hiddenPaths : initHiddenPaths ( props . deep , props . collapsedNodeLength ) ,
118+ startIndex : 0 ,
119+ endIndex : 0 ,
107120 } ) ;
108121
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+
109185 const flatData = computed ( ( ) => {
110186 let startHiddenItem : null | NodeDataType = null ;
111187 const data = [ ] ;
@@ -154,31 +230,89 @@ export default defineComponent({
154230 : '' ;
155231 } ) ;
156232
233+ const listHeight = computed ( ( ) => {
234+ if ( props . dynamicHeight ) {
235+ return totalHeight || 0 ;
236+ }
237+ return flatData . value . length * props . itemHeight ;
238+ } ) ;
239+
157240 const updateVisibleData = ( ) => {
158241 const flatDataValue = flatData . value ;
242+ if ( ! flatDataValue ) return ;
159243 if ( props . virtual ) {
160- const visibleCount = props . height / props . itemHeight ;
161244 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 ) ;
171299 }
172- const end = start + visibleCount ;
173- state . translateY = start * props . itemHeight ;
174- state . visibleData = flatDataValue . filter ( ( item , index ) => index >= start && index < end ) ;
175300 } else {
301+ state . translateY = 0 ;
302+ state . startIndex = 0 ;
303+ state . endIndex = flatDataValue . length ;
176304 state . visibleData = flatDataValue ;
177305 }
178306 } ;
179307
308+ let rafId : number | null = null ;
180309 const handleTreeScroll = ( ) => {
181- updateVisibleData ( ) ;
310+ if ( rafId ) {
311+ cancelAnimationFrame ( rafId ) ;
312+ }
313+ rafId = requestAnimationFrame ( ( ) => {
314+ updateVisibleData ( ) ;
315+ } ) ;
182316 } ;
183317
184318 const handleSelectedChange = ( { path } : NodeDataType ) => {
@@ -251,10 +385,26 @@ export default defineComponent({
251385
252386 watchEffect ( ( ) => {
253387 if ( flatData . value ) {
388+ if ( props . virtual && props . dynamicHeight ) {
389+ if ( heights . length !== flatData . value . length ) {
390+ initDynamicHeights ( flatData . value . length ) ;
391+ }
392+ }
254393 updateVisibleData ( ) ;
255394 }
256395 } ) ;
257396
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+
258408 watch (
259409 ( ) => props . deep ,
260410 val => {
@@ -274,47 +424,52 @@ export default defineComponent({
274424 const renderNodeValue = props . renderNodeValue ?? slots . renderNodeValue ;
275425 const renderNodeActions = props . renderNodeActions ?? slots . renderNodeActions ?? false ;
276426
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+ } ) ;
318473
319474 return (
320475 < div
@@ -336,10 +491,7 @@ export default defineComponent({
336491 >
337492 { props . virtual ? (
338493 < 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` } } >
343495 < div
344496 class = "vjs-tree-list-holder-inner"
345497 style = { { transform : `translateY(${ state . translateY } px)` } }
0 commit comments