virtua 0.38.1 → 0.38.3

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.
@@ -0,0 +1,1558 @@
1
+ import { mergeProps, createEffect, onCleanup, createMemo, createRoot, createSignal, onMount, createComputed, on } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
3
+
4
+ /** @internal */
5
+ const NULL = null;
6
+ /** @internal */
7
+ const { min, max, abs, floor } = Math;
8
+ /**
9
+ * @internal
10
+ */
11
+ const clamp = (value, minValue, maxValue) => min(maxValue, max(minValue, value));
12
+ /**
13
+ * @internal
14
+ */
15
+ const sort = (arr) => {
16
+ return [...arr].sort((a, b) => a - b);
17
+ };
18
+ /**
19
+ * @internal
20
+ */
21
+ const microtask = typeof queueMicrotask === "function"
22
+ ? queueMicrotask
23
+ : (fn) => {
24
+ Promise.resolve().then(fn);
25
+ };
26
+ /**
27
+ * @internal
28
+ */
29
+ const once = (fn) => {
30
+ let called;
31
+ let cache;
32
+ return () => {
33
+ if (!called) {
34
+ called = true;
35
+ cache = fn();
36
+ }
37
+ return cache;
38
+ };
39
+ };
40
+
41
+ /** @internal */
42
+ const UNCACHED = -1;
43
+ const fill = (array, length, prepend) => {
44
+ const key = prepend ? "unshift" : "push";
45
+ for (let i = 0; i < length; i++) {
46
+ array[key](UNCACHED);
47
+ }
48
+ return array;
49
+ };
50
+ /**
51
+ * @internal
52
+ */
53
+ const getItemSize = (cache, index) => {
54
+ const size = cache._sizes[index];
55
+ return size === UNCACHED ? cache._defaultItemSize : size;
56
+ };
57
+ /**
58
+ * @internal
59
+ */
60
+ const setItemSize = (cache, index, size) => {
61
+ const isInitialMeasurement = cache._sizes[index] === UNCACHED;
62
+ cache._sizes[index] = size;
63
+ // mark as dirty
64
+ cache._computedOffsetIndex = min(index, cache._computedOffsetIndex);
65
+ return isInitialMeasurement;
66
+ };
67
+ /**
68
+ * @internal
69
+ */
70
+ const computeOffset = (cache, index) => {
71
+ if (!cache._length)
72
+ return 0;
73
+ if (cache._computedOffsetIndex >= index) {
74
+ return cache._offsets[index];
75
+ }
76
+ if (cache._computedOffsetIndex < 0) {
77
+ // first offset must be 0 to avoid returning NaN, which can cause infinite rerender.
78
+ // https://github.com/inokawa/virtua/pull/160
79
+ cache._offsets[0] = 0;
80
+ cache._computedOffsetIndex = 0;
81
+ }
82
+ let i = cache._computedOffsetIndex;
83
+ let top = cache._offsets[i];
84
+ while (i < index) {
85
+ top += getItemSize(cache, i);
86
+ cache._offsets[++i] = top;
87
+ }
88
+ // mark as measured
89
+ cache._computedOffsetIndex = index;
90
+ return top;
91
+ };
92
+ /**
93
+ * @internal
94
+ */
95
+ const computeTotalSize = (cache) => {
96
+ if (!cache._length)
97
+ return 0;
98
+ return (computeOffset(cache, cache._length - 1) +
99
+ getItemSize(cache, cache._length - 1));
100
+ };
101
+ /**
102
+ * Finds the index of an item in the cache whose computed offset is closest to the specified offset.
103
+ *
104
+ * @internal
105
+ */
106
+ const findIndex = (cache, offset, low = 0, high = cache._length - 1) => {
107
+ // Find with binary search
108
+ while (low <= high) {
109
+ const mid = floor((low + high) / 2);
110
+ const itemOffset = computeOffset(cache, mid);
111
+ if (itemOffset <= offset) {
112
+ if (itemOffset + getItemSize(cache, mid) > offset) {
113
+ return mid;
114
+ }
115
+ low = mid + 1;
116
+ }
117
+ else {
118
+ high = mid - 1;
119
+ }
120
+ }
121
+ return clamp(low, 0, cache._length - 1);
122
+ };
123
+ /**
124
+ * @internal
125
+ */
126
+ const computeRange = (cache, scrollOffset, viewportSize, prevStartIndex) => {
127
+ // Clamp because prevStartIndex may exceed the limit when children decreased a lot after scrolling
128
+ prevStartIndex = min(prevStartIndex, cache._length - 1);
129
+ if (computeOffset(cache, prevStartIndex) <= scrollOffset) {
130
+ // search forward
131
+ // start <= end, prevStartIndex <= start
132
+ const end = findIndex(cache, scrollOffset + viewportSize, prevStartIndex);
133
+ return [findIndex(cache, scrollOffset, prevStartIndex, end), end];
134
+ }
135
+ else {
136
+ // search backward
137
+ // start <= end, start <= prevStartIndex
138
+ const start = findIndex(cache, scrollOffset, undefined, prevStartIndex);
139
+ return [start, findIndex(cache, scrollOffset + viewportSize, start)];
140
+ }
141
+ };
142
+ /**
143
+ * @internal
144
+ */
145
+ const estimateDefaultItemSize = (cache, startIndex) => {
146
+ let measuredCountBeforeStart = 0;
147
+ // This function will be called after measurement so measured size array must be longer than 0
148
+ const measuredSizes = [];
149
+ cache._sizes.forEach((s, i) => {
150
+ if (s !== UNCACHED) {
151
+ measuredSizes.push(s);
152
+ if (i < startIndex) {
153
+ measuredCountBeforeStart++;
154
+ }
155
+ }
156
+ });
157
+ // Discard cache for now
158
+ cache._computedOffsetIndex = -1;
159
+ // Calculate median
160
+ const sorted = sort(measuredSizes);
161
+ const len = sorted.length;
162
+ const mid = (len / 2) | 0;
163
+ const median = len % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
164
+ const prevDefaultItemSize = cache._defaultItemSize;
165
+ // Calculate diff of unmeasured items before start
166
+ return (((cache._defaultItemSize = median) - prevDefaultItemSize) *
167
+ max(startIndex - measuredCountBeforeStart, 0));
168
+ };
169
+ /**
170
+ * @internal
171
+ */
172
+ const initCache = (length, itemSize, snapshot) => {
173
+ return {
174
+ _defaultItemSize: snapshot ? snapshot[1] : itemSize,
175
+ _sizes: snapshot && snapshot[0]
176
+ ? // https://github.com/inokawa/virtua/issues/441
177
+ fill(snapshot[0].slice(0, min(length, snapshot[0].length)), max(0, length - snapshot[0].length))
178
+ : fill([], length),
179
+ _length: length,
180
+ _computedOffsetIndex: -1,
181
+ _offsets: fill([], length),
182
+ };
183
+ };
184
+ /**
185
+ * @internal
186
+ */
187
+ const takeCacheSnapshot = (cache) => {
188
+ return [cache._sizes.slice(), cache._defaultItemSize];
189
+ };
190
+ /**
191
+ * @internal
192
+ */
193
+ const updateCacheLength = (cache, length, isShift) => {
194
+ const diff = length - cache._length;
195
+ cache._computedOffsetIndex = isShift
196
+ ? // Discard cache for now
197
+ -1
198
+ : min(length - 1, cache._computedOffsetIndex);
199
+ cache._length = length;
200
+ if (diff > 0) {
201
+ // Added
202
+ fill(cache._offsets, diff);
203
+ fill(cache._sizes, diff, isShift);
204
+ return cache._defaultItemSize * diff;
205
+ }
206
+ else {
207
+ // Removed
208
+ cache._offsets.splice(diff);
209
+ return (isShift ? cache._sizes.splice(0, -diff) : cache._sizes.splice(diff)).reduce((acc, removed) => acc - (removed === UNCACHED ? cache._defaultItemSize : removed), 0);
210
+ }
211
+ };
212
+
213
+ /**
214
+ * @internal
215
+ */
216
+ const isBrowser = typeof window !== "undefined";
217
+ const getDocumentElement = () => document.documentElement;
218
+ /**
219
+ * @internal
220
+ */
221
+ const getCurrentDocument = (node) => node.ownerDocument;
222
+ /**
223
+ * @internal
224
+ */
225
+ const getCurrentWindow = (doc) => doc.defaultView;
226
+ /**
227
+ * @internal
228
+ */
229
+ const isRTLDocument = /*#__PURE__*/ once(() => {
230
+ // TODO support SSR in rtl
231
+ return isBrowser
232
+ ? getComputedStyle(getDocumentElement()).direction === "rtl"
233
+ : false;
234
+ });
235
+ /**
236
+ * Currently, all browsers on iOS/iPadOS are WebKit, including WebView.
237
+ * @internal
238
+ */
239
+ const isIOSWebKit = /*#__PURE__*/ once(() => {
240
+ return /iP(hone|od|ad)/.test(navigator.userAgent);
241
+ });
242
+ /**
243
+ * @internal
244
+ */
245
+ const isSmoothScrollSupported = /*#__PURE__*/ once(() => {
246
+ return "scrollBehavior" in getDocumentElement().style;
247
+ });
248
+
249
+ const SCROLL_IDLE = 0;
250
+ const SCROLL_DOWN = 1;
251
+ const SCROLL_UP = 2;
252
+ const SCROLL_BY_NATIVE = 0;
253
+ const SCROLL_BY_MANUAL_SCROLL = 1;
254
+ const SCROLL_BY_SHIFT = 2;
255
+ /** @internal */
256
+ const ACTION_SCROLL = 1;
257
+ /** @internal */
258
+ const ACTION_SCROLL_END = 2;
259
+ /** @internal */
260
+ const ACTION_ITEM_RESIZE = 3;
261
+ /** @internal */
262
+ const ACTION_VIEWPORT_RESIZE = 4;
263
+ /** @internal */
264
+ const ACTION_ITEMS_LENGTH_CHANGE = 5;
265
+ /** @internal */
266
+ const ACTION_START_OFFSET_CHANGE = 6;
267
+ /** @internal */
268
+ const ACTION_MANUAL_SCROLL = 7;
269
+ /** @internal */
270
+ const ACTION_BEFORE_MANUAL_SMOOTH_SCROLL = 8;
271
+ /** @internal */
272
+ const UPDATE_VIRTUAL_STATE = 0b0001;
273
+ /** @internal */
274
+ const UPDATE_SIZE_EVENT = 0b0010;
275
+ /** @internal */
276
+ const UPDATE_SCROLL_EVENT = 0b0100;
277
+ /** @internal */
278
+ const UPDATE_SCROLL_END_EVENT = 0b1000;
279
+ /**
280
+ * @internal
281
+ */
282
+ const getScrollSize = (store) => {
283
+ return max(store.$getTotalSize(), store.$getViewportSize());
284
+ };
285
+ /**
286
+ * @internal
287
+ */
288
+ const isInitialMeasurementDone = (store) => {
289
+ return !!store.$getViewportSize();
290
+ };
291
+ /**
292
+ * @internal
293
+ */
294
+ const createVirtualStore = (elementsCount, itemSize = 40, overscan = 4, ssrCount = 0, cacheSnapshot, shouldAutoEstimateItemSize = false) => {
295
+ let isSSR = !!ssrCount;
296
+ let stateVersion = [];
297
+ let viewportSize = 0;
298
+ let startSpacerSize = 0;
299
+ let scrollOffset = 0;
300
+ let jumpCount = 0;
301
+ let jump = 0;
302
+ let pendingJump = 0;
303
+ let _flushedJump = 0;
304
+ let _scrollDirection = SCROLL_IDLE;
305
+ let _scrollMode = SCROLL_BY_NATIVE;
306
+ let _frozenRange = isSSR
307
+ ? [0, max(ssrCount - 1, 0)]
308
+ : NULL;
309
+ let _prevRange = [0, 0];
310
+ let _totalMeasuredSize = 0;
311
+ const cache = initCache(elementsCount, itemSize, cacheSnapshot);
312
+ const subscribers = new Set();
313
+ const getRelativeScrollOffset = () => scrollOffset - startSpacerSize;
314
+ const getVisibleOffset = () => getRelativeScrollOffset() + pendingJump + jump;
315
+ const getRange = (offset) => {
316
+ return computeRange(cache, offset, viewportSize, _prevRange[0]);
317
+ };
318
+ const getTotalSize = () => computeTotalSize(cache);
319
+ const getItemOffset = (index) => {
320
+ return computeOffset(cache, index) - pendingJump;
321
+ };
322
+ const getItemSize$1 = (index) => {
323
+ return getItemSize(cache, index);
324
+ };
325
+ const applyJump = (j) => {
326
+ if (j) {
327
+ // In iOS WebKit browsers, updating scroll position will stop scrolling so it have to be deferred during scrolling.
328
+ if (isIOSWebKit() && _scrollDirection !== SCROLL_IDLE) {
329
+ pendingJump += j;
330
+ }
331
+ else {
332
+ jump += j;
333
+ jumpCount++;
334
+ }
335
+ }
336
+ };
337
+ return {
338
+ $getStateVersion: () => stateVersion,
339
+ $getCacheSnapshot: () => {
340
+ return takeCacheSnapshot(cache);
341
+ },
342
+ $getRange: () => {
343
+ // Return previous range for consistent render until next scroll event comes in.
344
+ if (_flushedJump) {
345
+ return _prevRange;
346
+ }
347
+ let [startIndex, endIndex] = getRange(max(0, getVisibleOffset()));
348
+ if (_frozenRange) {
349
+ startIndex = min(startIndex, _frozenRange[0]);
350
+ endIndex = max(endIndex, _frozenRange[1]);
351
+ }
352
+ if (_scrollDirection !== SCROLL_DOWN) {
353
+ startIndex -= max(0, overscan);
354
+ }
355
+ if (_scrollDirection !== SCROLL_UP) {
356
+ endIndex += max(0, overscan);
357
+ }
358
+ return (_prevRange = [
359
+ max(startIndex, 0),
360
+ min(endIndex, cache._length - 1),
361
+ ]);
362
+ },
363
+ $findStartIndex: () => findIndex(cache, getVisibleOffset()),
364
+ $findEndIndex: () => findIndex(cache, getVisibleOffset() + viewportSize),
365
+ $isUnmeasuredItem: (index) => cache._sizes[index] === UNCACHED,
366
+ _hasUnmeasuredItemsInFrozenRange: () => {
367
+ if (!_frozenRange)
368
+ return false;
369
+ return cache._sizes
370
+ .slice(max(0, _frozenRange[0] - 1), min(cache._length - 1, _frozenRange[1] + 1) + 1)
371
+ .includes(UNCACHED);
372
+ },
373
+ $getItemOffset: getItemOffset,
374
+ $getItemSize: getItemSize$1,
375
+ $getItemsLength: () => cache._length,
376
+ $getScrollOffset: () => scrollOffset,
377
+ $isScrolling: () => _scrollDirection !== SCROLL_IDLE,
378
+ $getViewportSize: () => viewportSize,
379
+ $getStartSpacerSize: () => startSpacerSize,
380
+ $getTotalSize: getTotalSize,
381
+ $getJumpCount: () => jumpCount,
382
+ _flushJump: () => {
383
+ _flushedJump = jump;
384
+ jump = 0;
385
+ return [
386
+ _flushedJump,
387
+ // Use absolute position not to exceed scrollable bounds
388
+ _scrollMode === SCROLL_BY_SHIFT ||
389
+ // https://github.com/inokawa/virtua/discussions/475
390
+ getRelativeScrollOffset() + viewportSize >= getTotalSize(),
391
+ ];
392
+ },
393
+ $subscribe: (target, cb) => {
394
+ const sub = [target, cb];
395
+ subscribers.add(sub);
396
+ return () => {
397
+ subscribers.delete(sub);
398
+ };
399
+ },
400
+ $update: (type, payload) => {
401
+ let shouldFlushPendingJump;
402
+ let shouldSync;
403
+ let mutated = 0;
404
+ switch (type) {
405
+ case ACTION_SCROLL: {
406
+ const flushedJump = _flushedJump;
407
+ _flushedJump = 0;
408
+ const delta = payload - scrollOffset;
409
+ const distance = abs(delta);
410
+ // Scroll event after jump compensation is not reliable because it may result in the opposite direction.
411
+ // The delta of artificial scroll may not be equal with the jump because it may be batched with other scrolls.
412
+ // And at least in latest Chrome/Firefox/Safari in 2023, setting value to scrollTop/scrollLeft can lose subpixel because its integer (sometimes float probably depending on dpr).
413
+ const isJustJumped = flushedJump && distance < abs(flushedJump) + 1;
414
+ // Scroll events are dispatched enough so it's ok to skip some of them.
415
+ if (!isJustJumped &&
416
+ // Ignore until manual scrolling
417
+ _scrollMode === SCROLL_BY_NATIVE) {
418
+ _scrollDirection = delta < 0 ? SCROLL_UP : SCROLL_DOWN;
419
+ }
420
+ // TODO This will cause glitch in reverse infinite scrolling. Disable this until better solution is found.
421
+ // if (
422
+ // pendingJump &&
423
+ // ((_scrollDirection === SCROLL_UP &&
424
+ // payload - max(pendingJump, 0) <= 0) ||
425
+ // (_scrollDirection === SCROLL_DOWN &&
426
+ // payload - min(pendingJump, 0) >= getScrollOffsetMax()))
427
+ // ) {
428
+ // // Flush if almost reached to start or end
429
+ // shouldFlushPendingJump = true;
430
+ // }
431
+ if (isSSR) {
432
+ _frozenRange = NULL;
433
+ isSSR = false;
434
+ }
435
+ scrollOffset = payload;
436
+ mutated = UPDATE_SCROLL_EVENT;
437
+ // Skip if offset is not changed
438
+ // Scroll offset may exceed min or max especially in Safari's elastic scrolling.
439
+ const relativeOffset = getRelativeScrollOffset();
440
+ if (relativeOffset >= -viewportSize &&
441
+ relativeOffset <= getTotalSize()) {
442
+ mutated += UPDATE_VIRTUAL_STATE;
443
+ // Update synchronously if scrolled a lot
444
+ shouldSync = distance > viewportSize;
445
+ }
446
+ break;
447
+ }
448
+ case ACTION_SCROLL_END: {
449
+ mutated = UPDATE_SCROLL_END_EVENT;
450
+ if (_scrollDirection !== SCROLL_IDLE) {
451
+ shouldFlushPendingJump = true;
452
+ mutated += UPDATE_VIRTUAL_STATE;
453
+ }
454
+ _scrollDirection = SCROLL_IDLE;
455
+ _scrollMode = SCROLL_BY_NATIVE;
456
+ _frozenRange = NULL;
457
+ break;
458
+ }
459
+ case ACTION_ITEM_RESIZE: {
460
+ const updated = payload.filter(([index, size]) => cache._sizes[index] !== size);
461
+ // Skip if all items are cached and not updated
462
+ if (!updated.length) {
463
+ break;
464
+ }
465
+ // Calculate jump by resize to minimize junks in appearance
466
+ applyJump(updated.reduce((acc, [index, size]) => {
467
+ if (
468
+ // Keep distance from end during shifting
469
+ _scrollMode === SCROLL_BY_SHIFT ||
470
+ (_frozenRange
471
+ ? // https://github.com/inokawa/virtua/issues/380
472
+ index < _frozenRange[0]
473
+ : // Otherwise we should maintain visible position
474
+ getItemOffset(index) +
475
+ // https://github.com/inokawa/virtua/issues/385
476
+ (_scrollDirection === SCROLL_IDLE &&
477
+ _scrollMode === SCROLL_BY_NATIVE
478
+ ? getItemSize$1(index)
479
+ : 0) <
480
+ getRelativeScrollOffset())) {
481
+ acc += size - getItemSize$1(index);
482
+ }
483
+ return acc;
484
+ }, 0));
485
+ // Update item sizes
486
+ for (const [index, size] of updated) {
487
+ const prevSize = getItemSize$1(index);
488
+ const isInitialMeasurement = setItemSize(cache, index, size);
489
+ if (shouldAutoEstimateItemSize) {
490
+ _totalMeasuredSize += isInitialMeasurement
491
+ ? size
492
+ : size - prevSize;
493
+ }
494
+ }
495
+ // Estimate initial item size from measured sizes
496
+ if (shouldAutoEstimateItemSize &&
497
+ viewportSize &&
498
+ // If the total size is lower than the viewport, the item may be a empty state
499
+ _totalMeasuredSize > viewportSize) {
500
+ applyJump(estimateDefaultItemSize(cache, findIndex(cache, getVisibleOffset())));
501
+ shouldAutoEstimateItemSize = false;
502
+ }
503
+ mutated = UPDATE_VIRTUAL_STATE + UPDATE_SIZE_EVENT;
504
+ // Synchronous update is necessary in current design to minimize visible glitch in concurrent rendering.
505
+ // However this seems to be the main cause of the errors from ResizeObserver.
506
+ // https://github.com/inokawa/virtua/issues/470
507
+ //
508
+ // And in React, synchronous update with flushSync after asynchronous update will overtake the asynchronous one.
509
+ // If items resize happens just after scroll, race condition can occur depending on implementation.
510
+ shouldSync = true;
511
+ break;
512
+ }
513
+ case ACTION_VIEWPORT_RESIZE: {
514
+ if (viewportSize !== payload) {
515
+ viewportSize = payload;
516
+ mutated = UPDATE_VIRTUAL_STATE + UPDATE_SIZE_EVENT;
517
+ }
518
+ break;
519
+ }
520
+ case ACTION_ITEMS_LENGTH_CHANGE: {
521
+ if (payload[1]) {
522
+ applyJump(updateCacheLength(cache, payload[0], true));
523
+ _scrollMode = SCROLL_BY_SHIFT;
524
+ mutated = UPDATE_VIRTUAL_STATE;
525
+ }
526
+ else {
527
+ updateCacheLength(cache, payload[0]);
528
+ }
529
+ break;
530
+ }
531
+ case ACTION_START_OFFSET_CHANGE: {
532
+ startSpacerSize = payload;
533
+ break;
534
+ }
535
+ case ACTION_MANUAL_SCROLL: {
536
+ _scrollMode = SCROLL_BY_MANUAL_SCROLL;
537
+ break;
538
+ }
539
+ case ACTION_BEFORE_MANUAL_SMOOTH_SCROLL: {
540
+ _frozenRange = getRange(payload);
541
+ mutated = UPDATE_VIRTUAL_STATE;
542
+ break;
543
+ }
544
+ }
545
+ if (mutated) {
546
+ stateVersion = [];
547
+ if (shouldFlushPendingJump && pendingJump) {
548
+ jump += pendingJump;
549
+ pendingJump = 0;
550
+ jumpCount++;
551
+ }
552
+ subscribers.forEach(([target, cb]) => {
553
+ // Early return to skip React's computation
554
+ if (!(mutated & target)) {
555
+ return;
556
+ }
557
+ // https://github.com/facebook/react/issues/25191
558
+ // https://github.com/facebook/react/blob/a5fc797db14c6e05d4d5c4dbb22a0dd70d41f5d5/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1443-L1447
559
+ cb(shouldSync);
560
+ });
561
+ }
562
+ },
563
+ };
564
+ };
565
+
566
+ const timeout = setTimeout;
567
+ const debounce = (fn, ms) => {
568
+ let id;
569
+ const cancel = () => {
570
+ if (id != NULL) {
571
+ clearTimeout(id);
572
+ }
573
+ };
574
+ const debouncedFn = () => {
575
+ cancel();
576
+ id = timeout(() => {
577
+ id = NULL;
578
+ fn();
579
+ }, ms);
580
+ };
581
+ debouncedFn._cancel = cancel;
582
+ return debouncedFn;
583
+ };
584
+ /**
585
+ * scrollLeft is negative value in rtl direction.
586
+ *
587
+ * left right
588
+ * 0 100 spec compliant (ltr)
589
+ * -100 0 spec compliant (rtl)
590
+ * https://github.com/othree/jquery.rtl-scroll-type
591
+ */
592
+ const normalizeOffset = (offset, isHorizontal) => {
593
+ if (isHorizontal && isRTLDocument()) {
594
+ return -offset;
595
+ }
596
+ else {
597
+ return offset;
598
+ }
599
+ };
600
+ const createScrollObserver = (store, viewport, isHorizontal, getScrollOffset, updateScrollOffset, getStartOffset) => {
601
+ const now = Date.now;
602
+ let lastScrollTime = 0;
603
+ let wheeling = false;
604
+ let touching = false;
605
+ let justTouchEnded = false;
606
+ let stillMomentumScrolling = false;
607
+ const onScrollEnd = debounce(() => {
608
+ if (wheeling || touching) {
609
+ wheeling = false;
610
+ // Wait while wheeling or touching
611
+ onScrollEnd();
612
+ return;
613
+ }
614
+ justTouchEnded = false;
615
+ store.$update(ACTION_SCROLL_END);
616
+ }, 150);
617
+ const onScroll = () => {
618
+ lastScrollTime = now();
619
+ if (justTouchEnded) {
620
+ stillMomentumScrolling = true;
621
+ }
622
+ if (getStartOffset) {
623
+ store.$update(ACTION_START_OFFSET_CHANGE, getStartOffset());
624
+ }
625
+ store.$update(ACTION_SCROLL, getScrollOffset());
626
+ onScrollEnd();
627
+ };
628
+ // Infer scroll state also from wheel events
629
+ // Sometimes scroll events do not fire when frame dropped even if the visual have been already scrolled
630
+ const onWheel = ((e) => {
631
+ if (wheeling ||
632
+ // Scroll start should be detected with scroll event
633
+ !store.$isScrolling() ||
634
+ // Probably a pinch-to-zoom gesture
635
+ e.ctrlKey) {
636
+ return;
637
+ }
638
+ const timeDelta = now() - lastScrollTime;
639
+ if (
640
+ // Check if wheel event occurs some time after scrolling
641
+ 150 > timeDelta &&
642
+ 50 < timeDelta &&
643
+ // Get delta before checking deltaMode for firefox behavior
644
+ // https://github.com/w3c/uievents/issues/181#issuecomment-392648065
645
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1392460#c34
646
+ (isHorizontal ? e.deltaX : e.deltaY)) {
647
+ wheeling = true;
648
+ }
649
+ }); // FIXME type error. why only here?
650
+ const onTouchStart = () => {
651
+ touching = true;
652
+ justTouchEnded = stillMomentumScrolling = false;
653
+ };
654
+ const onTouchEnd = () => {
655
+ touching = false;
656
+ if (isIOSWebKit()) {
657
+ justTouchEnded = true;
658
+ }
659
+ };
660
+ viewport.addEventListener("scroll", onScroll);
661
+ viewport.addEventListener("wheel", onWheel, { passive: true });
662
+ viewport.addEventListener("touchstart", onTouchStart, { passive: true });
663
+ viewport.addEventListener("touchend", onTouchEnd, { passive: true });
664
+ return {
665
+ _dispose: () => {
666
+ viewport.removeEventListener("scroll", onScroll);
667
+ viewport.removeEventListener("wheel", onWheel);
668
+ viewport.removeEventListener("touchstart", onTouchStart);
669
+ viewport.removeEventListener("touchend", onTouchEnd);
670
+ onScrollEnd._cancel();
671
+ },
672
+ _fixScrollJump: () => {
673
+ const [jump, shift] = store._flushJump();
674
+ if (!jump)
675
+ return;
676
+ updateScrollOffset(normalizeOffset(jump, isHorizontal), shift, stillMomentumScrolling);
677
+ stillMomentumScrolling = false;
678
+ if (shift && store.$getViewportSize() > store.$getTotalSize()) {
679
+ // In this case applying jump may not cause scroll.
680
+ // Current logic expects scroll event occurs after applying jump so we dispatch it manually.
681
+ store.$update(ACTION_SCROLL, getScrollOffset());
682
+ }
683
+ },
684
+ };
685
+ };
686
+ /**
687
+ * @internal
688
+ */
689
+ const createScroller = (store, isHorizontal) => {
690
+ let viewportElement;
691
+ let scrollObserver;
692
+ let cancelScroll;
693
+ const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
694
+ const overflowKey = isHorizontal ? "overflowX" : "overflowY";
695
+ // The given offset will be clamped by browser
696
+ // https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
697
+ const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
698
+ if (!viewportElement) {
699
+ // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
700
+ microtask(() => scheduleImperativeScroll(getTargetOffset, smooth));
701
+ return;
702
+ }
703
+ if (cancelScroll) {
704
+ // Cancel waiting scrollTo
705
+ cancelScroll();
706
+ }
707
+ const waitForMeasurement = () => {
708
+ // Wait for the scroll destination items to be measured.
709
+ // The measurement will be done asynchronously and the timing is not predictable so we use promise.
710
+ let queue;
711
+ return [
712
+ new Promise((resolve, reject) => {
713
+ queue = resolve;
714
+ cancelScroll = reject;
715
+ // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
716
+ // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
717
+ // https://github.com/inokawa/virtua/issues/450
718
+ if (isInitialMeasurementDone(store)) {
719
+ // Reject when items around scroll destination completely measured
720
+ timeout(reject, 150);
721
+ }
722
+ }),
723
+ store.$subscribe(UPDATE_SIZE_EVENT, () => {
724
+ queue && queue();
725
+ }),
726
+ ];
727
+ };
728
+ if (smooth && isSmoothScrollSupported()) {
729
+ while (true) {
730
+ store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
731
+ if (!store._hasUnmeasuredItemsInFrozenRange()) {
732
+ break;
733
+ }
734
+ const [promise, unsubscribe] = waitForMeasurement();
735
+ try {
736
+ await promise;
737
+ }
738
+ catch (e) {
739
+ // canceled
740
+ return;
741
+ }
742
+ finally {
743
+ unsubscribe();
744
+ }
745
+ }
746
+ viewportElement.scrollTo({
747
+ [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
748
+ behavior: "smooth",
749
+ });
750
+ }
751
+ else {
752
+ while (true) {
753
+ const [promise, unsubscribe] = waitForMeasurement();
754
+ try {
755
+ viewportElement[scrollOffsetKey] = normalizeOffset(getTargetOffset(), isHorizontal);
756
+ store.$update(ACTION_MANUAL_SCROLL);
757
+ await promise;
758
+ }
759
+ catch (e) {
760
+ // canceled or finished
761
+ return;
762
+ }
763
+ finally {
764
+ unsubscribe();
765
+ }
766
+ }
767
+ }
768
+ };
769
+ return {
770
+ $observe(viewport) {
771
+ viewportElement = viewport;
772
+ scrollObserver = createScrollObserver(store, viewport, isHorizontal, () => normalizeOffset(viewport[scrollOffsetKey], isHorizontal), (jump, shift, isMomentumScrolling) => {
773
+ // If we update scroll position while touching on iOS, the position will be reverted.
774
+ // However iOS WebKit fires touch events only once at the beginning of momentum scrolling.
775
+ // That means we have no reliable way to confirm still touched or not if user touches more than once during momentum scrolling...
776
+ // This is a hack for the suspectable situations, inspired by https://github.com/prud/ios-overflow-scroll-to-top
777
+ if (isMomentumScrolling) {
778
+ const style = viewport.style;
779
+ const prev = style[overflowKey];
780
+ style[overflowKey] = "hidden";
781
+ timeout(() => {
782
+ style[overflowKey] = prev;
783
+ });
784
+ }
785
+ if (shift) {
786
+ viewport[scrollOffsetKey] = store.$getScrollOffset() + jump;
787
+ // https://github.com/inokawa/virtua/issues/357
788
+ cancelScroll && cancelScroll();
789
+ }
790
+ else {
791
+ viewport[scrollOffsetKey] += jump;
792
+ }
793
+ });
794
+ },
795
+ $dispose() {
796
+ scrollObserver && scrollObserver._dispose();
797
+ },
798
+ $scrollTo(offset) {
799
+ scheduleImperativeScroll(() => offset);
800
+ },
801
+ $scrollBy(offset) {
802
+ offset += store.$getScrollOffset();
803
+ scheduleImperativeScroll(() => offset);
804
+ },
805
+ $scrollToIndex(index, { align, smooth, offset = 0 } = {}) {
806
+ index = clamp(index, 0, store.$getItemsLength() - 1);
807
+ if (align === "nearest") {
808
+ const itemOffset = store.$getItemOffset(index);
809
+ const scrollOffset = store.$getScrollOffset();
810
+ if (itemOffset < scrollOffset) {
811
+ align = "start";
812
+ }
813
+ else if (itemOffset + store.$getItemSize(index) >
814
+ scrollOffset + store.$getViewportSize()) {
815
+ align = "end";
816
+ }
817
+ else {
818
+ // already completely visible
819
+ return;
820
+ }
821
+ }
822
+ scheduleImperativeScroll(() => {
823
+ return (offset +
824
+ store.$getStartSpacerSize() +
825
+ store.$getItemOffset(index) +
826
+ (align === "end"
827
+ ? store.$getItemSize(index) - store.$getViewportSize()
828
+ : align === "center"
829
+ ? (store.$getItemSize(index) - store.$getViewportSize()) / 2
830
+ : 0));
831
+ }, smooth);
832
+ },
833
+ $fixScrollJump: () => {
834
+ scrollObserver && scrollObserver._fixScrollJump();
835
+ },
836
+ };
837
+ };
838
+ /**
839
+ * @internal
840
+ */
841
+ const createWindowScroller = (store, isHorizontal) => {
842
+ let containerElement;
843
+ let scrollObserver;
844
+ let cancelScroll;
845
+ const calcOffsetToViewport = (node, viewport, window, isHorizontal, offset = 0) => {
846
+ // TODO calc offset only when it changes (maybe impossible)
847
+ const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
848
+ const offsetSum = offset +
849
+ (isHorizontal && isRTLDocument()
850
+ ? window.innerWidth - node[offsetKey] - node.offsetWidth
851
+ : node[offsetKey]);
852
+ const parent = node.offsetParent;
853
+ if (node === viewport || !parent) {
854
+ return offsetSum;
855
+ }
856
+ return calcOffsetToViewport(parent, viewport, window, isHorizontal, offsetSum);
857
+ };
858
+ const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
859
+ if (!containerElement) {
860
+ // Wait for element assign
861
+ microtask(() => scheduleImperativeScroll(getTargetOffset, smooth));
862
+ return;
863
+ }
864
+ if (cancelScroll) {
865
+ cancelScroll();
866
+ }
867
+ const waitForMeasurement = () => {
868
+ let queue;
869
+ return [
870
+ new Promise((resolve, reject) => {
871
+ queue = resolve;
872
+ cancelScroll = reject;
873
+ if (isInitialMeasurementDone(store)) {
874
+ timeout(reject, 150);
875
+ }
876
+ }),
877
+ store.$subscribe(UPDATE_SIZE_EVENT, () => {
878
+ queue && queue();
879
+ }),
880
+ ];
881
+ };
882
+ const window = getCurrentWindow(getCurrentDocument(containerElement));
883
+ if (smooth && isSmoothScrollSupported()) {
884
+ while (true) {
885
+ store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
886
+ if (!store._hasUnmeasuredItemsInFrozenRange()) {
887
+ break;
888
+ }
889
+ const [promise, unsubscribe] = waitForMeasurement();
890
+ try {
891
+ await promise;
892
+ }
893
+ catch (e) {
894
+ return;
895
+ }
896
+ finally {
897
+ unsubscribe();
898
+ }
899
+ }
900
+ window.scroll({
901
+ [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
902
+ behavior: "smooth",
903
+ });
904
+ }
905
+ else {
906
+ while (true) {
907
+ const [promise, unsubscribe] = waitForMeasurement();
908
+ try {
909
+ window.scroll({
910
+ [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
911
+ });
912
+ store.$update(ACTION_MANUAL_SCROLL);
913
+ await promise;
914
+ }
915
+ catch (e) {
916
+ return;
917
+ }
918
+ finally {
919
+ unsubscribe();
920
+ }
921
+ }
922
+ }
923
+ };
924
+ return {
925
+ $observe(container) {
926
+ containerElement = container;
927
+ const scrollOffsetKey = isHorizontal ? "scrollX" : "scrollY";
928
+ const document = getCurrentDocument(container);
929
+ const window = getCurrentWindow(document);
930
+ const documentBody = document.body;
931
+ scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeOffset(window[scrollOffsetKey], isHorizontal), (jump, shift) => {
932
+ // TODO support case two window scrollers exist in the same view
933
+ if (shift) {
934
+ window.scroll({
935
+ [isHorizontal ? "left" : "top"]: store.$getScrollOffset() + jump,
936
+ });
937
+ }
938
+ else {
939
+ window.scrollBy(isHorizontal ? jump : 0, isHorizontal ? 0 : jump);
940
+ }
941
+ }, () => calcOffsetToViewport(container, documentBody, window, isHorizontal));
942
+ },
943
+ $dispose() {
944
+ scrollObserver && scrollObserver._dispose();
945
+ containerElement = undefined;
946
+ },
947
+ $fixScrollJump: () => {
948
+ scrollObserver && scrollObserver._fixScrollJump();
949
+ },
950
+ $scrollToIndex(index, { align, smooth, offset = 0 } = {}) {
951
+ if (!containerElement)
952
+ return;
953
+ index = clamp(index, 0, store.$getItemsLength() - 1);
954
+ if (align === "nearest") {
955
+ const itemOffset = store.$getItemOffset(index);
956
+ const scrollOffset = store.$getScrollOffset();
957
+ if (itemOffset < scrollOffset) {
958
+ align = "start";
959
+ }
960
+ else if (itemOffset + store.$getItemSize(index) >
961
+ scrollOffset + store.$getViewportSize()) {
962
+ align = "end";
963
+ }
964
+ else {
965
+ return;
966
+ }
967
+ }
968
+ const document = getCurrentDocument(containerElement);
969
+ const window = getCurrentWindow(document);
970
+ const html = document.documentElement;
971
+ const getScrollbarSize = () => store.$getViewportSize() -
972
+ (isHorizontal ? html.clientWidth : html.clientHeight);
973
+ scheduleImperativeScroll(() => {
974
+ return (offset +
975
+ // Calculate target scroll position including container's offset from document
976
+ calcOffsetToViewport(containerElement, document.body, window, isHorizontal) +
977
+ // store._getStartSpacerSize() +
978
+ store.$getItemOffset(index) +
979
+ (align === "end"
980
+ ? store.$getItemSize(index) -
981
+ (store.$getViewportSize() - getScrollbarSize())
982
+ : align === "center"
983
+ ? (store.$getItemSize(index) -
984
+ (store.$getViewportSize() - getScrollbarSize())) /
985
+ 2
986
+ : 0));
987
+ }, smooth);
988
+ },
989
+ };
990
+ };
991
+ /**
992
+ * @internal
993
+ */
994
+ const createGridScroller = (vStore, hStore) => {
995
+ const vScroller = createScroller(vStore, false);
996
+ const hScroller = createScroller(hStore, true);
997
+ return {
998
+ $observe(viewportElement) {
999
+ vScroller.$observe(viewportElement);
1000
+ hScroller.$observe(viewportElement);
1001
+ },
1002
+ $dispose() {
1003
+ vScroller.$dispose();
1004
+ hScroller.$dispose();
1005
+ },
1006
+ $scrollTo(offsetX, offsetY) {
1007
+ vScroller.$scrollTo(offsetY);
1008
+ hScroller.$scrollTo(offsetX);
1009
+ },
1010
+ $scrollBy(offsetX, offsetY) {
1011
+ vScroller.$scrollBy(offsetY);
1012
+ hScroller.$scrollBy(offsetX);
1013
+ },
1014
+ $scrollToIndex(indexX, indexY) {
1015
+ vScroller.$scrollToIndex(indexY);
1016
+ hScroller.$scrollToIndex(indexX);
1017
+ },
1018
+ $fixScrollJump() {
1019
+ vScroller.$fixScrollJump();
1020
+ hScroller.$fixScrollJump();
1021
+ },
1022
+ };
1023
+ };
1024
+
1025
+ const createResizeObserver = (cb) => {
1026
+ let ro;
1027
+ return {
1028
+ _observe(e) {
1029
+ // Initialize ResizeObserver lazily for SSR
1030
+ // https://www.w3.org/TR/resize-observer/#intro
1031
+ (ro ||
1032
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1491739
1033
+ (ro = new (getCurrentWindow(getCurrentDocument(e)).ResizeObserver)(cb))).observe(e);
1034
+ },
1035
+ _unobserve(e) {
1036
+ ro.unobserve(e);
1037
+ },
1038
+ _dispose() {
1039
+ ro && ro.disconnect();
1040
+ },
1041
+ };
1042
+ };
1043
+ /**
1044
+ * @internal
1045
+ */
1046
+ const createResizer = (store, isHorizontal) => {
1047
+ let viewportElement;
1048
+ const sizeKey = isHorizontal ? "width" : "height";
1049
+ const mountedIndexes = new WeakMap();
1050
+ const resizeObserver = createResizeObserver((entries) => {
1051
+ const resizes = [];
1052
+ for (const { target, contentRect } of entries) {
1053
+ // Skip zero-sized rects that may be observed under `display: none` style
1054
+ if (!target.offsetParent)
1055
+ continue;
1056
+ if (target === viewportElement) {
1057
+ store.$update(ACTION_VIEWPORT_RESIZE, contentRect[sizeKey]);
1058
+ }
1059
+ else {
1060
+ const index = mountedIndexes.get(target);
1061
+ if (index != NULL) {
1062
+ resizes.push([index, contentRect[sizeKey]]);
1063
+ }
1064
+ }
1065
+ }
1066
+ if (resizes.length) {
1067
+ store.$update(ACTION_ITEM_RESIZE, resizes);
1068
+ }
1069
+ });
1070
+ return {
1071
+ $observeRoot(viewport) {
1072
+ resizeObserver._observe((viewportElement = viewport));
1073
+ },
1074
+ $observeItem: (el, i) => {
1075
+ mountedIndexes.set(el, i);
1076
+ resizeObserver._observe(el);
1077
+ return () => {
1078
+ mountedIndexes.delete(el);
1079
+ resizeObserver._unobserve(el);
1080
+ };
1081
+ },
1082
+ $dispose: resizeObserver._dispose,
1083
+ };
1084
+ };
1085
+ /**
1086
+ * @internal
1087
+ */
1088
+ const createWindowResizer = (store, isHorizontal) => {
1089
+ const sizeKey = isHorizontal ? "width" : "height";
1090
+ const windowSizeKey = isHorizontal ? "innerWidth" : "innerHeight";
1091
+ const mountedIndexes = new WeakMap();
1092
+ const resizeObserver = createResizeObserver((entries) => {
1093
+ const resizes = [];
1094
+ for (const { target, contentRect } of entries) {
1095
+ // Skip zero-sized rects that may be observed under `display: none` style
1096
+ if (!target.offsetParent)
1097
+ continue;
1098
+ const index = mountedIndexes.get(target);
1099
+ if (index != NULL) {
1100
+ resizes.push([index, contentRect[sizeKey]]);
1101
+ }
1102
+ }
1103
+ if (resizes.length) {
1104
+ store.$update(ACTION_ITEM_RESIZE, resizes);
1105
+ }
1106
+ });
1107
+ let cleanupOnWindowResize;
1108
+ return {
1109
+ $observeRoot(container) {
1110
+ const window = getCurrentWindow(getCurrentDocument(container));
1111
+ const onWindowResize = () => {
1112
+ store.$update(ACTION_VIEWPORT_RESIZE, window[windowSizeKey]);
1113
+ };
1114
+ window.addEventListener("resize", onWindowResize);
1115
+ onWindowResize();
1116
+ cleanupOnWindowResize = () => {
1117
+ window.removeEventListener("resize", onWindowResize);
1118
+ };
1119
+ },
1120
+ $observeItem: (el, i) => {
1121
+ mountedIndexes.set(el, i);
1122
+ resizeObserver._observe(el);
1123
+ return () => {
1124
+ mountedIndexes.delete(el);
1125
+ resizeObserver._unobserve(el);
1126
+ };
1127
+ },
1128
+ $dispose() {
1129
+ cleanupOnWindowResize && cleanupOnWindowResize();
1130
+ resizeObserver._dispose();
1131
+ },
1132
+ };
1133
+ };
1134
+ /**
1135
+ * @internal
1136
+ */
1137
+ const createGridResizer = (vStore, hStore) => {
1138
+ let viewportElement;
1139
+ const heightKey = "height";
1140
+ const widthKey = "width";
1141
+ const mountedIndexes = new WeakMap();
1142
+ const maybeCachedRowIndexes = new Set();
1143
+ const maybeCachedColIndexes = new Set();
1144
+ const sizeCache = new Map();
1145
+ const getKey = (rowIndex, colIndex) => `${rowIndex}-${colIndex}`;
1146
+ const resizeObserver = createResizeObserver((entries) => {
1147
+ const resizedRows = new Set();
1148
+ const resizedCols = new Set();
1149
+ for (const { target, contentRect } of entries) {
1150
+ // Skip zero-sized rects that may be observed under `display: none` style
1151
+ if (!target.offsetParent)
1152
+ continue;
1153
+ if (target === viewportElement) {
1154
+ vStore.$update(ACTION_VIEWPORT_RESIZE, contentRect[heightKey]);
1155
+ hStore.$update(ACTION_VIEWPORT_RESIZE, contentRect[widthKey]);
1156
+ }
1157
+ else {
1158
+ const cell = mountedIndexes.get(target);
1159
+ if (cell) {
1160
+ const [rowIndex, colIndex] = cell;
1161
+ const key = getKey(rowIndex, colIndex);
1162
+ const prevSize = sizeCache.get(key);
1163
+ const size = [
1164
+ contentRect[heightKey],
1165
+ contentRect[widthKey],
1166
+ ];
1167
+ let rowResized;
1168
+ let colResized;
1169
+ if (!prevSize) {
1170
+ rowResized = colResized = true;
1171
+ }
1172
+ else {
1173
+ if (prevSize[0] !== size[0]) {
1174
+ rowResized = true;
1175
+ }
1176
+ if (prevSize[1] !== size[1]) {
1177
+ colResized = true;
1178
+ }
1179
+ }
1180
+ if (rowResized) {
1181
+ resizedRows.add(rowIndex);
1182
+ }
1183
+ if (colResized) {
1184
+ resizedCols.add(colIndex);
1185
+ }
1186
+ if (rowResized || colResized) {
1187
+ sizeCache.set(key, size);
1188
+ }
1189
+ }
1190
+ }
1191
+ }
1192
+ if (resizedRows.size) {
1193
+ const heightResizes = [];
1194
+ resizedRows.forEach((rowIndex) => {
1195
+ let maxHeight = 0;
1196
+ maybeCachedColIndexes.forEach((colIndex) => {
1197
+ const size = sizeCache.get(getKey(rowIndex, colIndex));
1198
+ if (size) {
1199
+ maxHeight = max(maxHeight, size[0]);
1200
+ }
1201
+ });
1202
+ if (maxHeight) {
1203
+ heightResizes.push([rowIndex, maxHeight]);
1204
+ }
1205
+ });
1206
+ vStore.$update(ACTION_ITEM_RESIZE, heightResizes);
1207
+ }
1208
+ if (resizedCols.size) {
1209
+ const widthResizes = [];
1210
+ resizedCols.forEach((colIndex) => {
1211
+ let maxWidth = 0;
1212
+ maybeCachedRowIndexes.forEach((rowIndex) => {
1213
+ const size = sizeCache.get(getKey(rowIndex, colIndex));
1214
+ if (size) {
1215
+ maxWidth = max(maxWidth, size[1]);
1216
+ }
1217
+ });
1218
+ if (maxWidth) {
1219
+ widthResizes.push([colIndex, maxWidth]);
1220
+ }
1221
+ });
1222
+ hStore.$update(ACTION_ITEM_RESIZE, widthResizes);
1223
+ }
1224
+ });
1225
+ return {
1226
+ $observeRoot(viewport) {
1227
+ resizeObserver._observe((viewportElement = viewport));
1228
+ },
1229
+ $observeItem(el, rowIndex, colIndex) {
1230
+ mountedIndexes.set(el, [rowIndex, colIndex]);
1231
+ maybeCachedRowIndexes.add(rowIndex);
1232
+ maybeCachedColIndexes.add(colIndex);
1233
+ resizeObserver._observe(el);
1234
+ return () => {
1235
+ mountedIndexes.delete(el);
1236
+ resizeObserver._unobserve(el);
1237
+ };
1238
+ },
1239
+ $dispose: resizeObserver._dispose,
1240
+ };
1241
+ };
1242
+
1243
+ /**
1244
+ * @jsxImportSource solid-js
1245
+ */
1246
+ /**
1247
+ * @internal
1248
+ */
1249
+ const ListItem = (props) => {
1250
+ let elementRef;
1251
+ props = mergeProps({ _as: "div" }, props);
1252
+ // The index may be changed if elements are inserted to or removed from the start of props.children
1253
+ createEffect(() => {
1254
+ if (!elementRef)
1255
+ return;
1256
+ onCleanup(props._resizer(elementRef, props._index));
1257
+ });
1258
+ const style = createMemo(() => {
1259
+ const isHorizontal = props._isHorizontal;
1260
+ const style = {
1261
+ position: "absolute",
1262
+ [isHorizontal ? "height" : "width"]: "100%",
1263
+ [isHorizontal ? "top" : "left"]: "0px",
1264
+ [isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]: props._offset + "px",
1265
+ visibility: props._hide ? "hidden" : "visible",
1266
+ };
1267
+ if (isHorizontal) {
1268
+ style.display = "flex";
1269
+ }
1270
+ return style;
1271
+ });
1272
+ return (<Dynamic component={props._as} ref={elementRef} style={style()}>
1273
+ {props._children}
1274
+ </Dynamic>);
1275
+ };
1276
+
1277
+ /**
1278
+ * @jsxImportSource solid-js
1279
+ */
1280
+ /**
1281
+ * https://github.com/solidjs/solid/blob/main/packages/solid/src/reactive/array.ts
1282
+ * https://github.com/solidjs/solid/blob/main/packages/solid/src/render/flow.ts
1283
+ * https://github.com/solidjs/solid/discussions/366
1284
+ * @internal
1285
+ */
1286
+ const RangedFor = (props) => {
1287
+ let prev = new Map();
1288
+ onCleanup(() => {
1289
+ for (const node of prev.values()) {
1290
+ node._dispose();
1291
+ }
1292
+ });
1293
+ return createMemo(() => {
1294
+ const list = props._each;
1295
+ const [start, end] = props._range;
1296
+ const current = new Map();
1297
+ const items = [];
1298
+ for (let i = start; i <= end; i++) {
1299
+ const newData = list[i];
1300
+ const lookup = prev.get(i);
1301
+ items.push(lookup
1302
+ ? lookup._element
1303
+ : createRoot((dispose) => {
1304
+ const data = createSignal(newData);
1305
+ const result = props._render(data[0], i);
1306
+ current.set(i, {
1307
+ _data: data,
1308
+ _element: result,
1309
+ _dispose: dispose,
1310
+ });
1311
+ return result;
1312
+ }));
1313
+ if (lookup) {
1314
+ if (newData !== lookup._data) {
1315
+ lookup._data[1](newData // TODO improve type
1316
+ );
1317
+ }
1318
+ current.set(i, lookup);
1319
+ }
1320
+ }
1321
+ for (const [key, node] of prev.entries()) {
1322
+ if (!current.has(key)) {
1323
+ node._dispose();
1324
+ }
1325
+ }
1326
+ prev = current;
1327
+ return items;
1328
+ }); // TODO improve type
1329
+ };
1330
+
1331
+ /**
1332
+ * @internal
1333
+ */
1334
+ const isSameRange = (prev, next) => {
1335
+ return prev[0] === next[0] && prev[1] === next[1];
1336
+ };
1337
+
1338
+ /**
1339
+ * @jsxImportSource solid-js
1340
+ */
1341
+ /**
1342
+ * Customizable list virtualizer for advanced usage. See {@link VirtualizerProps} and {@link VirtualizerHandle}.
1343
+ */
1344
+ const Virtualizer = (props) => {
1345
+ let containerRef;
1346
+ const { itemSize, horizontal = false, overscan } = props;
1347
+ props = mergeProps({ as: "div" }, props);
1348
+ const store = createVirtualStore(props.data.length, itemSize, overscan, undefined, undefined, !itemSize);
1349
+ const resizer = createResizer(store, horizontal);
1350
+ const scroller = createScroller(store, horizontal);
1351
+ const [rerender, setRerender] = createSignal(store.$getStateVersion());
1352
+ const unsubscribeStore = store.$subscribe(UPDATE_VIRTUAL_STATE, () => {
1353
+ setRerender(store.$getStateVersion());
1354
+ });
1355
+ const unsubscribeOnScroll = store.$subscribe(UPDATE_SCROLL_EVENT, () => {
1356
+ var _a;
1357
+ (_a = props.onScroll) === null || _a === void 0 ? void 0 : _a.call(props, store.$getScrollOffset());
1358
+ });
1359
+ const unsubscribeOnScrollEnd = store.$subscribe(UPDATE_SCROLL_END_EVENT, () => {
1360
+ var _a;
1361
+ (_a = props.onScrollEnd) === null || _a === void 0 ? void 0 : _a.call(props);
1362
+ });
1363
+ const range = createMemo((prev) => {
1364
+ rerender();
1365
+ const next = store.$getRange();
1366
+ if (prev && isSameRange(prev, next)) {
1367
+ return prev;
1368
+ }
1369
+ return next;
1370
+ });
1371
+ const isScrolling = createMemo(() => rerender() && store.$isScrolling());
1372
+ const totalSize = createMemo(() => rerender() && store.$getTotalSize());
1373
+ const jumpCount = createMemo(() => rerender() && store.$getJumpCount());
1374
+ onMount(() => {
1375
+ if (props.ref) {
1376
+ props.ref({
1377
+ get scrollOffset() {
1378
+ return store.$getScrollOffset();
1379
+ },
1380
+ get scrollSize() {
1381
+ return getScrollSize(store);
1382
+ },
1383
+ get viewportSize() {
1384
+ return store.$getViewportSize();
1385
+ },
1386
+ findStartIndex: store.$findStartIndex,
1387
+ findEndIndex: store.$findEndIndex,
1388
+ getItemOffset: store.$getItemOffset,
1389
+ getItemSize: store.$getItemSize,
1390
+ scrollToIndex: scroller.$scrollToIndex,
1391
+ scrollTo: scroller.$scrollTo,
1392
+ scrollBy: scroller.$scrollBy,
1393
+ });
1394
+ }
1395
+ const scrollable = props.scrollRef || containerRef.parentElement;
1396
+ resizer.$observeRoot(scrollable);
1397
+ scroller.$observe(scrollable);
1398
+ onCleanup(() => {
1399
+ if (props.ref) {
1400
+ props.ref();
1401
+ }
1402
+ unsubscribeStore();
1403
+ unsubscribeOnScroll();
1404
+ unsubscribeOnScrollEnd();
1405
+ resizer.$dispose();
1406
+ scroller.$dispose();
1407
+ });
1408
+ });
1409
+ createComputed(on(() => props.data.length, (count) => {
1410
+ if (count !== store.$getItemsLength()) {
1411
+ store.$update(ACTION_ITEMS_LENGTH_CHANGE, [count, props.shift]);
1412
+ }
1413
+ }));
1414
+ createComputed(on(() => props.startMargin || 0, (value) => {
1415
+ if (value !== store.$getStartSpacerSize()) {
1416
+ store.$update(ACTION_START_OFFSET_CHANGE, value);
1417
+ }
1418
+ }));
1419
+ createEffect(on(jumpCount, () => {
1420
+ scroller.$fixScrollJump();
1421
+ }));
1422
+ return (<Dynamic component={props.as} ref={containerRef} style={{
1423
+ // contain: "content",
1424
+ "overflow-anchor": "none", // opt out browser's scroll anchoring because it will conflict to scroll anchoring of virtualizer
1425
+ flex: "none", // flex style can break layout
1426
+ position: "relative",
1427
+ visibility: "hidden", // TODO replace with other optimization methods
1428
+ width: horizontal ? totalSize() + "px" : "100%",
1429
+ height: horizontal ? "100%" : totalSize() + "px",
1430
+ "pointer-events": isScrolling() ? "none" : undefined,
1431
+ }}>
1432
+ <RangedFor _each={props.data} _range={range()} _render={(data, index) => {
1433
+ const offset = createMemo(() => {
1434
+ rerender();
1435
+ return store.$getItemOffset(index);
1436
+ });
1437
+ const hide = createMemo(() => {
1438
+ rerender();
1439
+ return store.$isUnmeasuredItem(index);
1440
+ });
1441
+ return (<ListItem _as={props.item} _index={index} _resizer={resizer.$observeItem} _offset={offset()} _hide={hide()} _children={props.children(data(), index)} _isHorizontal={horizontal}/>);
1442
+ }}/>
1443
+ </Dynamic>);
1444
+ };
1445
+
1446
+ /**
1447
+ * Virtualized list component. See {@link VListProps} and {@link VListHandle}.
1448
+ */
1449
+ const VList = (props) => {
1450
+ const { ref, data, children, overscan, itemSize, shift, horizontal, onScroll, onScrollEnd, style, ...attrs } = props;
1451
+ return (<div {...attrs} style={{
1452
+ display: horizontal ? "inline-block" : "block",
1453
+ [horizontal ? "overflow-x" : "overflow-y"]: "auto",
1454
+ contain: "strict",
1455
+ width: "100%",
1456
+ height: "100%",
1457
+ ...props.style,
1458
+ }}>
1459
+ <Virtualizer ref={props.ref} data={props.data} overscan={props.overscan} itemSize={props.itemSize} shift={props.shift} horizontal={horizontal} onScroll={props.onScroll} onScrollEnd={props.onScrollEnd}>
1460
+ {props.children}
1461
+ </Virtualizer>
1462
+ </div>);
1463
+ };
1464
+
1465
+ /**
1466
+ * @jsxImportSource solid-js
1467
+ */
1468
+ /**
1469
+ * {@link Virtualizer} controlled by the window scrolling. See {@link WindowVirtualizerProps} and {@link WindowVirtualizerHandle}.
1470
+ */
1471
+ const WindowVirtualizer = (props) => {
1472
+ let containerRef;
1473
+ const { ref: _ref, data: _data, children: _children, overscan, itemSize, shift: _shift, horizontal = false, onScrollEnd: _onScrollEnd, } = props;
1474
+ const store = createVirtualStore(props.data.length, itemSize, overscan, undefined, undefined, !itemSize);
1475
+ const resizer = createWindowResizer(store, horizontal);
1476
+ const scroller = createWindowScroller(store, horizontal);
1477
+ const [rerender, setRerender] = createSignal(store.$getStateVersion());
1478
+ const unsubscribeStore = store.$subscribe(UPDATE_VIRTUAL_STATE, () => {
1479
+ setRerender(store.$getStateVersion());
1480
+ });
1481
+ const unsubscribeOnScroll = store.$subscribe(UPDATE_SCROLL_EVENT, () => {
1482
+ var _a;
1483
+ (_a = props.onScroll) === null || _a === void 0 ? void 0 : _a.call(props, store.$getScrollOffset());
1484
+ });
1485
+ const unsubscribeOnScrollEnd = store.$subscribe(UPDATE_SCROLL_END_EVENT, () => {
1486
+ var _a;
1487
+ (_a = props.onScrollEnd) === null || _a === void 0 ? void 0 : _a.call(props);
1488
+ });
1489
+ const range = createMemo((prev) => {
1490
+ rerender();
1491
+ const next = store.$getRange();
1492
+ if (prev && isSameRange(prev, next)) {
1493
+ return prev;
1494
+ }
1495
+ return next;
1496
+ });
1497
+ const isScrolling = createMemo(() => rerender() && store.$isScrolling());
1498
+ const totalSize = createMemo(() => rerender() && store.$getTotalSize());
1499
+ const jumpCount = createMemo(() => rerender() && store.$getJumpCount());
1500
+ onMount(() => {
1501
+ if (props.ref) {
1502
+ props.ref({
1503
+ findStartIndex: store.$findStartIndex,
1504
+ findEndIndex: store.$findEndIndex,
1505
+ scrollToIndex: scroller.$scrollToIndex,
1506
+ });
1507
+ }
1508
+ resizer.$observeRoot(containerRef);
1509
+ scroller.$observe(containerRef);
1510
+ onCleanup(() => {
1511
+ if (props.ref) {
1512
+ props.ref();
1513
+ }
1514
+ unsubscribeStore();
1515
+ unsubscribeOnScroll();
1516
+ unsubscribeOnScrollEnd();
1517
+ resizer.$dispose();
1518
+ scroller.$dispose();
1519
+ });
1520
+ });
1521
+ createComputed(on(() => props.data.length, (len) => {
1522
+ if (len !== store.$getItemsLength()) {
1523
+ store.$update(ACTION_ITEMS_LENGTH_CHANGE, [len, props.shift]);
1524
+ }
1525
+ }));
1526
+ createEffect(on(jumpCount, () => {
1527
+ scroller.$fixScrollJump();
1528
+ }));
1529
+ return (<div ref={containerRef} style={{
1530
+ // contain: "content",
1531
+ "overflow-anchor": "none", // opt out browser's scroll anchoring because it will conflict to scroll anchoring of virtualizer
1532
+ flex: "none", // flex style can break layout
1533
+ position: "relative",
1534
+ visibility: "hidden", // TODO replace with other optimization methods
1535
+ width: horizontal ? totalSize() + "px" : "100%",
1536
+ height: horizontal ? "100%" : totalSize() + "px",
1537
+ "pointer-events": isScrolling() ? "none" : undefined,
1538
+ }}>
1539
+ <RangedFor _each={props.data} _range={range()} _render={(data, index) => {
1540
+ const offset = createMemo(() => {
1541
+ rerender();
1542
+ return store.$getItemOffset(index);
1543
+ });
1544
+ const hide = createMemo(() => {
1545
+ rerender();
1546
+ return store.$isUnmeasuredItem(index);
1547
+ });
1548
+ return (<ListItem _index={index} _resizer={resizer.$observeItem} _offset={offset()} _hide={hide()} _children={props.children(data(), index)} _isHorizontal={horizontal}/>);
1549
+ }}/>
1550
+ </div>);
1551
+ };
1552
+
1553
+ /**
1554
+ * @module solid
1555
+ */
1556
+
1557
+ export { VList, Virtualizer, WindowVirtualizer };
1558
+ //# sourceMappingURL=index.jsx.map