cbvirtua 1.0.0

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.
package/core/cache.js ADDED
@@ -0,0 +1,142 @@
1
+ import { clamp, median, min } from "./utils";
2
+ /** @internal */
3
+ export const UNCACHED = -1;
4
+ const fill = (array, length, prepend) => {
5
+ const key = prepend ? "unshift" : "push";
6
+ for (let i = 0; i < length; i++) {
7
+ array[key](UNCACHED);
8
+ }
9
+ return array;
10
+ };
11
+ /**
12
+ * @internal
13
+ */
14
+ export const getItemSize = (cache, index) => {
15
+ const size = cache._sizes[index];
16
+ return size === UNCACHED ? cache._defaultItemSize : size;
17
+ };
18
+ /**
19
+ * @internal
20
+ */
21
+ export const setItemSize = (cache, index, size) => {
22
+ const isInitialMeasurement = cache._sizes[index] === UNCACHED;
23
+ cache._sizes[index] = size;
24
+ // mark as dirty
25
+ cache._computedOffsetIndex = min(index, cache._computedOffsetIndex);
26
+ return isInitialMeasurement;
27
+ };
28
+ /**
29
+ * @internal
30
+ */
31
+ export const computeOffset = (cache, index) => {
32
+ if (!cache._length)
33
+ return 0;
34
+ if (cache._computedOffsetIndex >= index) {
35
+ return cache._offsets[index];
36
+ }
37
+ if (cache._computedOffsetIndex < 0) {
38
+ // first offset must be 0 to avoid returning NaN, which can cause infinite rerender.
39
+ // https://github.com/inokawa/virtua/pull/160
40
+ cache._offsets[0] = 0;
41
+ cache._computedOffsetIndex = 0;
42
+ }
43
+ let i = cache._computedOffsetIndex;
44
+ let top = cache._offsets[i];
45
+ while (i < index) {
46
+ top += getItemSize(cache, i);
47
+ cache._offsets[++i] = top;
48
+ }
49
+ // mark as measured
50
+ cache._computedOffsetIndex = index;
51
+ return top;
52
+ };
53
+ /**
54
+ * @internal
55
+ */
56
+ export const computeTotalSize = (cache) => {
57
+ if (!cache._length)
58
+ return 0;
59
+ return (computeOffset(cache, cache._length - 1) +
60
+ getItemSize(cache, cache._length - 1));
61
+ };
62
+ /**
63
+ * @internal
64
+ */
65
+ export const findIndex = (cache, offset, i) => {
66
+ let sum = computeOffset(cache, i);
67
+ while (i >= 0 && i < cache._length) {
68
+ if (sum <= offset) {
69
+ const next = getItemSize(cache, i);
70
+ if (sum + next > offset) {
71
+ break;
72
+ }
73
+ else {
74
+ sum += next;
75
+ i++;
76
+ }
77
+ }
78
+ else {
79
+ sum -= getItemSize(cache, --i);
80
+ }
81
+ }
82
+ return clamp(i, 0, cache._length - 1);
83
+ };
84
+ /**
85
+ * @internal
86
+ */
87
+ export const computeRange = (cache, scrollOffset, prevStartIndex, viewportSize) => {
88
+ const start = findIndex(cache, scrollOffset,
89
+ // Clamp because prevStartIndex may exceed the limit when children decreased a lot after scrolling
90
+ min(prevStartIndex, cache._length - 1));
91
+ return [start, findIndex(cache, scrollOffset + viewportSize, start)];
92
+ };
93
+ /**
94
+ * @internal
95
+ */
96
+ export const estimateDefaultItemSize = (cache) => {
97
+ const measuredSizes = cache._sizes.filter((s) => s !== UNCACHED);
98
+ // This function will be called after measurement so measured size array must be longer than 0
99
+ const startItemSize = measuredSizes[0];
100
+ cache._defaultItemSize = measuredSizes.every((s) => s === startItemSize)
101
+ ? // Maybe a fixed size array
102
+ startItemSize
103
+ : // Maybe a variable size array
104
+ median(measuredSizes);
105
+ };
106
+ /**
107
+ * @internal
108
+ */
109
+ export const initCache = (length, itemSize) => {
110
+ return {
111
+ _defaultItemSize: itemSize,
112
+ _length: length,
113
+ _computedOffsetIndex: -1,
114
+ _sizes: fill([], length),
115
+ _offsets: fill([], length),
116
+ };
117
+ };
118
+ /**
119
+ * @internal
120
+ */
121
+ export const updateCacheLength = (cache, length, isShift) => {
122
+ const diff = length - cache._length;
123
+ const isAdd = diff > 0;
124
+ let shift;
125
+ if (isAdd) {
126
+ // Added
127
+ shift = cache._defaultItemSize * diff;
128
+ fill(cache._sizes, diff, isShift);
129
+ fill(cache._offsets, diff);
130
+ }
131
+ else {
132
+ // Removed
133
+ shift = (isShift ? cache._sizes.splice(0, -diff) : cache._sizes.splice(diff)).reduce((acc, removed) => acc + (removed === UNCACHED ? cache._defaultItemSize : removed), 0);
134
+ cache._offsets.splice(diff);
135
+ }
136
+ cache._computedOffsetIndex = isShift
137
+ ? // Discard cache for now
138
+ -1
139
+ : min(length - 1, cache._computedOffsetIndex);
140
+ cache._length = length;
141
+ return [shift, isAdd];
142
+ };
@@ -0,0 +1,36 @@
1
+ import { once } from "./utils";
2
+ /**
3
+ * @internal
4
+ */
5
+ export const isBrowser = typeof window !== "undefined";
6
+ const getDocumentElement = () => document.documentElement;
7
+ /**
8
+ * @internal
9
+ */
10
+ export const getCurrentDocument = (node) => node.ownerDocument;
11
+ /**
12
+ * @internal
13
+ */
14
+ export const getCurrentWindow = (doc) => doc.defaultView;
15
+ /**
16
+ * @internal
17
+ */
18
+ export const isRTLDocument = /*#__PURE__*/ once(() => {
19
+ // TODO support SSR in rtl
20
+ return isBrowser
21
+ ? getComputedStyle(getDocumentElement()).direction === "rtl"
22
+ : false;
23
+ });
24
+ /**
25
+ * Currently, all browsers on iOS/iPadOS are WebKit, including WebView.
26
+ * @internal
27
+ */
28
+ export const isIOSWebKit = /*#__PURE__*/ once(() => {
29
+ return /iP(hone|od|ad)/.test(navigator.userAgent);
30
+ });
31
+ /**
32
+ * @internal
33
+ */
34
+ export const isSmoothScrollSupported = /*#__PURE__*/ once(() => {
35
+ return "scrollBehavior" in getDocumentElement().style;
36
+ });
@@ -0,0 +1,219 @@
1
+ import { getCurrentDocument, getCurrentWindow } from "./environment";
2
+ import { ACTION_ITEM_RESIZE, ACTION_VIEWPORT_RESIZE, } from "./store";
3
+ import { exists, max } from "./utils";
4
+ const createResizeObserver = (cb) => {
5
+ let ro;
6
+ return {
7
+ _observe(e) {
8
+ // Initialize ResizeObserver lazily for SSR
9
+ // https://www.w3.org/TR/resize-observer/#intro
10
+ (ro ||
11
+ (ro = new (getCurrentWindow(getCurrentDocument(e)).ResizeObserver)(cb))).observe(e);
12
+ },
13
+ _unobserve(e) {
14
+ ro.unobserve(e);
15
+ },
16
+ _dispose() {
17
+ ro && ro.disconnect();
18
+ },
19
+ };
20
+ };
21
+ /**
22
+ * @internal
23
+ */
24
+ export const createResizer = (store, isHorizontal) => {
25
+ let viewportElement;
26
+ const sizeKey = isHorizontal ? "width" : "height";
27
+ const mountedIndexes = new WeakMap();
28
+ const resizeObserver = createResizeObserver((entries) => {
29
+ const resizes = [];
30
+ for (const { target, contentRect } of entries) {
31
+ // Skip zero-sized rects that may be observed under `display: none` style
32
+ if (!target.offsetParent)
33
+ continue;
34
+ if (target === viewportElement) {
35
+ store._update(ACTION_VIEWPORT_RESIZE, contentRect[sizeKey]);
36
+ }
37
+ else {
38
+ const index = mountedIndexes.get(target);
39
+ if (exists(index)) {
40
+ resizes.push([index, contentRect[sizeKey]]);
41
+ }
42
+ }
43
+ }
44
+ if (resizes.length) {
45
+ store._update(ACTION_ITEM_RESIZE, resizes);
46
+ }
47
+ });
48
+ return {
49
+ _observeRoot(viewport) {
50
+ resizeObserver._observe((viewportElement = viewport));
51
+ },
52
+ _observeItem: (el, i) => {
53
+ mountedIndexes.set(el, i);
54
+ resizeObserver._observe(el);
55
+ return () => {
56
+ mountedIndexes.delete(el);
57
+ resizeObserver._unobserve(el);
58
+ };
59
+ },
60
+ _dispose: resizeObserver._dispose,
61
+ };
62
+ };
63
+ /**
64
+ * @internal
65
+ */
66
+ export const createWindowResizer = (store, isHorizontal) => {
67
+ const sizeKey = isHorizontal ? "width" : "height";
68
+ const windowSizeKey = isHorizontal ? "innerWidth" : "innerHeight";
69
+ const mountedIndexes = new WeakMap();
70
+ const resizeObserver = createResizeObserver((entries) => {
71
+ const resizes = [];
72
+ for (const { target, contentRect } of entries) {
73
+ // Skip zero-sized rects that may be observed under `display: none` style
74
+ if (!target.offsetParent)
75
+ continue;
76
+ const index = mountedIndexes.get(target);
77
+ if (exists(index)) {
78
+ resizes.push([index, contentRect[sizeKey]]);
79
+ }
80
+ }
81
+ if (resizes.length) {
82
+ store._update(ACTION_ITEM_RESIZE, resizes);
83
+ }
84
+ });
85
+ let cleanupOnWindowResize;
86
+ return {
87
+ _observeRoot(container) {
88
+ const window = getCurrentWindow(getCurrentDocument(container));
89
+ const onWindowResize = () => {
90
+ store._update(ACTION_VIEWPORT_RESIZE, window[windowSizeKey]);
91
+ };
92
+ window.addEventListener("resize", onWindowResize);
93
+ onWindowResize();
94
+ cleanupOnWindowResize = () => {
95
+ window.removeEventListener("resize", onWindowResize);
96
+ };
97
+ },
98
+ _observeItem: (el, i) => {
99
+ mountedIndexes.set(el, i);
100
+ resizeObserver._observe(el);
101
+ return () => {
102
+ mountedIndexes.delete(el);
103
+ resizeObserver._unobserve(el);
104
+ };
105
+ },
106
+ _dispose() {
107
+ cleanupOnWindowResize && cleanupOnWindowResize();
108
+ resizeObserver._dispose();
109
+ },
110
+ };
111
+ };
112
+ /**
113
+ * @internal
114
+ */
115
+ export const createGridResizer = (vStore, hStore) => {
116
+ let viewportElement;
117
+ const heightKey = "height";
118
+ const widthKey = "width";
119
+ const mountedIndexes = new WeakMap();
120
+ const maybeCachedRowIndexes = new Set();
121
+ const maybeCachedColIndexes = new Set();
122
+ const sizeCache = new Map();
123
+ const getKey = (rowIndex, colIndex) => `${rowIndex}-${colIndex}`;
124
+ const resizeObserver = createResizeObserver((entries) => {
125
+ const resizedRows = new Set();
126
+ const resizedCols = new Set();
127
+ for (const { target, contentRect } of entries) {
128
+ // Skip zero-sized rects that may be observed under `display: none` style
129
+ if (!target.offsetParent)
130
+ continue;
131
+ if (target === viewportElement) {
132
+ vStore._update(ACTION_VIEWPORT_RESIZE, contentRect[heightKey]);
133
+ hStore._update(ACTION_VIEWPORT_RESIZE, contentRect[widthKey]);
134
+ }
135
+ else {
136
+ const cell = mountedIndexes.get(target);
137
+ if (cell) {
138
+ const [rowIndex, colIndex] = cell;
139
+ const key = getKey(rowIndex, colIndex);
140
+ const prevSize = sizeCache.get(key);
141
+ const size = [
142
+ contentRect[heightKey],
143
+ contentRect[widthKey],
144
+ ];
145
+ let rowResized;
146
+ let colResized;
147
+ if (!prevSize) {
148
+ rowResized = colResized = true;
149
+ }
150
+ else {
151
+ if (prevSize[0] !== size[0]) {
152
+ rowResized = true;
153
+ }
154
+ if (prevSize[1] !== size[1]) {
155
+ colResized = true;
156
+ }
157
+ }
158
+ if (rowResized) {
159
+ resizedRows.add(rowIndex);
160
+ }
161
+ if (colResized) {
162
+ resizedCols.add(colIndex);
163
+ }
164
+ if (rowResized || colResized) {
165
+ sizeCache.set(key, size);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ if (resizedRows.size) {
171
+ const heightResizes = [];
172
+ resizedRows.forEach((rowIndex) => {
173
+ let maxHeight = 0;
174
+ maybeCachedColIndexes.forEach((colIndex) => {
175
+ const size = sizeCache.get(getKey(rowIndex, colIndex));
176
+ if (size) {
177
+ maxHeight = max(maxHeight, size[0]);
178
+ }
179
+ });
180
+ if (maxHeight) {
181
+ heightResizes.push([rowIndex, maxHeight]);
182
+ }
183
+ });
184
+ vStore._update(ACTION_ITEM_RESIZE, heightResizes);
185
+ }
186
+ if (resizedCols.size) {
187
+ const widthResizes = [];
188
+ resizedCols.forEach((colIndex) => {
189
+ let maxWidth = 0;
190
+ maybeCachedRowIndexes.forEach((rowIndex) => {
191
+ const size = sizeCache.get(getKey(rowIndex, colIndex));
192
+ if (size) {
193
+ maxWidth = max(maxWidth, size[1]);
194
+ }
195
+ });
196
+ if (maxWidth) {
197
+ widthResizes.push([colIndex, maxWidth]);
198
+ }
199
+ });
200
+ hStore._update(ACTION_ITEM_RESIZE, widthResizes);
201
+ }
202
+ });
203
+ return {
204
+ _observeRoot(viewport) {
205
+ resizeObserver._observe((viewportElement = viewport));
206
+ },
207
+ _observeItem(el, rowIndex, colIndex) {
208
+ mountedIndexes.set(el, [rowIndex, colIndex]);
209
+ maybeCachedRowIndexes.add(rowIndex);
210
+ maybeCachedColIndexes.add(colIndex);
211
+ resizeObserver._observe(el);
212
+ return () => {
213
+ mountedIndexes.delete(el);
214
+ resizeObserver._unobserve(el);
215
+ };
216
+ },
217
+ _dispose: resizeObserver._dispose,
218
+ };
219
+ };
@@ -0,0 +1,273 @@
1
+ import { getCurrentDocument, getCurrentWindow, isIOSWebKit, isRTLDocument, isSmoothScrollSupported, } from "./environment";
2
+ import { ACTION_SCROLL, ACTION_SCROLL_END, UPDATE_SIZE_STATE, ACTION_MANUAL_SCROLL, SCROLL_IDLE, ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, } from "./store";
3
+ import { debounce, timeout, clamp } from "./utils";
4
+ /**
5
+ * scrollLeft is negative value in rtl direction.
6
+ *
7
+ * left right
8
+ * -100 0 spec compliant
9
+ * https://github.com/othree/jquery.rtl-scroll-type
10
+ */
11
+ const normalizeOffset = (offset, isHorizontal) => {
12
+ if (isHorizontal && isRTLDocument()) {
13
+ return -offset;
14
+ }
15
+ else {
16
+ return offset;
17
+ }
18
+ };
19
+ const createScrollObserver = (store, viewport, isHorizontal, getScrollOffset, updateScrollOffset) => {
20
+ const now = Date.now;
21
+ let lastScrollTime = 0;
22
+ let wheeling = false;
23
+ let touching = false;
24
+ let justTouchEnded = false;
25
+ let stillMomentumScrolling = false;
26
+ const onScrollEnd = debounce(() => {
27
+ if (wheeling || touching) {
28
+ wheeling = false;
29
+ // Wait while wheeling or touching
30
+ onScrollEnd();
31
+ return;
32
+ }
33
+ justTouchEnded = false;
34
+ store._update(ACTION_SCROLL_END);
35
+ }, 150);
36
+ const onScroll = () => {
37
+ lastScrollTime = now();
38
+ if (justTouchEnded) {
39
+ stillMomentumScrolling = true;
40
+ }
41
+ store._update(ACTION_SCROLL, getScrollOffset());
42
+ onScrollEnd();
43
+ };
44
+ // Infer scroll state also from wheel events
45
+ // Sometimes scroll events do not fire when frame dropped even if the visual have been already scrolled
46
+ const onWheel = ((e) => {
47
+ if (wheeling ||
48
+ // Scroll start should be detected with scroll event
49
+ store._getScrollDirection() === SCROLL_IDLE ||
50
+ // Probably a pinch-to-zoom gesture
51
+ e.ctrlKey) {
52
+ return;
53
+ }
54
+ const timeDelta = now() - lastScrollTime;
55
+ if (
56
+ // Check if wheel event occurs some time after scrolling
57
+ 150 > timeDelta &&
58
+ 50 < timeDelta &&
59
+ // Get delta before checking deltaMode for firefox behavior
60
+ // https://github.com/w3c/uievents/issues/181#issuecomment-392648065
61
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1392460#c34
62
+ (isHorizontal ? e.deltaX : e.deltaY)) {
63
+ wheeling = true;
64
+ }
65
+ }); // FIXME type error. why only here?
66
+ const onTouchStart = () => {
67
+ touching = true;
68
+ justTouchEnded = stillMomentumScrolling = false;
69
+ };
70
+ const onTouchEnd = () => {
71
+ touching = false;
72
+ if (isIOSWebKit()) {
73
+ justTouchEnded = true;
74
+ }
75
+ };
76
+ viewport.addEventListener("scroll", onScroll);
77
+ viewport.addEventListener("wheel", onWheel, { passive: true });
78
+ viewport.addEventListener("touchstart", onTouchStart, { passive: true });
79
+ viewport.addEventListener("touchend", onTouchEnd, { passive: true });
80
+ return {
81
+ _dispose: () => {
82
+ viewport.removeEventListener("scroll", onScroll);
83
+ viewport.removeEventListener("wheel", onWheel);
84
+ viewport.removeEventListener("touchstart", onTouchStart);
85
+ viewport.removeEventListener("touchend", onTouchEnd);
86
+ onScrollEnd._cancel();
87
+ },
88
+ _fixScrollJump: (jump) => {
89
+ updateScrollOffset(jump, stillMomentumScrolling);
90
+ stillMomentumScrolling = false;
91
+ },
92
+ };
93
+ };
94
+ /**
95
+ * @internal
96
+ */
97
+ export const createScroller = (store, isHorizontal) => {
98
+ let viewportElement;
99
+ let scrollObserver;
100
+ let cancelScroll;
101
+ const scrollToKey = isHorizontal ? "scrollLeft" : "scrollTop";
102
+ const overflowKey = isHorizontal ? "overflowX" : "overflowY";
103
+ // The given offset will be clamped by browser
104
+ // https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
105
+ const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
106
+ if (!viewportElement)
107
+ return;
108
+ if (cancelScroll) {
109
+ // Cancel waiting scrollTo
110
+ cancelScroll();
111
+ }
112
+ const waitForMeasurement = () => {
113
+ // Wait for the scroll destination items to be measured.
114
+ // The measurement will be done asynchronously and the timing is not predictable so we use promise.
115
+ // For example, ResizeObserver may not fire when window is not visible.
116
+ let queue;
117
+ return [
118
+ new Promise((resolve, reject) => {
119
+ queue = resolve;
120
+ // Reject when items around scroll destination completely measured
121
+ timeout((cancelScroll = reject), 150);
122
+ }),
123
+ store._subscribe(UPDATE_SIZE_STATE, () => {
124
+ queue && queue();
125
+ }),
126
+ ];
127
+ };
128
+ if (smooth && isSmoothScrollSupported()) {
129
+ while (true) {
130
+ store._update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
131
+ if (!store._hasUnmeasuredItemsInFrozenRange()) {
132
+ break;
133
+ }
134
+ const [promise, unsubscribe] = waitForMeasurement();
135
+ try {
136
+ await promise;
137
+ }
138
+ catch (e) {
139
+ // canceled
140
+ return;
141
+ }
142
+ finally {
143
+ unsubscribe();
144
+ }
145
+ }
146
+ viewportElement.scrollTo({
147
+ [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
148
+ behavior: "smooth",
149
+ });
150
+ }
151
+ else {
152
+ while (true) {
153
+ const [promise, unsubscribe] = waitForMeasurement();
154
+ try {
155
+ viewportElement[scrollToKey] = normalizeOffset(getTargetOffset(), isHorizontal);
156
+ store._update(ACTION_MANUAL_SCROLL);
157
+ await promise;
158
+ }
159
+ catch (e) {
160
+ // canceled or finished
161
+ return;
162
+ }
163
+ finally {
164
+ unsubscribe();
165
+ }
166
+ }
167
+ }
168
+ };
169
+ return {
170
+ _observe(viewport) {
171
+ viewportElement = viewport;
172
+ scrollObserver = createScrollObserver(store, viewport, isHorizontal, () => normalizeOffset(viewport[scrollToKey], isHorizontal), (jump, isMomentumScrolling) => {
173
+ // If we update scroll position while touching on iOS, the position will be reverted.
174
+ // However iOS WebKit fires touch events only once at the beginning of momentum scrolling.
175
+ // That means we have no reliable way to confirm still touched or not if user touches more than once during momentum scrolling...
176
+ // This is a hack for the suspectable situations, inspired by https://github.com/prud/ios-overflow-scroll-to-top
177
+ if (isMomentumScrolling) {
178
+ const style = viewport.style;
179
+ const prev = style[overflowKey];
180
+ style[overflowKey] = "hidden";
181
+ timeout(() => {
182
+ style[overflowKey] = prev;
183
+ });
184
+ }
185
+ viewport[scrollToKey] += normalizeOffset(jump, isHorizontal);
186
+ });
187
+ },
188
+ _dispose() {
189
+ scrollObserver && scrollObserver._dispose();
190
+ },
191
+ _scrollTo(offset) {
192
+ scheduleImperativeScroll(() => offset);
193
+ },
194
+ _scrollBy(offset) {
195
+ offset += store._getScrollOffset();
196
+ scheduleImperativeScroll(() => offset);
197
+ },
198
+ _scrollToIndex(index, { align, smooth } = {}) {
199
+ index = clamp(index, 0, store._getItemsLength() - 1);
200
+ if (align === "nearest") {
201
+ const itemOffset = store._getItemOffset(index);
202
+ const scrollOffset = store._getScrollOffset();
203
+ if (itemOffset < scrollOffset) {
204
+ align = "start";
205
+ }
206
+ else if (itemOffset + store._getItemSize(index) >
207
+ scrollOffset + store._getViewportSize()) {
208
+ align = "end";
209
+ }
210
+ else {
211
+ // already completely visible
212
+ return;
213
+ }
214
+ }
215
+ scheduleImperativeScroll(() => {
216
+ return (store._getStartSpacerSize() +
217
+ (align === "end"
218
+ ? store._getItemOffset(index) +
219
+ store._getItemSize(index) -
220
+ store._getViewportSize()
221
+ : align === "center"
222
+ ? store._getItemOffset(index) +
223
+ (store._getItemSize(index) - store._getViewportSize()) / 2
224
+ : store._getItemOffset(index)));
225
+ }, smooth);
226
+ },
227
+ _fixScrollJump: (jump) => {
228
+ if (!scrollObserver)
229
+ return;
230
+ scrollObserver._fixScrollJump(jump);
231
+ },
232
+ };
233
+ };
234
+ /**
235
+ * @internal
236
+ */
237
+ export const createWindowScroller = (store, isHorizontal) => {
238
+ let scrollObserver;
239
+ return {
240
+ _observe(container) {
241
+ const scrollToKey = isHorizontal ? "scrollX" : "scrollY";
242
+ const document = getCurrentDocument(container);
243
+ const window = getCurrentWindow(document);
244
+ const documentBody = document.body;
245
+ const calcOffsetToViewport = (node, viewport, isHorizontal, offset = 0) => {
246
+ // TODO calc offset only when it changes (maybe impossible)
247
+ const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
248
+ const offsetSum = offset +
249
+ (isHorizontal && isRTLDocument()
250
+ ? window.innerWidth - node[offsetKey] - node.offsetWidth
251
+ : node[offsetKey]);
252
+ const parent = node.offsetParent;
253
+ if (node === viewport || !parent) {
254
+ return offsetSum;
255
+ }
256
+ return calcOffsetToViewport(parent, viewport, isHorizontal, offsetSum);
257
+ };
258
+ scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeOffset(window[scrollToKey], isHorizontal) -
259
+ calcOffsetToViewport(container, documentBody, isHorizontal), (jump) => {
260
+ // TODO support case two window scrollers exist in the same view
261
+ window.scrollBy(isHorizontal ? normalizeOffset(jump, isHorizontal) : 0, isHorizontal ? 0 : jump);
262
+ });
263
+ },
264
+ _dispose() {
265
+ scrollObserver && scrollObserver._dispose();
266
+ },
267
+ _fixScrollJump: (jump) => {
268
+ if (!scrollObserver)
269
+ return;
270
+ scrollObserver._fixScrollJump(jump);
271
+ },
272
+ };
273
+ };
package/core/store.js ADDED
@@ -0,0 +1,345 @@
1
+ import { initCache, getItemSize, computeTotalSize, computeOffset as computeStartOffset, UNCACHED, setItemSize, estimateDefaultItemSize, updateCacheLength, computeRange, } from "./cache";
2
+ import { isIOSWebKit } from "./environment";
3
+ import { abs, clamp, max, min } from "./utils";
4
+ // Scroll offset and sizes can have sub-pixel value if window.devicePixelRatio has decimal value
5
+ const SUBPIXEL_THRESHOLD = 1.5; // 0.5 * 3
6
+ /** @internal */
7
+ export const SCROLL_IDLE = 0;
8
+ /** @internal */
9
+ export const SCROLL_DOWN = 1;
10
+ /** @internal */
11
+ export const SCROLL_UP = 2;
12
+ const SCROLL_BY_NATIVE = 0;
13
+ const SCROLL_BY_MANUAL_SCROLL = 1;
14
+ const SCROLL_BY_PREPENDING = 2;
15
+ /** @internal */
16
+ export const ACTION_ITEM_RESIZE = 1;
17
+ /** @internal */
18
+ export const ACTION_VIEWPORT_RESIZE = 2;
19
+ /** @internal */
20
+ export const ACTION_ITEMS_LENGTH_CHANGE = 3;
21
+ /** @internal */
22
+ export const ACTION_SCROLL = 4;
23
+ /** @internal */
24
+ export const ACTION_SCROLL_END = 5;
25
+ /** @internal */
26
+ export const ACTION_MANUAL_SCROLL = 6;
27
+ /** @internal */
28
+ export const ACTION_BEFORE_MANUAL_SMOOTH_SCROLL = 7;
29
+ /** @internal */
30
+ export const UPDATE_SCROLL_STATE = 0b0001;
31
+ /** @internal */
32
+ export const UPDATE_SIZE_STATE = 0b0010;
33
+ /** @internal */
34
+ export const UPDATE_SCROLL_EVENT = 0b0100;
35
+ /** @internal */
36
+ export const UPDATE_SCROLL_END_EVENT = 0b1000;
37
+ /**
38
+ * @internal
39
+ */
40
+ export const getScrollSize = (store) => {
41
+ return max(store._getTotalSize(), store._getViewportSize());
42
+ };
43
+ /**
44
+ * @internal
45
+ */
46
+ export const getMinContainerSize = (store) => {
47
+ return max(store._getTotalSize(), store._getViewportSize() -
48
+ store._getStartSpacerSize() -
49
+ store._getEndSpacerSize());
50
+ };
51
+ /**
52
+ * @internal
53
+ */
54
+ export const overscanStartIndex = (startIndex, overscan, scrollDirection) => {
55
+ return max(startIndex - (scrollDirection === SCROLL_DOWN ? 1 : max(1, overscan)), 0);
56
+ };
57
+ /**
58
+ * @internal
59
+ */
60
+ export const overscanEndIndex = (endIndex, overscan, scrollDirection, count) => {
61
+ return min(endIndex + (scrollDirection === SCROLL_UP ? 1 : max(1, overscan)), count - 1);
62
+ };
63
+ const calculateJump = (cache, items, keepEnd) => {
64
+ return items.reduce((acc, [index, size]) => {
65
+ const diff = size - getItemSize(cache, index);
66
+ if (!keepEnd || diff > 0) {
67
+ acc += diff;
68
+ }
69
+ return acc;
70
+ }, 0);
71
+ };
72
+ /**
73
+ * @internal
74
+ */
75
+ export const createVirtualStore = (elementsCount, itemSize = 40, ssrCount = 0, cacheSnapshot, shouldAutoEstimateItemSize, startSpacerSize = 0, endSpacerSize = 0) => {
76
+ let isSSR = !!ssrCount;
77
+ let stateVersion = [];
78
+ let viewportSize = 0;
79
+ let scrollOffset = 0;
80
+ let jumpCount = 0;
81
+ let jump = 0;
82
+ let pendingJump = 0;
83
+ let _flushedJump = 0;
84
+ let _scrollDirection = SCROLL_IDLE;
85
+ let _scrollMode = SCROLL_BY_NATIVE;
86
+ let _frozenRange = isSSR
87
+ ? [0, max(ssrCount - 1, 0)]
88
+ : null;
89
+ let _prevRange = [0, 0];
90
+ const cache = cacheSnapshot || initCache(elementsCount, itemSize);
91
+ const subscribers = new Set();
92
+ const getTotalSize = () => computeTotalSize(cache);
93
+ const getScrollableSize = () => getTotalSize() + startSpacerSize + endSpacerSize;
94
+ const getRelativeScrollOffset = () => scrollOffset - startSpacerSize;
95
+ const getMaxScrollOffset = () =>
96
+ // total size can become smaller than viewport size
97
+ max(0, getScrollableSize() - viewportSize);
98
+ const applyJump = (j) => {
99
+ // In iOS WebKit browsers, updating scroll position will stop scrolling so it have to be deferred during scrolling.
100
+ if (isIOSWebKit() && _scrollDirection !== SCROLL_IDLE) {
101
+ pendingJump += j;
102
+ }
103
+ else {
104
+ jump += j;
105
+ jumpCount++;
106
+ }
107
+ };
108
+ return {
109
+ _getStateVersion() {
110
+ return stateVersion;
111
+ },
112
+ _getCacheSnapshot() {
113
+ return JSON.parse(JSON.stringify(cache));
114
+ },
115
+ _getRange() {
116
+ if (_frozenRange) {
117
+ return [
118
+ min(_prevRange[0], _frozenRange[0]),
119
+ max(_prevRange[1], _frozenRange[1]),
120
+ ];
121
+ }
122
+ // Return previous range for consistent render until next scroll event comes in.
123
+ if (_flushedJump) {
124
+ return _prevRange;
125
+ }
126
+ return (_prevRange = computeRange(cache, getRelativeScrollOffset() + pendingJump + jump, _prevRange[0], viewportSize));
127
+ },
128
+ _isUnmeasuredItem(index) {
129
+ return cache._sizes[index] === UNCACHED;
130
+ },
131
+ _hasUnmeasuredItemsInFrozenRange() {
132
+ if (!_frozenRange)
133
+ return false;
134
+ return cache._sizes
135
+ .slice(max(0, _frozenRange[0] - 1), min(cache._length - 1, _frozenRange[1] + 1) + 1)
136
+ .includes(UNCACHED);
137
+ },
138
+ _getItemOffset(index) {
139
+ return computeStartOffset(cache, index) - pendingJump;
140
+ },
141
+ _getItemSize(index) {
142
+ return getItemSize(cache, index);
143
+ },
144
+ _getItemsLength() {
145
+ return cache._length;
146
+ },
147
+ _getScrollOffset() {
148
+ return scrollOffset;
149
+ },
150
+ _getScrollDirection() {
151
+ return _scrollDirection;
152
+ },
153
+ _getViewportSize() {
154
+ return viewportSize;
155
+ },
156
+ _getStartSpacerSize() {
157
+ return startSpacerSize;
158
+ },
159
+ _getEndSpacerSize() {
160
+ return endSpacerSize;
161
+ },
162
+ _getTotalSize: getTotalSize,
163
+ _getJumpCount() {
164
+ return jumpCount;
165
+ },
166
+ _flushJump() {
167
+ if (viewportSize > getScrollableSize()) {
168
+ // In this case applying jump will not cause scroll.
169
+ // Current logic expects scroll event occurs after applying jump so discard it.
170
+ return (jump = 0);
171
+ }
172
+ _flushedJump = jump;
173
+ jump = 0;
174
+ return _flushedJump;
175
+ },
176
+ _subscribe(target, cb) {
177
+ const sub = [target, cb];
178
+ subscribers.add(sub);
179
+ return () => {
180
+ subscribers.delete(sub);
181
+ };
182
+ },
183
+ _update(type, payload) {
184
+ let shouldFlushPendingJump;
185
+ let shouldSync;
186
+ let mutated = 0;
187
+ switch (type) {
188
+ case ACTION_ITEM_RESIZE: {
189
+ const updated = payload.filter(([index, size]) => cache._sizes[index] !== size);
190
+ // Skip if all items are cached and not updated
191
+ if (!updated.length) {
192
+ break;
193
+ }
194
+ // Calculate jump
195
+ // Should maintain visible position to minimize junks in appearance
196
+ let diff = 0;
197
+ if (scrollOffset === 0) {
198
+ // Do nothing to stick to the start
199
+ }
200
+ else if (scrollOffset > getMaxScrollOffset() - SUBPIXEL_THRESHOLD) {
201
+ // Keep end to stick to the end
202
+ diff = calculateJump(cache, updated, true);
203
+ }
204
+ else {
205
+ if (_scrollMode === SCROLL_BY_PREPENDING) {
206
+ // Keep distance from end immediately after prepending
207
+ // We can assume jumps occurred on the upper outside
208
+ diff = calculateJump(cache, updated);
209
+ }
210
+ else {
211
+ // Keep start at mid
212
+ const [startIndex] = _prevRange;
213
+ diff = calculateJump(cache, updated.filter(([index]) => index < startIndex));
214
+ }
215
+ }
216
+ if (diff) {
217
+ applyJump(diff);
218
+ }
219
+ // Update item sizes
220
+ let isNewItemMeasured = false;
221
+ updated.forEach(([index, size]) => {
222
+ if (setItemSize(cache, index, size)) {
223
+ isNewItemMeasured = true;
224
+ }
225
+ });
226
+ // Estimate initial item size from measured sizes
227
+ if (shouldAutoEstimateItemSize &&
228
+ isNewItemMeasured &&
229
+ // TODO support reverse scroll also
230
+ !scrollOffset) {
231
+ estimateDefaultItemSize(cache);
232
+ }
233
+ mutated = UPDATE_SIZE_STATE;
234
+ // Synchronous update is necessary in current design to minimize visible glitch in concurrent rendering.
235
+ // However in React, synchronous update with flushSync after asynchronous update will overtake the asynchronous one.
236
+ // If items resize happens just after scroll, race condition can occur depending on implementation.
237
+ shouldSync = true;
238
+ break;
239
+ }
240
+ case ACTION_VIEWPORT_RESIZE: {
241
+ if (viewportSize !== payload) {
242
+ viewportSize = payload;
243
+ mutated = UPDATE_SIZE_STATE;
244
+ }
245
+ break;
246
+ }
247
+ case ACTION_ITEMS_LENGTH_CHANGE: {
248
+ if (payload[1]) {
249
+ // Calc distance before updating cache
250
+ const distanceToEnd = getMaxScrollOffset() - scrollOffset;
251
+ const [shift, isAdd] = updateCacheLength(cache, payload[0], true);
252
+ applyJump(isAdd ? shift : -min(shift, distanceToEnd));
253
+ if (isAdd) {
254
+ _scrollMode = SCROLL_BY_PREPENDING;
255
+ }
256
+ mutated = UPDATE_SCROLL_STATE;
257
+ }
258
+ else {
259
+ updateCacheLength(cache, payload[0]);
260
+ }
261
+ break;
262
+ }
263
+ case ACTION_SCROLL: {
264
+ // Scroll offset may exceed min or max especially in Safari's elastic scrolling.
265
+ const nextScrollOffset = clamp(payload, 0, getMaxScrollOffset());
266
+ const flushedJump = _flushedJump;
267
+ _flushedJump = 0;
268
+ // Skip if offset is not changed
269
+ if (nextScrollOffset === scrollOffset) {
270
+ break;
271
+ }
272
+ const delta = nextScrollOffset - scrollOffset;
273
+ const distance = abs(delta);
274
+ // Scroll event after jump compensation is not reliable because it may result in the opposite direction.
275
+ // The delta of artificial scroll may not be equal with the jump because it may be batched with other scrolls.
276
+ // 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).
277
+ const isJustJumped = flushedJump && distance < abs(flushedJump) + 1;
278
+ // Scroll events are dispatched enough so it's ok to skip some of them.
279
+ if (!isJustJumped &&
280
+ // Ignore until manual scrolling
281
+ _scrollMode === SCROLL_BY_NATIVE) {
282
+ _scrollDirection = delta < 0 ? SCROLL_UP : SCROLL_DOWN;
283
+ }
284
+ // TODO This will cause glitch in reverse infinite scrolling. Disable this until better solution is found.
285
+ // if (
286
+ // pendingJump &&
287
+ // ((_scrollDirection === SCROLL_UP &&
288
+ // payload - max(pendingJump, 0) <= 0) ||
289
+ // (_scrollDirection === SCROLL_DOWN &&
290
+ // payload - min(pendingJump, 0) >= getScrollOffsetMax()))
291
+ // ) {
292
+ // // Flush if almost reached to start or end
293
+ // shouldFlushPendingJump = true;
294
+ // }
295
+ if (isSSR) {
296
+ _frozenRange = null;
297
+ isSSR = false;
298
+ }
299
+ // Update synchronously if scrolled a lot
300
+ shouldSync = distance > viewportSize;
301
+ scrollOffset = nextScrollOffset;
302
+ mutated = UPDATE_SCROLL_STATE + UPDATE_SCROLL_EVENT;
303
+ break;
304
+ }
305
+ case ACTION_SCROLL_END: {
306
+ mutated = UPDATE_SCROLL_END_EVENT;
307
+ if (_scrollDirection !== SCROLL_IDLE) {
308
+ shouldFlushPendingJump = true;
309
+ mutated += UPDATE_SCROLL_STATE;
310
+ }
311
+ _scrollDirection = SCROLL_IDLE;
312
+ _scrollMode = SCROLL_BY_NATIVE;
313
+ _frozenRange = null;
314
+ break;
315
+ }
316
+ case ACTION_MANUAL_SCROLL: {
317
+ _scrollMode = SCROLL_BY_MANUAL_SCROLL;
318
+ break;
319
+ }
320
+ case ACTION_BEFORE_MANUAL_SMOOTH_SCROLL: {
321
+ _frozenRange = computeRange(cache, payload, _prevRange[0], viewportSize);
322
+ mutated = UPDATE_SCROLL_STATE;
323
+ break;
324
+ }
325
+ }
326
+ if (mutated) {
327
+ stateVersion = [];
328
+ if (shouldFlushPendingJump && pendingJump) {
329
+ jump += pendingJump;
330
+ pendingJump = 0;
331
+ jumpCount++;
332
+ }
333
+ subscribers.forEach(([target, cb]) => {
334
+ // Early return to skip React's computation
335
+ if (!(mutated & target)) {
336
+ return;
337
+ }
338
+ // https://github.com/facebook/react/issues/25191
339
+ // https://github.com/facebook/react/blob/a5fc797db14c6e05d4d5c4dbb22a0dd70d41f5d5/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1443-L1447
340
+ cb(shouldSync);
341
+ });
342
+ }
343
+ },
344
+ };
345
+ };
package/core/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/core/utils.js ADDED
@@ -0,0 +1,73 @@
1
+ /** @internal */
2
+ export const min = Math.min;
3
+ /** @internal */
4
+ export const max = Math.max;
5
+ /** @internal */
6
+ export const abs = Math.abs;
7
+ /** @internal */
8
+ export const values = Object.values;
9
+ /** @internal */
10
+ export const isArray = Array.isArray;
11
+ /** @internal */
12
+ export const timeout = setTimeout;
13
+ /**
14
+ * @internal
15
+ */
16
+ export const clamp = (value, minValue, maxValue) => min(maxValue, max(minValue, value));
17
+ /**
18
+ * @internal
19
+ */
20
+ export const exists = (v) => v != null;
21
+ /**
22
+ * @internal
23
+ */
24
+ export const median = (arr) => {
25
+ const s = [...arr].sort((a, b) => a - b);
26
+ const mid = (arr.length / 2) | 0;
27
+ return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid];
28
+ };
29
+ /**
30
+ * @internal
31
+ */
32
+ export const debounce = (fn, ms) => {
33
+ let id;
34
+ const cancel = () => {
35
+ if (exists(id)) {
36
+ clearTimeout(id);
37
+ }
38
+ };
39
+ const debouncedFn = () => {
40
+ cancel();
41
+ id = timeout(() => {
42
+ id = null;
43
+ fn();
44
+ }, ms);
45
+ };
46
+ debouncedFn._cancel = cancel;
47
+ return debouncedFn;
48
+ };
49
+ /**
50
+ * @internal
51
+ */
52
+ export const once = (fn) => {
53
+ let called;
54
+ let cache;
55
+ return ((...args) => {
56
+ if (!called) {
57
+ called = true;
58
+ cache = fn(...args);
59
+ }
60
+ return cache;
61
+ });
62
+ };
63
+ /**
64
+ * @internal
65
+ */
66
+ export const getStyleNumber = (v) => {
67
+ if (v) {
68
+ return parseFloat(v);
69
+ }
70
+ else {
71
+ return 0;
72
+ }
73
+ };
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./react";
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "cbvirtua",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "",
10
+ "license": "ISC"
11
+ }
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "vue/jsx-runtime";
2
+ /** @jsxImportSource vue */
3
+ import { ref, defineComponent, watch } from "vue";
4
+ import { isRTLDocument } from "../core/environment";
5
+ /**
6
+ * @internal
7
+ */
8
+ export const ListItem = /*#__PURE__*/ defineComponent({
9
+ props: {
10
+ _children: { type: Object, required: true },
11
+ _resizer: { type: Object, required: true },
12
+ _index: { type: Number, required: true },
13
+ _offset: { type: Number, required: true },
14
+ _hide: { type: Boolean },
15
+ _isHorizontal: { type: Boolean },
16
+ _element: { type: String, required: true },
17
+ },
18
+ setup(props) {
19
+ const elementRef = ref();
20
+ // The index may be changed if elements are inserted to or removed from the start of props.children
21
+ watch(() => elementRef.value && props._index, (_, __, onCleanup) => {
22
+ const cleanupObserver = props._resizer(elementRef.value, props._index);
23
+ onCleanup(cleanupObserver);
24
+ }, {
25
+ flush: "post",
26
+ });
27
+ return () => {
28
+ const { _children: children, _offset: offset, _hide: hide, _element: Element, _isHorizontal: isHorizontal, } = props;
29
+ const style = {
30
+ margin: 0,
31
+ padding: 0,
32
+ position: "absolute",
33
+ [isHorizontal ? "height" : "width"]: "100%",
34
+ [isHorizontal ? "top" : "left"]: "0px",
35
+ [isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]: offset + "px",
36
+ visibility: hide ? "hidden" : "visible",
37
+ };
38
+ if (isHorizontal) {
39
+ style.display = "flex";
40
+ }
41
+ return (_jsx(Element, { ref: elementRef, style: style, children: children }));
42
+ };
43
+ },
44
+ });
package/vue/VList.js ADDED
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx } from "vue/jsx-runtime";
2
+ /** @jsxImportSource vue */
3
+ import { ref, onMounted, defineComponent, onUnmounted, watch, } from "vue";
4
+ import { SCROLL_IDLE, UPDATE_SCROLL_STATE, UPDATE_SCROLL_EVENT, UPDATE_SCROLL_END_EVENT, UPDATE_SIZE_STATE, overscanEndIndex, overscanStartIndex, createVirtualStore, ACTION_ITEMS_LENGTH_CHANGE, getScrollSize, getMinContainerSize, } from "../core/store";
5
+ import { createResizer } from "../core/resizer";
6
+ import { createScroller } from "../core/scroller";
7
+ import { ListItem } from "./ListItem";
8
+ import { exists } from "../core/utils";
9
+ const props = {
10
+ /**
11
+ * The data items rendered by this component.
12
+ */
13
+ data: { type: Array, required: true },
14
+ /**
15
+ * Number of items to render above/below the visible bounds of the list. You can increase to avoid showing blank items in fast scrolling.
16
+ * @defaultValue 4
17
+ */
18
+ overscan: { type: Number, default: 4 },
19
+ /**
20
+ * Item size hint for unmeasured items. It will help to reduce scroll jump when items are measured if used properly.
21
+ *
22
+ * - If not set, initial item sizes will be automatically estimated from measured sizes. This is recommended for most cases.
23
+ * - If set, you can opt out estimation and use the value as initial item size.
24
+ */
25
+ itemSize: Number,
26
+ /**
27
+ * While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling.
28
+ */
29
+ shift: Boolean,
30
+ /**
31
+ * If true, rendered as a horizontally scrollable list. Otherwise rendered as a vertically scrollable list.
32
+ */
33
+ horizontal: Boolean,
34
+ };
35
+ export const VList = /*#__PURE__*/ defineComponent({
36
+ props: props,
37
+ emits: ["scroll", "scrollEnd", "rangeChange"],
38
+ setup(props, { emit, expose, slots }) {
39
+ var _a;
40
+ const isHorizontal = props.horizontal;
41
+ const rootRef = ref();
42
+ const store = createVirtualStore(props.data.length, (_a = props.itemSize) !== null && _a !== void 0 ? _a : 40, undefined, undefined, !props.itemSize);
43
+ const resizer = createResizer(store, isHorizontal);
44
+ const scroller = createScroller(store, isHorizontal);
45
+ const rerender = ref(store._getStateVersion());
46
+ const unsubscribeStore = store._subscribe(UPDATE_SCROLL_STATE + UPDATE_SIZE_STATE, () => {
47
+ rerender.value = store._getStateVersion();
48
+ });
49
+ const unsubscribeOnScroll = store._subscribe(UPDATE_SCROLL_EVENT, () => {
50
+ emit("scroll", store._getScrollOffset());
51
+ });
52
+ const unsubscribeOnScrollEnd = store._subscribe(UPDATE_SCROLL_END_EVENT, () => {
53
+ emit("scrollEnd");
54
+ });
55
+ onMounted(() => {
56
+ const root = rootRef.value;
57
+ if (!root)
58
+ return;
59
+ resizer._observeRoot(root);
60
+ scroller._observe(root);
61
+ });
62
+ onUnmounted(() => {
63
+ unsubscribeStore();
64
+ unsubscribeOnScroll();
65
+ unsubscribeOnScrollEnd();
66
+ resizer._dispose();
67
+ scroller._dispose();
68
+ });
69
+ watch(() => props.data.length, (count) => {
70
+ store._update(ACTION_ITEMS_LENGTH_CHANGE, [count, props.shift]);
71
+ });
72
+ watch([rerender, store._getJumpCount], ([, count], [, prevCount]) => {
73
+ if (count === prevCount)
74
+ return;
75
+ const jump = store._flushJump();
76
+ if (!jump)
77
+ return;
78
+ scroller._fixScrollJump(jump);
79
+ }, { flush: "post" });
80
+ watch([rerender, store._getRange], ([, [start, end]], [, [prevStart, prevEnd]]) => {
81
+ if (prevStart === start && prevEnd === end)
82
+ return;
83
+ emit("rangeChange", start, end);
84
+ }, { flush: "post" });
85
+ expose({
86
+ get scrollOffset() {
87
+ return store._getScrollOffset();
88
+ },
89
+ get scrollSize() {
90
+ return getScrollSize(store);
91
+ },
92
+ get viewportSize() {
93
+ return store._getViewportSize();
94
+ },
95
+ scrollToIndex: scroller._scrollToIndex,
96
+ scrollTo: scroller._scrollTo,
97
+ scrollBy: scroller._scrollBy,
98
+ });
99
+ return () => {
100
+ rerender.value;
101
+ const count = props.data.length;
102
+ const [startIndex, endIndex] = store._getRange();
103
+ const scrollDirection = store._getScrollDirection();
104
+ const totalSize = store._getTotalSize();
105
+ // https://github.com/inokawa/virtua/issues/252#issuecomment-1822861368
106
+ const minSize = getMinContainerSize(store);
107
+ const items = [];
108
+ for (let i = overscanStartIndex(startIndex, props.overscan, scrollDirection), j = overscanEndIndex(endIndex, props.overscan, scrollDirection, count); i <= j; i++) {
109
+ const e = slots.default(props.data[i])[0];
110
+ const key = e.key;
111
+ items.push(_jsx(ListItem, { _resizer: resizer._observeItem, _index: i, _offset: store._getItemOffset(i), _hide: store._isUnmeasuredItem(i), _element: "div", _children: e, _isHorizontal: isHorizontal }, exists(key) ? key : "_" + i));
112
+ }
113
+ return (_jsx("div", { ref: rootRef, style: {
114
+ display: isHorizontal ? "inline-block" : "block",
115
+ [isHorizontal ? "overflowX" : "overflowY"]: "auto",
116
+ overflowAnchor: "none",
117
+ contain: "strict",
118
+ width: "100%",
119
+ height: "100%",
120
+ }, children: _jsx("div", { style: {
121
+ // contain: "content",
122
+ overflowAnchor: "none", // opt out browser's scroll anchoring because it will conflict to scroll anchoring of virtualizer
123
+ flex: "none", // flex style on parent can break layout
124
+ position: "relative",
125
+ visibility: "hidden",
126
+ width: isHorizontal ? totalSize + "px" : "100%",
127
+ height: isHorizontal ? "100%" : totalSize + "px",
128
+ [isHorizontal ? "minWidth" : "minHeight"]: minSize + "px",
129
+ pointerEvents: scrollDirection !== SCROLL_IDLE ? "none" : "auto",
130
+ }, children: items }) }));
131
+ };
132
+ },
133
+ });
package/vue/index.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @module vue
3
+ */
4
+ export { VList } from "./VList";