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

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
- return element.getBoundingClientRect()[instance.options.horizontal ? 'width' : 'height'];
176
+ return Math.round(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,26 @@
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.ro = new ResizeObserver(entries => {
218
+ entries.forEach(entry => {
219
+ this._measureElement(entry.target, false);
220
+ });
221
+ });
222
+ this.range = {
223
+ startIndex: 0,
224
+ endIndex: 0
225
+ };
235
226
 
236
227
  this.setOptions = opts => {
237
- Object.entries(opts).forEach(_ref => {
238
- let [key, value] = _ref;
228
+ Object.entries(opts).forEach(_ref3 => {
229
+ let [key, value] = _ref3;
239
230
  if (typeof value === 'undefined') delete opts[key];
240
231
  });
241
232
  this.options = {
@@ -256,6 +247,9 @@
256
247
  width: 0,
257
248
  height: 0
258
249
  },
250
+ scrollMargin: 0,
251
+ scrollingDelay: 150,
252
+ indexAttribute: 'data-index',
259
253
  ...opts
260
254
  };
261
255
  };
@@ -269,10 +263,13 @@
269
263
  this.cleanup = () => {
270
264
  this.unsubs.filter(Boolean).forEach(d => d());
271
265
  this.unsubs = [];
266
+ this.scrollElement = null;
272
267
  };
273
268
 
274
269
  this._didMount = () => {
275
270
  return () => {
271
+ this.ro.disconnect();
272
+ this.measureElementCache = {};
276
273
  this.cleanup();
277
274
  };
278
275
  };
@@ -283,14 +280,41 @@
283
280
  if (this.scrollElement !== scrollElement) {
284
281
  this.cleanup();
285
282
  this.scrollElement = scrollElement;
283
+
284
+ this._scrollToOffset(this.scrollOffset, {
285
+ canSmooth: false,
286
+ sync: true,
287
+ requested: false
288
+ });
289
+
286
290
  this.unsubs.push(this.options.observeElementRect(this, rect => {
287
291
  this.scrollRect = rect;
288
- this.notify();
292
+ this.calculateRange();
289
293
  }));
290
294
  this.unsubs.push(this.options.observeElementOffset(this, offset => {
291
- this.scrollOffset = offset;
292
- this.notify();
295
+ if (this.isScrollingTimeoutId !== null) {
296
+ clearTimeout(this.isScrollingTimeoutId);
297
+ this.isScrollingTimeoutId = null;
298
+ }
299
+
300
+ if (this.scrollOffset !== offset) {
301
+ this.scrollOffset = offset;
302
+ this.isScrolling = true;
303
+ this.scrollDelta = 0;
304
+ this.isScrollingTimeoutId = setTimeout(() => {
305
+ this.isScrollingTimeoutId = null;
306
+ this.isScrolling = false;
307
+ this.notify();
308
+ }, this.options.scrollingDelay);
309
+ } else {
310
+ this.isScrolling = false;
311
+ this.scrollDelta = 0;
312
+ }
313
+
314
+ this.calculateRange();
293
315
  }));
316
+ } else if (!this.isScrolling) {
317
+ this.calculateRange();
294
318
  }
295
319
  };
296
320
 
@@ -325,104 +349,166 @@
325
349
  debug: () => this.options.debug
326
350
  });
