virtua 0.38.0 → 0.38.2

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