@tanstack/virtual-core 3.0.0-beta.44 → 3.0.0-beta.47

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/src/index.ts CHANGED
@@ -58,141 +58,124 @@ export const defaultRangeExtractor = (range: Range) => {
58
58
  return arr
59
59
  }
60
60
 
61
- const memoRectCallback = (
62
- instance: Virtualizer<any, any>,
61
+ export const observeElementRect = <T extends Element>(
62
+ instance: Virtualizer<T, any>,
63
63
  cb: (rect: Rect) => void,
64
64
  ) => {
65
- let prev: Rect = { height: -1, width: -1 }
66
-
67
- return (rect: Rect) => {
68
- if (
69
- instance.options.horizontal
70
- ? rect.width !== prev.width
71
- : rect.height !== prev.height
72
- ) {
73
- cb(rect)
74
- }
65
+ const element = instance.scrollElement
66
+ if (!element) {
67
+ return
68
+ }
75
69
 
76
- prev = rect
70
+ const handler = (rect: { width: number; height: number }) => {
71
+ const { width, height } = rect
72
+ cb({ width: Math.round(width), height: Math.round(height) })
77
73
  }
78
- }
79
74
 
80
- export const observeElementRect = (
81
- instance: Virtualizer<any, any>,
82
- cb: (rect: Rect) => void,
83
- ) => {
75
+ handler(element.getBoundingClientRect())
76
+
84
77
  const observer = new ResizeObserver((entries) => {
85
78
  const entry = entries[0]
86
79
  if (entry) {
87
- const { width, height } = entry.contentRect
88
- cb({
89
- width: Math.round(width),
90
- height: Math.round(height),
91
- })
92
- } else {
93
- cb({ width: 0, height: 0 })
80
+ const box = entry.borderBoxSize[0]
81
+ if (box) {
82
+ handler({ width: box.inlineSize, height: box.blockSize })
83
+ return
84
+ }
94
85
  }
86
+ handler(element.getBoundingClientRect())
95
87
  })
96
88
 
97
- if (!instance.scrollElement) {
98
- return
99
- }
100
-
101
- cb(instance.scrollElement.getBoundingClientRect())
102
-
103
- observer.observe(instance.scrollElement)
89
+ observer.observe(element, { box: 'border-box' })
104
90
 
105
91
  return () => {
106
- observer.unobserve(instance.scrollElement)
92
+ observer.unobserve(element)
107
93
  }
108
94
  }
109
95
 
110
96
  export const observeWindowRect = (
111
- instance: Virtualizer<any, any>,
97
+ instance: Virtualizer<Window, any>,
112
98
  cb: (rect: Rect) => void,
113
99
  ) => {
114
- const memoizedCallback = memoRectCallback(instance, cb)
115
- const onResize = () =>
116
- memoizedCallback({
117
- width: instance.scrollElement.innerWidth,
118
- height: instance.scrollElement.innerHeight,
119
- })
120
-
121
- if (!instance.scrollElement) {
100
+ const element = instance.scrollElement
101
+ if (!element) {
122
102
  return
123
103
  }
124
104
 
125
- onResize()
105
+ const handler = () => {
106
+ cb({ width: element.innerWidth, height: element.innerHeight })
107
+ }
108
+ handler()
126
109
 
127
- instance.scrollElement.addEventListener('resize', onResize, {
128
- capture: false,
110
+ element.addEventListener('resize', handler, {
129
111
  passive: true,
130
112
  })
131
113
 
132
114
  return () => {
133
- instance.scrollElement.removeEventListener('resize', onResize)
115
+ element.removeEventListener('resize', handler)
134
116
  }
135
117
  }
136
118
 
137
- type ObserverMode = 'element' | 'window'
138
-
139
- const scrollProps = {
140
- element: ['scrollLeft', 'scrollTop'],
141
- window: ['scrollX', 'scrollY'],
142
- } as const
143
-
144
- const createOffsetObserver = (mode: ObserverMode) => {
145
- return (instance: Virtualizer<any, any>, cb: (offset: number) => void) => {
146
- if (!instance.scrollElement) {
147
- return
148
- }
149
-
150
- const propX = scrollProps[mode][0]
151
- const propY = scrollProps[mode][1]
152
-
153
- let prevX: number = instance.scrollElement[propX]
154
- let prevY: number = instance.scrollElement[propY]
155
-
156
- const scroll = () => {
157
- const offset =
158
- instance.scrollElement[instance.options.horizontal ? propX : propY]
119
+ export const observeElementOffset = <T extends Element>(
120
+ instance: Virtualizer<T, any>,
121
+ cb: (offset: number) => void,
122
+ ) => {
123
+ const element = instance.scrollElement
124
+ if (!element) {
125
+ return
126
+ }
159
127
 
160
- cb(offset)
161
- }
128
+ const handler = () => {
129
+ cb(element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop'])
130
+ }
131
+ handler()
162
132
 
163
- scroll()
133
+ element.addEventListener('scroll', handler, {
134
+ passive: true,
135
+ })
164
136
 
165
- const onScroll = (e: Event) => {
166
- const target = e.currentTarget as HTMLElement & Window
167
- const scrollX = target[propX]
168
- const scrollY = target[propY]
137
+ return () => {
138
+ element.removeEventListener('scroll', handler)
139
+ }
140
+ }
169
141
 
170
- if (instance.options.horizontal ? prevX - scrollX : prevY - scrollY) {
171
- scroll()
172
- }
142
+ export const observeWindowOffset = (
143
+ instance: Virtualizer<Window, any>,
144
+ cb: (offset: number) => void,
145
+ ) => {
146
+ const element = instance.scrollElement
147
+ if (!element) {
148
+ return
149
+ }
173
150
 
174
- prevX = scrollX
175
- prevY = scrollY
176
- }
151
+ const handler = () => {
152
+ cb(element[instance.options.horizontal ? 'scrollX' : 'scrollY'])
153
+ }
154
+ handler()
177
155
 
178
- instance.scrollElement.addEventListener('scroll', onScroll, {
179
- capture: false,
180
- passive: true,
181
- })
156
+ element.addEventListener('scroll', handler, {
157
+ passive: true,
158
+ })
182
159
 
183
- return () => {
184
- instance.scrollElement.removeEventListener('scroll', onScroll)
185
- }
160
+ return () => {
161
+ element.removeEventListener('scroll', handler)
186
162
  }
187
163
  }
188
164
 
189
- export const observeElementOffset = createOffsetObserver('element')
190
- export const observeWindowOffset = createOffsetObserver('window')
191
-
192
165
  export const measureElement = <TItemElement extends Element>(
193
166
  element: TItemElement,
167
+ entry: ResizeObserverEntry | undefined,
194
168
  instance: Virtualizer<any, TItemElement>,
195
169
  ) => {
170
+ if (entry) {
171
+ const box = entry.borderBoxSize[0]
172
+ if (box) {
173
+ const size = Math.round(
174
+ box[instance.options.horizontal ? 'inlineSize' : 'blockSize'],
175
+ )
176
+ return size
177
+ }
178
+ }
196
179
  return Math.round(
197
180
  element.getBoundingClientRect()[
198
181
  instance.options.horizontal ? 'width' : 'height'
@@ -261,7 +244,8 @@ export interface VirtualizerOptions<
261
244
  initialRect?: Rect
262
245
  onChange?: (instance: Virtualizer<TScrollElement, TItemElement>) => void
263
246
  measureElement?: (
264
- el: TItemElement,
247
+ element: TItemElement,
248
+ entry: ResizeObserverEntry | undefined,
265
249
  instance: Virtualizer<TScrollElement, TItemElement>,
266
250
  ) => number
267
251
  overscan?: number
@@ -290,32 +274,36 @@ export class Virtualizer<
290
274
  private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
291
275
  private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
292
276
  measurementsCache: VirtualItem[] = []
293
- private itemSizeCache: Record<Key, number> = {}
277
+ private itemSizeCache = new Map<Key, number>()
294
278
  private pendingMeasuredCacheIndexes: number[] = []
295
279
  private scrollRect: Rect
296
280
  scrollOffset: number
297
281
  scrollDirection: ScrollDirection | null = null
298
282
  private scrollAdjustments: number = 0
299
- private measureElementCache: Record<
300
- Key,
301
- TItemElement & { __virtualizerSkipFirstNotSync?: boolean }
302
- > = {}
303
- private getResizeObserver = (() => {
283
+ measureElementCache = new Map<Key, TItemElement>()
284
+ private observer = (() => {
304
285
  let _ro: ResizeObserver | null = null
305
286
 
306
- return () => {
287
+ const get = () => {
307
288
  if (_ro) {
308
289
  return _ro
309
290
  } else if (typeof ResizeObserver !== 'undefined') {
310
291
  return (_ro = new ResizeObserver((entries) => {
311
292
  entries.forEach((entry) => {
312
- this._measureElement(entry.target as TItemElement, false)
293
+ this._measureElement(entry.target as TItemElement, entry)
313
294
  })
314
295
  }))
315
296
  } else {
316
297
  return null
317
298
  }
318
299
  }
300
+
301
+ return {
302
+ disconnect: () => get()?.disconnect(),
303
+ observe: (target: Element) =>
304
+ get()?.observe(target, { box: 'border-box' }),
305
+ unobserve: (target: Element) => get()?.unobserve(target),
306
+ }
319
307
  })()
320
308
  range: { startIndex: number; endIndex: number } = {
321
309
  startIndex: 0,
@@ -328,7 +316,7 @@ export class Virtualizer<
328
316
  this.scrollOffset = this.options.initialOffset
329
317
  this.measurementsCache = this.options.initialMeasurementsCache
330
318
  this.measurementsCache.forEach((item) => {
331
- this.itemSizeCache[item.key] = item.size
319
+ this.itemSizeCache.set(item.key, item.size)
332
320
  })
333
321
 
334
322
  this.maybeNotify()
@@ -372,12 +360,9 @@ export class Virtualizer<
372
360
  }
373
361
 
374
362
  _didMount = () => {
375
- const ro = this.getResizeObserver()
376
- Object.values(this.measureElementCache).forEach((node) => ro?.observe(node))
377
-
363
+ this.measureElementCache.forEach(this.observer.observe)
378
364
  return () => {
379
- ro?.disconnect()
380
-
365
+ this.observer.disconnect()
381
366
  this.cleanup()
382
367
  }
383
368
  }
@@ -397,8 +382,15 @@ export class Virtualizer<
397
382
 
398
383
  this.unsubs.push(
399
384
  this.options.observeElementRect(this, (rect) => {
385
+ const prev = this.scrollRect
400
386
  this.scrollRect = rect
401
- this.maybeNotify()
387
+ if (
388
+ this.options.horizontal
389
+ ? rect.width !== prev.width
390
+ : rect.height !== prev.height
391
+ ) {
392
+ this.maybeNotify()
393
+ }
402
394
  }),
403
395
  )
404
396
 
@@ -457,7 +449,7 @@ export class Virtualizer<
457
449
 
458
450
  for (let i = min; i < count; i++) {
459
451
  const key = getItemKey(i)
460
- const measuredSize = itemSizeCache[key]
452
+ const measuredSize = itemSizeCache.get(key)
461
453
  const start = measurements[i - 1]
462
454
  ? measurements[i - 1]!.end
463
455
  : paddingStart + scrollMargin
@@ -540,7 +532,10 @@ export class Virtualizer<
540
532
  return parseInt(indexStr, 10)
541
533
  }
542
534
 
543
- private _measureElement = (node: TItemElement, sync: boolean) => {
535
+ private _measureElement = (
536
+ node: TItemElement,
537
+ entry: ResizeObserverEntry | undefined,
538
+ ) => {
544
539
  const index = this.indexFromElement(node)
545
540
 
546
541
  const item = this.measurementsCache[index]
@@ -548,34 +543,27 @@ export class Virtualizer<
548
543
  return
549
544
  }
550
545
 
551
- const prevNode = this.measureElementCache[item.key]
552
-
553
- const ro = this.getResizeObserver()
546
+ const prevNode = this.measureElementCache.get(item.key)
554
547
 
555
548
  if (!node.isConnected) {
556
- ro?.unobserve(node)
549
+ this.observer.unobserve(node)
557
550
  if (node === prevNode) {
558
- delete this.measureElementCache[item.key]
551
+ this.measureElementCache.delete(item.key)
559
552
  }
560
553
  return
561
554
  }
562
555
 
563
556
  if (prevNode !== node) {
564
557
  if (prevNode) {
565
- ro?.unobserve(prevNode)
566
- }
567
- ro?.observe(node)
568
- this.measureElementCache[item.key] = node
569
- } else {
570
- if (!sync && !prevNode.__virtualizerSkipFirstNotSync) {
571
- prevNode.__virtualizerSkipFirstNotSync = true
572
- return
558
+ this.observer.unobserve(prevNode)
573
559
  }
560
+ this.observer.observe(node)
561
+ this.measureElementCache.set(item.key, node)
574
562
  }
575
563
 
576
- const measuredItemSize = this.options.measureElement(node, this)
564
+ const measuredItemSize = this.options.measureElement(node, entry, this)
577
565
 
578
- const itemSize = this.itemSizeCache[item.key] ?? item.size
566
+ const itemSize = this.itemSizeCache.get(item.key) ?? item.size
579
567
 
580
568
  const delta = measuredItemSize - itemSize
581
569
 
@@ -592,10 +580,11 @@ export class Virtualizer<
592
580
  }
593
581
 
594
582
  this.pendingMeasuredCacheIndexes.push(index)
595
- this.itemSizeCache = {
596
- ...this.itemSizeCache,
597
- [item.key]: measuredItemSize,
598
- }
583
+
584
+ this.itemSizeCache = new Map(
585
+ this.itemSizeCache.set(item.key, measuredItemSize),
586
+ )
587
+
599
588
  this.notify()
600
589
  }
601
590
  }
@@ -605,7 +594,7 @@ export class Virtualizer<
605
594
  return
606
595
  }
607
596
 
608
- this._measureElement(node, true)
597
+ this._measureElement(node, undefined)
609
598
  }
610
599
 
611
600
  getVirtualItems = memo(
@@ -692,7 +681,7 @@ export class Virtualizer<
692
681
  return [this.getOffsetForAlignment(toOffset, align), align] as const
693
682
  }
694
683
 
695
- private isDynamicMode = () => Object.keys(this.measureElementCache).length > 0
684
+ private isDynamicMode = () => this.measureElementCache.size > 0
696
685
 
697
686
  private cancelScrollToIndex = () => {
698
687
  if (this.scrollToIndexTimeoutId !== null) {
@@ -741,8 +730,9 @@ export class Virtualizer<
741
730
  this.scrollToIndexTimeoutId = setTimeout(() => {
742
731
  this.scrollToIndexTimeoutId = null
743
732
 
744
- const elementInDOM =
745
- !!this.measureElementCache[this.options.getItemKey(index)]
733
+ const elementInDOM = this.measureElementCache.has(
734
+ this.options.getItemKey(index),
735
+ )
746
736
 
747
737
  if (elementInDOM) {
748
738
  const [toOffset] = this.getOffsetForIndex(index, align)
@@ -792,7 +782,7 @@ export class Virtualizer<
792
782
  }
793
783
 
794
784
  measure = () => {
795
- this.itemSizeCache = {}
785
+ this.itemSizeCache = new Map()
796
786
  this.notify()
797
787
  }
798
788
  }