327
351
  this.calculateRange = memo(() => [this.getMeasurements(), this.getSize(), this.scrollOffset], (measurements, outerSize, scrollOffset) => {
328
- return calculateRange({
352
+ const range = calculateRange({
329
353
  measurements,
330
354
  outerSize,
331
355
  scrollOffset
332
356
  });
357
+
358
+ if (range.startIndex !== this.range.startIndex || range.endIndex !== this.range.endIndex) {
359
+ this.range = range;
360
+ this.notify();
361
+ }
362
+
363
+ return this.range;
333
364
  }, {
334
365
  key: 'calculateRange',
335
366
  debug: () => this.options.debug
336
367
  });
337
- this.getIndexes = memo(() => [this.options.rangeExtractor, this.calculateRange(), this.options.overscan, this.options.count], (rangeExtractor, range, overscan, count) => {
368
+ this.getIndexes = memo(() => [this.options.rangeExtractor, this.range, this.options.overscan, this.options.count], (rangeExtractor, range, overscan, count) => {
338
369
  return rangeExtractor({ ...range,
339
370
  overscan,
340
371
  count: count
341
372
  });
342
373
  }, {
343
- key: 'getIndexes'
374
+ key: 'getIndexes',
375
+ debug: () => this.options.debug
344
376
  });
345
- this.getVirtualItems = memo(() => [this.getIndexes(), this.getMeasurements(), this.options.measureElement], (indexes, measurements, measureElement) => {
377
+
378
+ this.indexFromElement = node => {
379
+ const attributeName = this.options.indexAttribute;
380
+ const indexStr = node.getAttribute(attributeName);
381
+
382
+ if (!indexStr) {
383
+ console.warn("Missing attribute name '" + attributeName + "={index}' on measured element.");
384
+ return -1;
385
+ }
386
+
387
+ return parseInt(indexStr, 10);
388
+ };
389
+
390
+ this._measureElement = (node, _sync) => {
391
+ var _this$itemMeasurement;
392
+
393
+ const index = this.indexFromElement(node);
394
+ const item = this.measurementsCache[index];
395
+
396
+ if (!item) {
397
+ return;
398
+ }
399
+
400
+ const key = String(item.key);
401
+ const prevNode = this.measureElementCache[key];
402
+
403
+ if (!node.isConnected) {
404
+ if (prevNode) {
405
+ this.ro.unobserve(prevNode);
406
+ delete this.measureElementCache[key];
407
+ }
408
+
409
+ return;
410
+ }
411
+
412
+ if (!prevNode || prevNode !== node) {
413
+ if (prevNode) {
414
+ this.ro.unobserve(prevNode);
415
+ }
416
+
417
+ this.measureElementCache[key] = node;
418
+ this.ro.observe(node);
419
+ }
420
+
421
+ const measuredItemSize = this.options.measureElement(node, this);
422
+ const itemSize = (_this$itemMeasurement = this.itemMeasurementsCache[item.key]) != null ? _this$itemMeasurement : item.size;
423
+
424
+ if (measuredItemSize !== itemSize) {
425
+ if (item.start < this.scrollOffset) {
426
+ if (this.options.debug) {
427
+ console.info('correction', measuredItemSize - itemSize);
428
+ }
429
+
430
+ if (this.destinationOffset === undefined) {
431
+ this.scrollDelta += measuredItemSize - itemSize;
432
+
433
+ this._scrollToOffset(this.scrollOffset + this.scrollDelta, {
434
+ canSmooth: false,
435
+ sync: false,
436
+ requested: false
437
+ });
438
+ }
439
+ }
440
+
441
+ this.pendingMeasuredCacheIndexes.push(index);
442
+ this.itemMeasurementsCache = { ...this.itemMeasurementsCache,
443
+ [item.key]: measuredItemSize
444
+ };
445
+ this.notify();
446
+ }
447
+ };
448
+
449
+ this.measureElement = node => {
450
+ if (!node) {
451
+ return;
452
+ }
453
+
454
+ this._measureElement(node, true);
455
+ };
456
+
457
+ this.getVirtualItems = memo(() => [this.getIndexes(), this.getMeasurements()], (indexes, measurements) => {
346
458
  const virtualItems = [];
347
459
 
348
460
  for (let k = 0, len = indexes.length; k < len; k++) {
349
461
  const i = indexes[k];
350
462
  const measurement = measurements[i];
351
- 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
- }
373
- };
374
- virtualItems.push(item);
463
+ virtualItems.push(measurement);
375
464
  }
376
465
 
377
466
  return virtualItems;
378
467
  }, {
379
- key: 'getIndexes'
468
+ key: 'getIndexes',
469
+ debug: () => this.options.debug
380
470
  });
381
471
 
382
472
  this.scrollToOffset = function (toOffset, _temp) {
383
473
  let {
384
- align
385
- } = _temp === void 0 ? {
386
- align: 'start'
387
- } : _temp;
388
-
389
- const attempt = () => {
390
- const offset = _this.scrollOffset;
474
+ align = 'start',
475
+ smoothScroll = _this.options.enableSmoothScroll
476
+ } = _temp === void 0 ? {} : _temp;
477
+ const offset = _this.scrollOffset;
391
478
 
392
- const size = _this.getSize();
479
+ const size = _this.getSize();
393
480
 
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
- }
481
+ if (align === 'auto') {
482
+ if (toOffset <= offset) {
483
+ align = 'start';
484
+ } else if (toOffset >= offset + size) {
485
+ align = 'end';
486
+ } else {
487
+ align = 'start';
402
488
  }
489
+ }
403
490
 
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
- }
491
+ const options = {
492
+ canSmooth: smoothScroll,
493
+ sync: false,
494
+ requested: true
411
495
  };
412
496
 
413
- attempt();
414
- requestAnimationFrame(() => {
415
- attempt();
416
- });
497
+ if (align === 'start') {
498
+ _this._scrollToOffset(toOffset, options);
499
+ } else if (align === 'end') {
500
+ _this._scrollToOffset(toOffset - size, options);
501
+ } else if (align === 'center') {
502
+ _this._scrollToOffset(toOffset - size / 2, options);
503
+ }
417
504
  };
418
505
 
419
506
  this.scrollToIndex = function (index, _temp2) {
420
507
  let {
421
- align,
508
+ align = 'auto',
509
+ smoothScroll = _this.options.enableSmoothScroll,
422
510
  ...rest
423
- } = _temp2 === void 0 ? {
424
- align: 'auto'
425
- } : _temp2;
511
+ } = _temp2 === void 0 ? {} : _temp2;
426
512
 
427
513
  const measurements = _this.getMeasurements();
428
514
 
@@ -453,6 +539,7 @@
453
539
 
454
540
  _this.scrollToOffset(toOffset, {
455
541
  align,
542
+ smoothScroll,
456
543
  ...rest
457
544
  });
458
545
  };
@@ -463,10 +550,22 @@
463
550
  return (((_this$getMeasurements = this.getMeasurements()[this.options.count - 1]) == null ? void 0 : _this$getMeasurements.end) || this.options.paddingStart) + this.options.paddingEnd;
464
551
  };
