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