@tanstack/virtual-core 3.0.0-beta.2 → 3.0.0-beta.20

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.
@@ -8,76 +8,6 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- var props = ["bottom", "height", "left", "right", "top", "width"];
12
-
13
- var rectChanged = function rectChanged(a, b) {
14
- if (a === void 0) {
15
- a = {};
16
- }
17
-
18
- if (b === void 0) {
19
- b = {};
20
- }
21
-
22
- return props.some(function (prop) {
23
- return a[prop] !== b[prop];
24
- });
25
- };
26
-
27
- var observedNodes = /*#__PURE__*/new Map();
28
- var rafId;
29
-
30
- var run = function run() {
31
- var changedStates = [];
32
- observedNodes.forEach(function (state, node) {
33
- var newRect = node.getBoundingClientRect();
34
-
35
- if (rectChanged(newRect, state.rect)) {
36
- state.rect = newRect;
37
- changedStates.push(state);
38
- }
39
- });
40
- changedStates.forEach(function (state) {
41
- state.callbacks.forEach(function (cb) {
42
- return cb(state.rect);
43
- });
44
- });
45
- rafId = window.requestAnimationFrame(run);
46
- };
47
-
48
- function observeRect(node, cb) {
49
- return {
50
- observe: function observe() {
51
- var wasEmpty = observedNodes.size === 0;
52
-
53
- if (observedNodes.has(node)) {
54
- observedNodes.get(node).callbacks.push(cb);
55
- } else {
56
- observedNodes.set(node, {
57
- rect: undefined,
58
- hasRectChanged: false,
59
- callbacks: [cb]
60
- });
61
- }
62
-
63
- if (wasEmpty) run();
64
- },
65
- unobserve: function unobserve() {
66
- var state = observedNodes.get(node);
67
-
68
- if (state) {
69
- // Remove the callback
70
- var index = state.callbacks.indexOf(cb);
71
- if (index >= 0) state.callbacks.splice(index, 1); // Remove the node reference
72
-
73
- if (!state.callbacks.length) observedNodes["delete"](node); // Stop the loop
74
-
75
- if (!observedNodes.size) cancelAnimationFrame(rafId);
76
- }
77
- }
78
- };
79
- }
80
-
81
11
  function memo(getDeps, fn, opts) {
82
12
  let deps = [];
83
13
  let result;
@@ -132,9 +62,29 @@ const defaultRangeExtractor = range => {
132
62
 
133
63
  return arr;
134
64
  };
65
+
66
+ const memoRectCallback = (instance, cb) => {
67
+ let prev = {
68
+ height: -1,
69
+ width: -1
70
+ };
71
+ return rect => {
72
+ if (instance.options.horizontal ? rect.width !== prev.width : rect.height !== prev.height) {
73
+ cb(rect);
74
+ }
75
+
76
+ prev = rect;
77
+ };
78
+ };
79
+
135
80
  const observeElementRect = (instance, cb) => {
136
- const observer = observeRect(instance.scrollElement, rect => {
137
- cb(rect);
81
+ const observer = new ResizeObserver(entries => {
82
+ var _entries$, _entries$2;
83
+
84
+ cb({
85
+ width: (_entries$ = entries[0]) == null ? void 0 : _entries$.contentRect.width,
86
+ height: (_entries$2 = entries[0]) == null ? void 0 : _entries$2.contentRect.height
87
+ });
138
88
  });
139
89
 
140
90
  if (!instance.scrollElement) {
@@ -142,18 +92,18 @@ const observeElementRect = (instance, cb) => {
142
92
  }
143
93
 
144
94
  cb(instance.scrollElement.getBoundingClientRect());
145
- observer.observe();
95
+ observer.observe(instance.scrollElement);
146
96
  return () => {
147
- observer.unobserve();
97
+ observer.unobserve(instance.scrollElement);
148
98
  };
149
99
  };
150
100
  const observeWindowRect = (instance, cb) => {
151
- const onResize = () => {
152
- cb({
153
- width: instance.scrollElement.innerWidth,
154
- height: instance.scrollElement.innerHeight
155
- });
156
- };
101
+ const memoizedCallback = memoRectCallback(instance, cb);
102
+
103
+ const onResize = () => memoizedCallback({
104
+ width: instance.scrollElement.innerWidth,
105
+ height: instance.scrollElement.innerHeight
106
+ });
157
107
 
158
108
  if (!instance.scrollElement) {
159
109
  return;
@@ -168,52 +118,80 @@ const observeWindowRect = (instance, cb) => {
168
118
  instance.scrollElement.removeEventListener('resize', onResize);
169
119
  };
170
120
  };
171
- const observeElementOffset = (instance, cb) => {
172
- const onScroll = () => cb(instance.scrollElement[instance.options.horizontal ? 'scrollLeft' : 'scrollTop']);
121
+ const scrollProps = {
122
+ element: ['scrollLeft', 'scrollTop'],
123
+ window: ['scrollX', 'scrollY']
124
+ };
173
125
 
174
- if (!instance.scrollElement) {
175
- return;
176
- }
126
+ const createOffsetObserver = mode => {
127
+ return (instance, cb) => {
128
+ if (!instance.scrollElement) {
129
+ return;
130
+ }
177
131
 
178
- onScroll();
179
- instance.scrollElement.addEventListener('scroll', onScroll, {
180
- capture: false,
181
- passive: true
182
- });
183
- return () => {
184
- instance.scrollElement.removeEventListener('scroll', onScroll);
185
- };
186
- };
187
- const observeWindowOffset = (instance, cb) => {
188
- const onScroll = () => cb(instance.scrollElement[instance.options.horizontal ? 'scrollX' : 'scrollY']);
132
+ const propX = scrollProps[mode][0];
133
+ const propY = scrollProps[mode][1];
134
+ let prevX = instance.scrollElement[propX];
135
+ let prevY = instance.scrollElement[propY];
189
136
 
190
- if (!instance.scrollElement) {
191
- return;
192
- }
137
+ const scroll = () => {
138
+ const offset = instance.scrollElement[instance.options.horizontal ? propX : propY];
139
+ cb(Math.max(0, offset - instance.options.scrollMargin));
140
+ };
193
141
 
194
- onScroll();
195
- instance.scrollElement.addEventListener('scroll', onScroll, {
196
- capture: false,
197
- passive: true
198
- });
199
- return () => {
200
- instance.scrollElement.removeEventListener('scroll', onScroll);
142
+ scroll();
143
+
144
+ const onScroll = e => {
145
+ const target = e.currentTarget;
146
+ const scrollX = target[propX];
147
+ const scrollY = target[propY];
148
+
149
+ if (instance.options.horizontal ? prevX - scrollX : prevY - scrollY) {
150
+ scroll();
151
+ }
152
+
153
+ prevX = scrollX;
154
+ prevY = scrollY;
155
+ };
156
+
157
+ instance.scrollElement.addEventListener('scroll', onScroll, {
158
+ capture: false,
159
+ passive: true
160
+ });
161
+ return () => {
162
+ instance.scrollElement.removeEventListener('scroll', onScroll);
163
+ };
201
164
  };
202
165
  };
166
+
167
+ const observeElementOffset = createOffsetObserver('element');
168
+ const observeWindowOffset = createOffsetObserver('window');
203
169
  const measureElement = (element, instance) => {
204
170
  return element.getBoundingClientRect()[instance.options.horizontal ? 'width' : 'height'];
205
171
  };
206
- const windowScroll = (offset, canSmooth, instance) => {
172
+ const windowScroll = (offset, _ref, instance) => {
207
173
  var _instance$scrollEleme;
208
- (_instance$scrollEleme = instance.scrollElement) == null ? void 0 : _instance$scrollEleme.scrollTo({
209
- [instance.options.horizontal ? 'left' : 'top']: offset,
174
+
175
+ let {
176
+ canSmooth,
177
+ sync
178
+ } = _ref;
179
+ const toOffset = sync ? offset : offset + instance.options.scrollMargin;
180
+ (_instance$scrollEleme = instance.scrollElement) == null ? void 0 : _instance$scrollEleme.scrollTo == null ? void 0 : _instance$scrollEleme.scrollTo({
181
+ [instance.options.horizontal ? 'left' : 'top']: toOffset,
210
182
  behavior: canSmooth ? 'smooth' : undefined
211
183
  });
212
184
  };
213
- const elementScroll = (offset, canSmooth, instance) => {
185
+ const elementScroll = (offset, _ref2, instance) => {
214
186
  var _instance$scrollEleme2;
215
- (_instance$scrollEleme2 = instance.scrollElement) == null ? void 0 : _instance$scrollEleme2.scrollTo({
216
- [instance.options.horizontal ? 'left' : 'top']: offset,
187
+
188
+ let {
189
+ canSmooth,
190
+ sync
191
+ } = _ref2;
192
+ const toOffset = sync ? offset : offset + instance.options.scrollMargin;
193
+ (_instance$scrollEleme2 = instance.scrollElement) == null ? void 0 : _instance$scrollEleme2.scrollTo == null ? void 0 : _instance$scrollEleme2.scrollTo({
194
+ [instance.options.horizontal ? 'left' : 'top']: toOffset,
217
195
  behavior: canSmooth ? 'smooth' : undefined
218
196
  });
219
197
  };
@@ -223,13 +201,21 @@ class Virtualizer {
223
201
 
224
202
  this.unsubs = [];
225
203
  this.scrollElement = null;
204
+ this.isScrolling = false;
205
+ this.isScrollingTimeoutId = null;
226
206
  this.measurementsCache = [];
227
207
  this.itemMeasurementsCache = {};
228
208
  this.pendingMeasuredCacheIndexes = [];
209
+ this.scrollDelta = 0;
210
+ this.measureElementCache = {};
211
+ this.range = {
212
+ startIndex: 0,
213
+ endIndex: 0
214
+ };
229
215
 
230
216
  this.setOptions = opts => {
231
- Object.entries(opts).forEach(_ref => {
232
- let [key, value] = _ref;
217
+ Object.entries(opts).forEach(_ref3 => {
218
+ let [key, value] = _ref3;
233
219
  if (typeof value === 'undefined') delete opts[key];
234
220
  });
235
221
  this.options = {
@@ -250,6 +236,8 @@ class Virtualizer {
250
236
  width: 0,
251
237
  height: 0
252
238
  },
239
+ scrollMargin: 0,
240
+ scrollingDelay: 150,
253
241
  ...opts
254
242
  };
255
243
  };
@@ -263,6 +251,7 @@ class Virtualizer {
263
251
  this.cleanup = () => {
264
252
  this.unsubs.filter(Boolean).forEach(d => d());
265
253
  this.unsubs = [];
254
+ this.scrollElement = null;
266
255
  };
267
256
 
268
257
  this._didMount = () => {
@@ -277,14 +266,41 @@ class Virtualizer {
277
266
  if (this.scrollElement !== scrollElement) {
278
267
  this.cleanup();
279
268
  this.scrollElement = scrollElement;
269
+
270
+ this._scrollToOffset(this.scrollOffset, {
271
+ canSmooth: false,
272
+ sync: true,
273
+ requested: false
274
+ });
275
+
280
276
  this.unsubs.push(this.options.observeElementRect(this, rect => {
281
277
  this.scrollRect = rect;
282
- this.notify();
278
+ this.calculateRange();
283
279
  }));
284
280
  this.unsubs.push(this.options.observeElementOffset(this, offset => {
285
- this.scrollOffset = offset;
286
- this.notify();
281
+ if (this.isScrollingTimeoutId !== null) {
282
+ clearTimeout(this.isScrollingTimeoutId);
283
+ this.isScrollingTimeoutId = null;
284
+ }
285
+
286
+ if (this.scrollOffset !== offset) {
287
+ this.scrollOffset = offset;
288
+ this.isScrolling = true;
289
+ this.scrollDelta = 0;
290
+ this.isScrollingTimeoutId = setTimeout(() => {
291
+ this.isScrollingTimeoutId = null;
292
+ this.isScrolling = false;
293
+ this.notify();
294
+ }, this.options.scrollingDelay);
295
+ } else {
296
+ this.isScrolling = false;
297
+ this.scrollDelta = 0;
298
+ }
299
+
300
+ this.calculateRange();
287
301
  }));
302
+ } else if (!this.isScrolling) {
303
+ this.calculateRange();
288
304
  }
289
305
  };
290
306
 
@@ -315,108 +331,134 @@ class Virtualizer {
315
331
  this.measurementsCache = measurements;
316
332
  return measurements;
317
333
  }, {
318
- key: process.env.NODE_ENV === 'development' && 'getMeasurements',
334
+ key: process.env.NODE_ENV !== 'production' && 'getMeasurements',
319
335
  debug: () => this.options.debug
320
336
  });
321
337
  this.calculateRange = memo(() => [this.getMeasurements(), this.getSize(), this.scrollOffset], (measurements, outerSize, scrollOffset) => {
322
- return calculateRange({
338
+ const range = calculateRange({
323
339
  measurements,
324
340
  outerSize,
325
341
  scrollOffset
326
342
  });
343
+
344
+ if (range.startIndex !== this.range.startIndex || range.endIndex !== this.range.endIndex) {
345
+ this.range = range;
346
+ this.notify();
347
+ }
348
+
349
+ return this.range;
327
350
  }, {
328
- key: process.env.NODE_ENV === 'development' && 'calculateRange',
351
+ key: process.env.NODE_ENV !== 'production' && 'calculateRange',
329
352
  debug: () => this.options.debug
330
353
  });
331
- this.getIndexes = memo(() => [this.options.rangeExtractor, this.calculateRange(), this.options.overscan, this.options.count], (rangeExtractor, range, overscan, count) => {
354
+ this.getIndexes = memo(() => [this.options.rangeExtractor, this.range, this.options.overscan, this.options.count], (rangeExtractor, range, overscan, count) => {
332
355
  return rangeExtractor({ ...range,
333
356
  overscan,
334
357
  count: count
335
358
  });
336
359
  }, {
337
- key: process.env.NODE_ENV === 'development' && 'getIndexes'
360
+ key: process.env.NODE_ENV !== 'production' && 'getIndexes',
361
+ debug: () => this.options.debug
338
362
  });
339
363
  this.getVirtualItems = memo(() => [this.getIndexes(), this.getMeasurements(), this.options.measureElement], (indexes, measurements, measureElement) => {
364
+ const makeMeasureElement = index => measurableItem => {
365
+ var _this$itemMeasurement;
366
+
367
+ const item = this.measurementsCache[index];
368
+
369
+ if (!measurableItem) {
370
+ return;
371
+ }
372
+
373
+ const measuredItemSize = measureElement(measurableItem, this);
374
+ const itemSize = (_this$itemMeasurement = this.itemMeasurementsCache[item.key]) != null ? _this$itemMeasurement : item.size;
375
+
376
+ if (measuredItemSize !== itemSize) {
377
+ if (item.start < this.scrollOffset) {
378
+ if (process.env.NODE_ENV !== 'production' && this.options.debug) {
379
+ console.info('correction', measuredItemSize - itemSize);
380
+ }
381
+
382
+ if (this.destinationOffset === undefined) {
383
+ this.scrollDelta += measuredItemSize - itemSize;
384
+
385
+ this._scrollToOffset(this.scrollOffset + this.scrollDelta, {
386
+ canSmooth: false,
387
+ sync: false,
388
+ requested: false
389
+ });
390
+ }
391
+ }
392
+
393
+ this.pendingMeasuredCacheIndexes.push(index);
394
+ this.itemMeasurementsCache = { ...this.itemMeasurementsCache,
395
+ [item.key]: measuredItemSize
396
+ };
397
+ this.notify();
398
+ }
399
+ };
400
+
340
401
  const virtualItems = [];
402
+ const currentMeasureElements = {};
341
403
 
342
404
  for (let k = 0, len = indexes.length; k < len; k++) {
405
+ var _this$measureElementC;
406
+
343
407
  const i = indexes[k];
344
408
  const measurement = measurements[i];
345
409
  const item = { ...measurement,
346
- measureElement: measurableItem => {
347
- if (measurableItem) {
348
- const measuredItemSize = measureElement(measurableItem, this);
349
-
350
- if (measuredItemSize !== item.size) {
351
- if (item.start < this.scrollOffset) {
352
- if (process.env.NODE_ENV === 'development' && this.options.debug) console.info('correction', measuredItemSize - item.size);
353
-
354
- if (!this.destinationOffset) {
355
- this._scrollToOffset(this.scrollOffset + (measuredItemSize - item.size), false);
356
- }
357
- }
358
-
359
- this.pendingMeasuredCacheIndexes.push(i);
360
- this.itemMeasurementsCache = { ...this.itemMeasurementsCache,
361
- [item.key]: measuredItemSize
362
- };
363
- this.notify();
364
- }
365
- }
366
- }
410
+ measureElement: currentMeasureElements[i] = (_this$measureElementC = this.measureElementCache[i]) != null ? _this$measureElementC : makeMeasureElement(i)
367
411
  };
368
412
  virtualItems.push(item);
369
413
  }
370
414
 
415
+ this.measureElementCache = currentMeasureElements;
371
416
  return virtualItems;
372
417
  }, {
373
- key: process.env.NODE_ENV === 'development' && 'getIndexes'
418
+ key: process.env.NODE_ENV !== 'production' && 'getIndexes',
419
+ debug: () => this.options.debug
374
420
  });
375
421
 
376
422
  this.scrollToOffset = function (toOffset, _temp) {
377
423
  let {
378
- align
379
- } = _temp === void 0 ? {
380
- align: 'start'
381
- } : _temp;
382
-
383
- const attempt = () => {
384
- const offset = _this.scrollOffset;
424
+ align = 'start',
425
+ smoothScroll = _this.options.enableSmoothScroll
426
+ } = _temp === void 0 ? {} : _temp;
427
+ const offset = _this.scrollOffset;
385
428
 
386
- const size = _this.getSize();
429
+ const size = _this.getSize();
387
430
 
388
- if (align === 'auto') {
389
- if (toOffset <= offset) {
390
- align = 'start';
391
- } else if (toOffset >= offset + size) {
392
- align = 'end';
393
- } else {
394
- align = 'start';
395
- }
431
+ if (align === 'auto') {
432
+ if (toOffset <= offset) {
433
+ align = 'start';
434
+ } else if (toOffset >= offset + size) {
435
+ align = 'end';
436
+ } else {
437
+ align = 'start';
396
438
  }
439
+ }
397
440
 
398
- if (align === 'start') {
399
- _this._scrollToOffset(toOffset, true);
400
- } else if (align === 'end') {
401
- _this._scrollToOffset(toOffset - size, true);
402
- } else if (align === 'center') {
403
- _this._scrollToOffset(toOffset - size / 2, true);
404
- }
441
+ const options = {
442
+ canSmooth: smoothScroll,
443
+ sync: false,
444
+ requested: true
405
445
  };
406
446
 
407
- attempt();
408
- requestAnimationFrame(() => {
409
- attempt();
410
- });
447
+ if (align === 'start') {
448
+ _this._scrollToOffset(toOffset, options);
449
+ } else if (align === 'end') {
450
+ _this._scrollToOffset(toOffset - size, options);
451
+ } else if (align === 'center') {
452
+ _this._scrollToOffset(toOffset - size / 2, options);
453
+ }
411
454
  };
412
455
 
413
456
  this.scrollToIndex = function (index, _temp2) {
414
457
  let {
415
- align,
458
+ align = 'auto',
459
+ smoothScroll = _this.options.enableSmoothScroll,
416
460
  ...rest
417
- } = _temp2 === void 0 ? {
418
- align: 'auto'
419
- } : _temp2;
461
+ } = _temp2 === void 0 ? {} : _temp2;
420
462
 
421
463
  const measurements = _this.getMeasurements();
422
464
 
@@ -447,6 +489,7 @@ class Virtualizer {
447
489
 
448
490
  _this.scrollToOffset(toOffset, {
449
491
  align,
492
+ smoothScroll,
450
493
  ...rest
451
494
  });
452
495
  };
@@ -457,10 +500,22 @@ class Virtualizer {
457
500
  return (((_this$getMeasurements = this.getMeasurements()[this.options.count - 1]) == null ? void 0 : _this$getMeasurements.end) || this.options.paddingStart) + this.options.paddingEnd;
458
501
  };
459
502
 
460
- this._scrollToOffset = (offset, canSmooth) => {
503
+ this._scrollToOffset = (offset, _ref4) => {
504
+ let {
505
+ requested,
506
+ canSmooth,
507
+ sync
508
+ } = _ref4;
461
509
  clearTimeout(this.scrollCheckFrame);
462
- this.destinationOffset = offset;
463
- this.options.scrollToFn(offset, this.options.enableSmoothScroll && canSmooth, this);
510
+
511
+ if (requested) {
512
+ this.destinationOffset = offset;
513
+ }
514
+
515
+ this.options.scrollToFn(offset, {
516
+ canSmooth,
517
+ sync
518
+ }, this);
464
519
  let scrollCheckFrame;
465
520
 
466
521
  const check = () => {
@@ -491,6 +546,7 @@ class Virtualizer {
491
546
  this.setOptions(_opts);
492
547
  this.scrollRect = this.options.initialRect;
493
548
  this.scrollOffset = this.options.initialOffset;
549
+ this.calculateRange();
494
550
  }
495
551
 
496
552
  }
@@ -516,12 +572,12 @@ const findNearestBinarySearch = (low, high, getCurrentValue, value) => {
516
572
  }
517
573
  };
518
574
 
519
- function calculateRange(_ref2) {
575
+ function calculateRange(_ref5) {
520
576
  let {
521
577
  measurements,
522
578
  outerSize,
523
579
  scrollOffset
524
- } = _ref2;
580
+ } = _ref5;
525
581
  const count = measurements.length - 1;
526
582
 
527
583
  const getOffset = index => measurements[index].start;