465
552
 
466
- this._scrollToOffset = (offset, canSmooth) => {
553
+ this._scrollToOffset = (offset, _ref4) => {
554
+ let {
555
+ requested,
556
+ canSmooth,
557
+ sync
558
+ } = _ref4;
467
559
  clearTimeout(this.scrollCheckFrame);
468
- this.destinationOffset = offset;
469
- this.options.scrollToFn(offset, this.options.enableSmoothScroll && canSmooth, this);
560
+
561
+ if (requested) {
562
+ this.destinationOffset = offset;
563
+ }
564
+
565
+ this.options.scrollToFn(offset, {
566
+ canSmooth,
567
+ sync
568
+ }, this);
470
569
  let scrollCheckFrame;
471
570
 
472
571
  const check = () => {
@@ -497,6 +596,7 @@
497
596
  this.setOptions(_opts);
498
597
  this.scrollRect = this.options.initialRect;
499
598
  this.scrollOffset = this.options.initialOffset;
599
+ this.calculateRange();
500
600
  }
501
601
 
502
602
  }
@@ -522,12 +622,12 @@
522
622
  }
523
623
  };
524
624
 
525
- function calculateRange(_ref2) {
625
+ function calculateRange(_ref5) {
526
626
  let {
527
627
  measurements,
528
628
  outerSize,
529
629
  scrollOffset
530
- } = _ref2;
630
+ } = _ref5;
531
631
  const count = measurements.length - 1;
532
632
 
533
633
  const getOffset = index => measurements[index].start;