@tanstack/virtual-core 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +58 -66
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +5 -7
- package/dist/cjs/utils.cjs +8 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/index.d.ts +5 -7
- package/dist/esm/index.js +59 -67
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +8 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +89 -78
- package/src/utils.ts +8 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { approxEqual, memo, notUndefined } from './utils'
|
|
1
|
+
import { approxEqual, memo, notUndefined, debounce } from './utils'
|
|
2
2
|
|
|
3
3
|
export * from './utils'
|
|
4
4
|
|
|
@@ -98,6 +98,10 @@ export const observeElementRect = <T extends Element>(
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const addEventListenerOptions = {
|
|
102
|
+
passive: true,
|
|
103
|
+
}
|
|
104
|
+
|
|
101
105
|
export const observeWindowRect = (
|
|
102
106
|
instance: Virtualizer<Window, any>,
|
|
103
107
|
cb: (rect: Rect) => void,
|
|
@@ -112,58 +116,81 @@ export const observeWindowRect = (
|
|
|
112
116
|
}
|
|
113
117
|
handler()
|
|
114
118
|
|
|
115
|
-
element.addEventListener('resize', handler,
|
|
116
|
-
passive: true,
|
|
117
|
-
})
|
|
119
|
+
element.addEventListener('resize', handler, addEventListenerOptions)
|
|
118
120
|
|
|
119
121
|
return () => {
|
|
120
122
|
element.removeEventListener('resize', handler)
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
const supportsScrollend =
|
|
127
|
+
typeof window == 'undefined' ? true : 'onscrollend' in window
|
|
128
|
+
|
|
124
129
|
export const observeElementOffset = <T extends Element>(
|
|
125
130
|
instance: Virtualizer<T, any>,
|
|
126
|
-
cb: (offset: number) => void,
|
|
131
|
+
cb: (offset: number, isScrolling: boolean) => void,
|
|
127
132
|
) => {
|
|
128
133
|
const element = instance.scrollElement
|
|
129
134
|
if (!element) {
|
|
130
135
|
return
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
let offset = 0
|
|
139
|
+
const fallback = supportsScrollend
|
|
140
|
+
? () => undefined
|
|
141
|
+
: debounce(() => {
|
|
142
|
+
cb(offset, false)
|
|
143
|
+
}, 150)
|
|
144
|
+
|
|
145
|
+
const createHandler = (isScrolling: boolean) => () => {
|
|
146
|
+
offset = element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop']
|
|
147
|
+
fallback()
|
|
148
|
+
cb(offset, isScrolling)
|
|
135
149
|
}
|
|
136
|
-
handler()
|
|
150
|
+
const handler = createHandler(true)
|
|
151
|
+
const endHandler = createHandler(false)
|
|
152
|
+
endHandler()
|
|
137
153
|
|
|
138
|
-
element.addEventListener('scroll', handler,
|
|
139
|
-
|
|
140
|
-
})
|
|
154
|
+
element.addEventListener('scroll', handler, addEventListenerOptions)
|
|
155
|
+
element.addEventListener('scrollend', endHandler, addEventListenerOptions)
|
|
141
156
|
|
|
142
157
|
return () => {
|
|
143
158
|
element.removeEventListener('scroll', handler)
|
|
159
|
+
element.removeEventListener('scrollend', endHandler)
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
export const observeWindowOffset = (
|
|
148
164
|
instance: Virtualizer<Window, any>,
|
|
149
|
-
cb: (offset: number) => void,
|
|
165
|
+
cb: (offset: number, isScrolling: boolean) => void,
|
|
150
166
|
) => {
|
|
151
167
|
const element = instance.scrollElement
|
|
152
168
|
if (!element) {
|
|
153
169
|
return
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
let offset = 0
|
|
173
|
+
const fallback = supportsScrollend
|
|
174
|
+
? () => undefined
|
|
175
|
+
: debounce(() => {
|
|
176
|
+
cb(offset, false)
|
|
177
|
+
}, 150)
|
|
178
|
+
|
|
179
|
+
const createHandler = (isScrolling: boolean) => () => {
|
|
180
|
+
offset = element[instance.options.horizontal ? 'scrollX' : 'scrollY']
|
|
181
|
+
fallback()
|
|
182
|
+
cb(offset, isScrolling)
|
|
158
183
|
}
|
|
159
|
-
handler()
|
|
184
|
+
const handler = createHandler(true)
|
|
185
|
+
const endHandler = createHandler(false)
|
|
186
|
+
endHandler()
|
|
160
187
|
|
|
161
|
-
element.addEventListener('scroll', handler,
|
|
162
|
-
|
|
163
|
-
})
|
|
188
|
+
element.addEventListener('scroll', handler, addEventListenerOptions)
|
|
189
|
+
element.addEventListener('scrollend', endHandler, addEventListenerOptions)
|
|
164
190
|
|
|
165
191
|
return () => {
|
|
166
192
|
element.removeEventListener('scroll', handler)
|
|
193
|
+
element.removeEventListener('scrollend', endHandler)
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
|
|
@@ -241,7 +268,7 @@ export interface VirtualizerOptions<
|
|
|
241
268
|
) => void | (() => void)
|
|
242
269
|
observeElementOffset: (
|
|
243
270
|
instance: Virtualizer<TScrollElement, TItemElement>,
|
|
244
|
-
cb: (offset: number) => void,
|
|
271
|
+
cb: (offset: number, isScrolling: boolean) => void,
|
|
245
272
|
) => void | (() => void)
|
|
246
273
|
|
|
247
274
|
// Optional
|
|
@@ -267,7 +294,6 @@ export interface VirtualizerOptions<
|
|
|
267
294
|
rangeExtractor?: (range: Range) => number[]
|
|
268
295
|
scrollMargin?: number
|
|
269
296
|
gap?: number
|
|
270
|
-
scrollingDelay?: number
|
|
271
297
|
indexAttribute?: string
|
|
272
298
|
initialMeasurementsCache?: VirtualItem[]
|
|
273
299
|
lanes?: number
|
|
@@ -281,7 +307,6 @@ export class Virtualizer<
|
|
|
281
307
|
options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
|
|
282
308
|
scrollElement: TScrollElement | null = null
|
|
283
309
|
isScrolling: boolean = false
|
|
284
|
-
private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
285
310
|
private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
286
311
|
measurementsCache: VirtualItem[] = []
|
|
287
312
|
private itemSizeCache = new Map<Key, number>()
|
|
@@ -290,6 +315,13 @@ export class Virtualizer<
|
|
|
290
315
|
scrollOffset: number
|
|
291
316
|
scrollDirection: ScrollDirection | null = null
|
|
292
317
|
private scrollAdjustments: number = 0
|
|
318
|
+
shouldAdjustScrollPositionOnItemSizeChange:
|
|
319
|
+
| undefined
|
|
320
|
+
| ((
|
|
321
|
+
item: VirtualItem,
|
|
322
|
+
delta: number,
|
|
323
|
+
instance: Virtualizer<TScrollElement, TItemElement>,
|
|
324
|
+
) => boolean)
|
|
293
325
|
measureElementCache = new Map<Key, TItemElement>()
|
|
294
326
|
private observer = (() => {
|
|
295
327
|
let _ro: ResizeObserver | null = null
|
|
@@ -329,7 +361,7 @@ export class Virtualizer<
|
|
|
329
361
|
this.itemSizeCache.set(item.key, item.size)
|
|
330
362
|
})
|
|
331
363
|
|
|
332
|
-
this.
|
|
364
|
+
this.notify(false, false)
|
|
333
365
|
}
|
|
334
366
|
|
|
335
367
|
setOptions = (opts: VirtualizerOptions<TScrollElement, TItemElement>) => {
|
|
@@ -353,7 +385,6 @@ export class Virtualizer<
|
|
|
353
385
|
initialRect: { width: 0, height: 0 },
|
|
354
386
|
scrollMargin: 0,
|
|
355
387
|
gap: 0,
|
|
356
|
-
scrollingDelay: 150,
|
|
357
388
|
indexAttribute: 'data-index',
|
|
358
389
|
initialMeasurementsCache: [],
|
|
359
390
|
lanes: 1,
|
|
@@ -361,34 +392,22 @@ export class Virtualizer<
|
|
|
361
392
|
}
|
|
362
393
|
}
|
|
363
394
|
|
|
364
|
-
private notify = (sync: boolean) => {
|
|
365
|
-
this.
|
|
395
|
+
private notify = (force: boolean, sync: boolean) => {
|
|
396
|
+
const { startIndex, endIndex } = this.range ?? {
|
|
397
|
+
startIndex: undefined,
|
|
398
|
+
endIndex: undefined,
|
|
399
|
+
}
|
|
400
|
+
const range = this.calculateRange()
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
force ||
|
|
404
|
+
startIndex !== range?.startIndex ||
|
|
405
|
+
endIndex !== range?.endIndex
|
|
406
|
+
) {
|
|
407
|
+
this.options.onChange?.(this, sync)
|
|
408
|
+
}
|
|
366
409
|
}
|
|
367
410
|
|
|
368
|
-
private maybeNotify = memo(
|
|
369
|
-
() => {
|
|
370
|
-
this.calculateRange()
|
|
371
|
-
|
|
372
|
-
return [
|
|
373
|
-
this.isScrolling,
|
|
374
|
-
this.range ? this.range.startIndex : null,
|
|
375
|
-
this.range ? this.range.endIndex : null,
|
|
376
|
-
]
|
|
377
|
-
},
|
|
378
|
-
(isScrolling) => {
|
|
379
|
-
this.notify(isScrolling)
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
key: process.env.NODE_ENV !== 'production' && 'maybeNotify',
|
|
383
|
-
debug: () => this.options.debug,
|
|
384
|
-
initialDeps: [
|
|
385
|
-
this.isScrolling,
|
|
386
|
-
this.range ? this.range.startIndex : null,
|
|
387
|
-
this.range ? this.range.endIndex : null,
|
|
388
|
-
] as [boolean, number | null, number | null],
|
|
389
|
-
},
|
|
390
|
-
)
|
|
391
|
-
|
|
392
411
|
private cleanup = () => {
|
|
393
412
|
this.unsubs.filter(Boolean).forEach((d) => d!())
|
|
394
413
|
this.unsubs = []
|
|
@@ -419,37 +438,24 @@ export class Virtualizer<
|
|
|
419
438
|
this.unsubs.push(
|
|
420
439
|
this.options.observeElementRect(this, (rect) => {
|
|
421
440
|
this.scrollRect = rect
|
|
422
|
-
this.
|
|
441
|
+
this.notify(false, false)
|
|
423
442
|
}),
|
|
424
443
|
)
|
|
425
444
|
|
|
426
445
|
this.unsubs.push(
|
|
427
|
-
this.options.observeElementOffset(this, (offset) => {
|
|
446
|
+
this.options.observeElementOffset(this, (offset, isScrolling) => {
|
|
428
447
|
this.scrollAdjustments = 0
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (this.isScrollingTimeoutId !== null) {
|
|
435
|
-
clearTimeout(this.isScrollingTimeoutId)
|
|
436
|
-
this.isScrollingTimeoutId = null
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this.isScrolling = true
|
|
440
|
-
this.scrollDirection =
|
|
441
|
-
this.scrollOffset < offset ? 'forward' : 'backward'
|
|
448
|
+
this.scrollDirection = isScrolling
|
|
449
|
+
? this.scrollOffset < offset
|
|
450
|
+
? 'forward'
|
|
451
|
+
: 'backward'
|
|
452
|
+
: null
|
|
442
453
|
this.scrollOffset = offset
|
|
443
454
|
|
|
444
|
-
this.
|
|
455
|
+
const prevIsScrolling = this.isScrolling
|
|
456
|
+
this.isScrolling = isScrolling
|
|
445
457
|
|
|
446
|
-
this.
|
|
447
|
-
this.isScrollingTimeoutId = null
|
|
448
|
-
this.isScrolling = false
|
|
449
|
-
this.scrollDirection = null
|
|
450
|
-
|
|
451
|
-
this.maybeNotify()
|
|
452
|
-
}, this.options.scrollingDelay)
|
|
458
|
+
this.notify(prevIsScrolling !== isScrolling, isScrolling)
|
|
453
459
|
}),
|
|
454
460
|
)
|
|
455
461
|
}
|
|
@@ -459,7 +465,7 @@ export class Virtualizer<
|
|
|
459
465
|
return this.scrollRect[this.options.horizontal ? 'width' : 'height']
|
|
460
466
|
}
|
|
461
467
|
|
|
462
|
-
private
|
|
468
|
+
private getMeasurementOptions = memo(
|
|
463
469
|
() => [
|
|
464
470
|
this.options.count,
|
|
465
471
|
this.options.paddingStart,
|
|
@@ -522,7 +528,7 @@ export class Virtualizer<
|
|
|
522
528
|
}
|
|
523
529
|
|
|
524
530
|
private getMeasurements = memo(
|
|
525
|
-
() => [this.
|
|
531
|
+
() => [this.getMeasurementOptions(), this.itemSizeCache],
|
|
526
532
|
({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => {
|
|
527
533
|
const min =
|
|
528
534
|
this.pendingMeasuredCacheIndexes.length > 0
|
|
@@ -605,7 +611,8 @@ export class Virtualizer<
|
|
|
605
611
|
return range === null
|
|
606
612
|
? []
|
|
607
613
|
: rangeExtractor({
|
|
608
|
-
|
|
614
|
+
startIndex: range.startIndex,
|
|
615
|
+
endIndex: range.endIndex,
|
|
609
616
|
overscan,
|
|
610
617
|
count,
|
|
611
618
|
})
|
|
@@ -666,7 +673,11 @@ export class Virtualizer<
|
|
|
666
673
|
const delta = size - itemSize
|
|
667
674
|
|
|
668
675
|
if (delta !== 0) {
|
|
669
|
-
if (
|
|
676
|
+
if (
|
|
677
|
+
this.shouldAdjustScrollPositionOnItemSizeChange !== undefined
|
|
678
|
+
? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this)
|
|
679
|
+
: item.start < this.scrollOffset + this.scrollAdjustments
|
|
680
|
+
) {
|
|
670
681
|
if (process.env.NODE_ENV !== 'production' && this.options.debug) {
|
|
671
682
|
console.info('correction', delta)
|
|
672
683
|
}
|
|
@@ -680,7 +691,7 @@ export class Virtualizer<
|
|
|
680
691
|
this.pendingMeasuredCacheIndexes.push(item.index)
|
|
681
692
|
this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size))
|
|
682
693
|
|
|
683
|
-
this.notify(false)
|
|
694
|
+
this.notify(true, false)
|
|
684
695
|
}
|
|
685
696
|
}
|
|
686
697
|
|
|
@@ -907,7 +918,7 @@ export class Virtualizer<
|
|
|
907
918
|
|
|
908
919
|
measure = () => {
|
|
909
920
|
this.itemSizeCache = new Map()
|
|
910
|
-
this.
|
|
921
|
+
this.options.onChange?.(this, false)
|
|
911
922
|
}
|
|
912
923
|
}
|
|
913
924
|
|
package/src/utils.ts
CHANGED
|
@@ -77,3 +77,11 @@ export function notUndefined<T>(value: T | undefined, msg?: string): T {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1
|
|
80
|
+
|
|
81
|
+
export const debounce = (fn: Function, ms: number) => {
|
|
82
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
83
|
+
return function (this: any, ...args: any[]) {
|
|
84
|
+
clearTimeout(timeoutId)
|
|
85
|
+
timeoutId = setTimeout(() => fn.apply(this, args), ms)
|
|
86
|
+
}
|
|
87
|
+
}
|