@pdanpdan/virtual-scroll 0.4.0 → 0.6.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/README.md +172 -324
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +836 -376
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1334 -741
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +8 -2
- package/src/components/VirtualScroll.test.ts +1921 -325
- package/src/components/VirtualScroll.vue +829 -386
- package/src/components/VirtualScrollbar.test.ts +174 -0
- package/src/components/VirtualScrollbar.vue +102 -0
- package/src/composables/useVirtualScroll.test.ts +1506 -228
- package/src/composables/useVirtualScroll.ts +869 -517
- package/src/composables/useVirtualScrollbar.test.ts +526 -0
- package/src/composables/useVirtualScrollbar.ts +244 -0
- package/src/index.ts +9 -0
- package/src/types.ts +353 -110
- package/src/utils/fenwick-tree.test.ts +39 -39
- package/src/utils/scroll.test.ts +181 -101
- package/src/utils/scroll.ts +43 -5
- package/src/utils/virtual-scroll-logic.test.ts +673 -323
- package/src/utils/virtual-scroll-logic.ts +759 -430
|
@@ -7,13 +7,18 @@ import type {
|
|
|
7
7
|
ScrollToIndexOptions,
|
|
8
8
|
VirtualScrollProps,
|
|
9
9
|
} from '../types';
|
|
10
|
-
import type {
|
|
10
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
11
11
|
|
|
12
12
|
/* global ScrollToOptions */
|
|
13
|
-
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
|
13
|
+
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, toValue, watch } from 'vue';
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_BUFFER,
|
|
17
|
+
DEFAULT_COLUMN_WIDTH,
|
|
18
|
+
DEFAULT_ITEM_SIZE,
|
|
19
|
+
} from '../types';
|
|
15
20
|
import { FenwickTree } from '../utils/fenwick-tree';
|
|
16
|
-
import { getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions } from '../utils/scroll';
|
|
21
|
+
import { BROWSER_MAX_SIZE, getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions, isWindowLike, scrollTo } from '../utils/scroll';
|
|
17
22
|
import {
|
|
18
23
|
calculateColumnRange,
|
|
19
24
|
calculateItemPosition,
|
|
@@ -21,30 +26,21 @@ import {
|
|
|
21
26
|
calculateScrollTarget,
|
|
22
27
|
calculateStickyItem,
|
|
23
28
|
calculateTotalSize,
|
|
29
|
+
displayToVirtual,
|
|
30
|
+
findPrevStickyIndex,
|
|
31
|
+
virtualToDisplay,
|
|
24
32
|
} from '../utils/virtual-scroll-logic';
|
|
25
33
|
|
|
26
|
-
export {
|
|
27
|
-
type RenderedItem,
|
|
28
|
-
type ScrollAlignment,
|
|
29
|
-
type ScrollAlignmentOptions,
|
|
30
|
-
type ScrollDetails,
|
|
31
|
-
type ScrollDirection,
|
|
32
|
-
type ScrollToIndexOptions,
|
|
33
|
-
type VirtualScrollProps,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const DEFAULT_ITEM_SIZE = 40;
|
|
37
|
-
export const DEFAULT_COLUMN_WIDTH = 100;
|
|
38
|
-
export const DEFAULT_BUFFER = 5;
|
|
39
|
-
|
|
40
34
|
/**
|
|
41
35
|
* Composable for virtual scrolling logic.
|
|
42
36
|
* Handles calculation of visible items, scroll events, dynamic item sizes, and programmatic scrolling.
|
|
43
37
|
*
|
|
44
|
-
* @param
|
|
38
|
+
* @param propsInput - The configuration properties. Can be a plain object, a Ref, or a getter function.
|
|
45
39
|
* @see VirtualScrollProps
|
|
46
40
|
*/
|
|
47
|
-
export function useVirtualScroll<T = unknown>(
|
|
41
|
+
export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<VirtualScrollProps<T>>) {
|
|
42
|
+
const props = computed(() => toValue(propsInput));
|
|
43
|
+
|
|
48
44
|
// --- State ---
|
|
49
45
|
const scrollX = ref(0);
|
|
50
46
|
const scrollY = ref(0);
|
|
@@ -52,12 +48,38 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
52
48
|
const isHydrated = ref(false);
|
|
53
49
|
const isHydrating = ref(false);
|
|
54
50
|
const isMounted = ref(false);
|
|
51
|
+
const isRtl = ref(false);
|
|
55
52
|
const viewportWidth = ref(0);
|
|
56
53
|
const viewportHeight = ref(0);
|
|
57
54
|
const hostOffset = reactive({ x: 0, y: 0 });
|
|
55
|
+
const hostRefOffset = reactive({ x: 0, y: 0 });
|
|
58
56
|
let scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
59
57
|
|
|
60
58
|
const isProgrammaticScroll = ref(false);
|
|
59
|
+
const internalScrollX = ref(0);
|
|
60
|
+
const internalScrollY = ref(0);
|
|
61
|
+
|
|
62
|
+
let computedStyle: CSSStyleDeclaration | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detects the current direction (LTR/RTL) of the scroll container.
|
|
66
|
+
*/
|
|
67
|
+
const updateDirection = () => {
|
|
68
|
+
if (typeof window === 'undefined') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const container = props.value.container || props.value.hostRef || window;
|
|
72
|
+
const el = isElement(container) ? container : document.documentElement;
|
|
73
|
+
|
|
74
|
+
if (!computedStyle || !('direction' in computedStyle)) {
|
|
75
|
+
computedStyle = window.getComputedStyle(el);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const newRtl = computedStyle.direction === 'rtl';
|
|
79
|
+
if (isRtl.value !== newRtl) {
|
|
80
|
+
isRtl.value = newRtl;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
// --- Fenwick Trees for efficient size and offset management ---
|
|
63
85
|
const itemSizesX = new FenwickTree(props.value.items?.length || 0);
|
|
@@ -82,6 +104,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
82
104
|
let lastItems: T[] = [];
|
|
83
105
|
|
|
84
106
|
// --- Computed Config ---
|
|
107
|
+
const direction = computed(() => [ 'vertical', 'horizontal', 'both' ].includes(props.value.direction as string) ? props.value.direction as ScrollDirection : 'vertical' as ScrollDirection);
|
|
108
|
+
|
|
85
109
|
const isDynamicItemSize = computed(() =>
|
|
86
110
|
props.value.itemSize === undefined || props.value.itemSize === null || props.value.itemSize === 0,
|
|
87
111
|
);
|
|
@@ -111,47 +135,79 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
111
135
|
const paddingStartY = computed(() => getPaddingY(props.value.scrollPaddingStart, props.value.direction));
|
|
112
136
|
const paddingEndY = computed(() => getPaddingY(props.value.scrollPaddingEnd, props.value.direction));
|
|
113
137
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
const stickyStartX = computed(() => getPaddingX(props.value.stickyStart, props.value.direction));
|
|
139
|
+
const stickyEndX = computed(() => getPaddingX(props.value.stickyEnd, props.value.direction));
|
|
140
|
+
const stickyStartY = computed(() => getPaddingY(props.value.stickyStart, props.value.direction));
|
|
141
|
+
const stickyEndY = computed(() => getPaddingY(props.value.stickyEnd, props.value.direction));
|
|
118
142
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
const flowStartX = computed(() => getPaddingX(props.value.flowPaddingStart, props.value.direction));
|
|
144
|
+
const flowEndX = computed(() => getPaddingX(props.value.flowPaddingEnd, props.value.direction));
|
|
145
|
+
const flowStartY = computed(() => getPaddingY(props.value.flowPaddingStart, props.value.direction));
|
|
146
|
+
const flowEndY = computed(() => getPaddingY(props.value.flowPaddingEnd, props.value.direction));
|
|
147
|
+
|
|
148
|
+
const usableWidth = computed(() => viewportWidth.value - (direction.value !== 'vertical' ? (stickyStartX.value + stickyEndX.value) : 0));
|
|
149
|
+
|
|
150
|
+
const usableHeight = computed(() => viewportHeight.value - (direction.value !== 'horizontal' ? (stickyStartY.value + stickyEndY.value) : 0));
|
|
123
151
|
|
|
124
152
|
// --- Size Calculations ---
|
|
125
153
|
/**
|
|
126
|
-
* Total width of all items in the scrollable area.
|
|
154
|
+
* Total size (width and height) of all items in the scrollable area.
|
|
127
155
|
*/
|
|
128
|
-
const
|
|
156
|
+
const totalSize = computed(() => {
|
|
129
157
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
130
158
|
treeUpdateFlag.value;
|
|
131
159
|
|
|
132
160
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
133
161
|
const { start = 0, end = 0, colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
134
162
|
const colCount = props.value.columnCount || 0;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
163
|
+
const gap = props.value.gap || 0;
|
|
164
|
+
const columnGap = props.value.columnGap || 0;
|
|
165
|
+
|
|
166
|
+
let width = 0;
|
|
167
|
+
let height = 0;
|
|
168
|
+
|
|
169
|
+
if (direction.value === 'both') {
|
|
170
|
+
if (colCount > 0) {
|
|
171
|
+
const effectiveColEnd = colEnd || colCount;
|
|
172
|
+
const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
|
|
173
|
+
width = Math.max(0, total - (effectiveColEnd > colStart ? columnGap : 0));
|
|
138
174
|
}
|
|
139
|
-
const effectiveColEnd = colEnd || colCount;
|
|
140
|
-
const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
|
|
141
|
-
return Math.max(0, total - (effectiveColEnd > colStart ? (props.value.columnGap || 0) : 0));
|
|
142
|
-
}
|
|
143
|
-
if (props.value.direction === 'horizontal') {
|
|
144
175
|
if (fixedItemSize.value !== null) {
|
|
145
176
|
const len = end - start;
|
|
146
|
-
|
|
177
|
+
height = Math.max(0, len * (fixedItemSize.value + gap) - (len > 0 ? gap : 0));
|
|
178
|
+
} else {
|
|
179
|
+
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
180
|
+
height = Math.max(0, total - (end > start ? gap : 0));
|
|
181
|
+
}
|
|
182
|
+
} else if (direction.value === 'horizontal') {
|
|
183
|
+
if (fixedItemSize.value !== null) {
|
|
184
|
+
const len = end - start;
|
|
185
|
+
width = Math.max(0, len * (fixedItemSize.value + columnGap) - (len > 0 ? columnGap : 0));
|
|
186
|
+
} else {
|
|
187
|
+
const total = itemSizesX.query(end) - itemSizesX.query(start);
|
|
188
|
+
width = Math.max(0, total - (end > start ? columnGap : 0));
|
|
189
|
+
}
|
|
190
|
+
height = usableHeight.value;
|
|
191
|
+
} else {
|
|
192
|
+
// vertical
|
|
193
|
+
width = usableWidth.value;
|
|
194
|
+
if (fixedItemSize.value !== null) {
|
|
195
|
+
const len = end - start;
|
|
196
|
+
height = Math.max(0, len * (fixedItemSize.value + gap) - (len > 0 ? gap : 0));
|
|
197
|
+
} else {
|
|
198
|
+
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
199
|
+
height = Math.max(0, total - (end > start ? gap : 0));
|
|
147
200
|
}
|
|
148
|
-
const total = itemSizesX.query(end) - itemSizesX.query(start);
|
|
149
|
-
return Math.max(0, total - (end > start ? (props.value.columnGap || 0) : 0));
|
|
150
201
|
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
width: Math.max(width, usableWidth.value),
|
|
205
|
+
height: Math.max(height, usableHeight.value),
|
|
206
|
+
};
|
|
151
207
|
}
|
|
152
208
|
|
|
153
209
|
return calculateTotalSize({
|
|
154
|
-
direction:
|
|
210
|
+
direction: direction.value,
|
|
155
211
|
itemsLength: props.value.items.length,
|
|
156
212
|
columnCount: props.value.columnCount || 0,
|
|
157
213
|
fixedSize: fixedItemSize.value,
|
|
@@ -163,60 +219,74 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
163
219
|
queryY: (idx) => itemSizesY.query(idx),
|
|
164
220
|
queryX: (idx) => itemSizesX.query(idx),
|
|
165
221
|
queryColumn: (idx) => columnSizes.query(idx),
|
|
166
|
-
})
|
|
222
|
+
});
|
|
167
223
|
});
|
|
168
224
|
|
|
169
|
-
|
|
170
|
-
* Total height of all items in the scrollable area.
|
|
171
|
-
*/
|
|
172
|
-
const totalHeight = computed(() => {
|
|
173
|
-
// eslint-disable-next-line ts/no-unused-expressions
|
|
174
|
-
treeUpdateFlag.value;
|
|
225
|
+
const isWindowContainer = computed(() => isWindowLike(props.value.container));
|
|
175
226
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
227
|
+
const virtualWidth = computed(() => totalSize.value.width + paddingStartX.value + paddingEndX.value);
|
|
228
|
+
const virtualHeight = computed(() => totalSize.value.height + paddingStartY.value + paddingEndY.value);
|
|
229
|
+
|
|
230
|
+
const totalWidth = computed(() => (flowStartX.value + stickyStartX.value + stickyEndX.value + flowEndX.value + virtualWidth.value));
|
|
231
|
+
|
|
232
|
+
const totalHeight = computed(() => (flowStartY.value + stickyStartY.value + stickyEndY.value + flowEndY.value + virtualHeight.value));
|
|
233
|
+
|
|
234
|
+
const componentOffset = reactive({
|
|
235
|
+
x: computed(() => Math.max(0, hostOffset.x - (flowStartX.value + stickyStartX.value))),
|
|
236
|
+
y: computed(() => Math.max(0, hostOffset.y - (flowStartY.value + stickyStartY.value))),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const renderedWidth = computed(() => (isWindowContainer.value ? totalWidth.value : Math.min(totalWidth.value, BROWSER_MAX_SIZE)));
|
|
240
|
+
const renderedHeight = computed(() => (isWindowContainer.value ? totalHeight.value : Math.min(totalHeight.value, BROWSER_MAX_SIZE)));
|
|
241
|
+
|
|
242
|
+
const renderedVirtualWidth = computed(() => (isWindowContainer.value ? virtualWidth.value : Math.max(0, renderedWidth.value - (flowStartX.value + stickyStartX.value + stickyEndX.value + flowEndX.value))));
|
|
243
|
+
const renderedVirtualHeight = computed(() => (isWindowContainer.value ? virtualHeight.value : Math.max(0, renderedHeight.value - (flowStartY.value + stickyStartY.value + stickyEndY.value + flowEndY.value))));
|
|
244
|
+
|
|
245
|
+
const scaleX = computed(() => {
|
|
246
|
+
if (isWindowContainer.value || totalWidth.value <= BROWSER_MAX_SIZE) {
|
|
247
|
+
return 1;
|
|
186
248
|
}
|
|
249
|
+
const realRange = totalWidth.value - viewportWidth.value;
|
|
250
|
+
const displayRange = renderedWidth.value - viewportWidth.value;
|
|
251
|
+
return displayRange > 0 ? realRange / displayRange : 1;
|
|
252
|
+
});
|
|
187
253
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
columnGap: props.value.columnGap || 0,
|
|
196
|
-
usableWidth: usableWidth.value,
|
|
197
|
-
usableHeight: usableHeight.value,
|
|
198
|
-
queryY: (idx) => itemSizesY.query(idx),
|
|
199
|
-
queryX: (idx) => itemSizesX.query(idx),
|
|
200
|
-
queryColumn: (idx) => columnSizes.query(idx),
|
|
201
|
-
}).height;
|
|
254
|
+
const scaleY = computed(() => {
|
|
255
|
+
if (isWindowContainer.value || totalHeight.value <= BROWSER_MAX_SIZE) {
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const realRange = totalHeight.value - viewportHeight.value;
|
|
259
|
+
const displayRange = renderedHeight.value - viewportHeight.value;
|
|
260
|
+
return displayRange > 0 ? realRange / displayRange : 1;
|
|
202
261
|
});
|
|
203
262
|
|
|
204
263
|
const relativeScrollX = computed(() => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
264
|
+
if (direction.value === 'vertical') {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
const flowPaddingX = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
268
|
+
return internalScrollX.value - flowPaddingX;
|
|
208
269
|
});
|
|
270
|
+
|
|
209
271
|
const relativeScrollY = computed(() => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
272
|
+
if (direction.value === 'horizontal') {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const flowPaddingY = flowStartY.value + stickyStartY.value + paddingStartY.value;
|
|
276
|
+
return internalScrollY.value - flowPaddingY;
|
|
213
277
|
});
|
|
214
278
|
|
|
215
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Returns the currently calculated width for a specific column index, taking measurements and gaps into account.
|
|
281
|
+
*
|
|
282
|
+
* @param index - The column index.
|
|
283
|
+
* @returns The width in pixels (excluding gap).
|
|
284
|
+
*/
|
|
216
285
|
const getColumnWidth = (index: number) => {
|
|
217
286
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
218
287
|
treeUpdateFlag.value;
|
|
219
288
|
|
|
289
|
+
const columnGap = props.value.columnGap || 0;
|
|
220
290
|
const cw = props.value.columnWidth;
|
|
221
291
|
if (typeof cw === 'number' && cw > 0) {
|
|
222
292
|
return cw;
|
|
@@ -228,7 +298,36 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
228
298
|
if (typeof cw === 'function') {
|
|
229
299
|
return cw(index);
|
|
230
300
|
}
|
|
231
|
-
|
|
301
|
+
const val = columnSizes.get(index);
|
|
302
|
+
return val > 0 ? val - columnGap : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Returns the currently calculated height for a specific row index, taking measurements and gaps into account.
|
|
307
|
+
*
|
|
308
|
+
* @param index - The row index.
|
|
309
|
+
* @returns The height in pixels (excluding gap).
|
|
310
|
+
*/
|
|
311
|
+
const getRowHeight = (index: number) => {
|
|
312
|
+
// eslint-disable-next-line ts/no-unused-expressions
|
|
313
|
+
treeUpdateFlag.value;
|
|
314
|
+
|
|
315
|
+
if (direction.value === 'horizontal') {
|
|
316
|
+
return usableHeight.value;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const gap = props.value.gap || 0;
|
|
320
|
+
const itemSize = props.value.itemSize;
|
|
321
|
+
if (typeof itemSize === 'number' && itemSize > 0) {
|
|
322
|
+
return itemSize;
|
|
323
|
+
}
|
|
324
|
+
if (typeof itemSize === 'function') {
|
|
325
|
+
const item = props.value.items[ index ];
|
|
326
|
+
return item !== undefined ? itemSize(item, index) : (props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const val = itemSizesY.get(index);
|
|
330
|
+
return val > 0 ? val - gap : (props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
|
|
232
331
|
};
|
|
233
332
|
|
|
234
333
|
// --- Public Scroll API ---
|
|
@@ -240,29 +339,24 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
240
339
|
* @param options - Scroll options including alignment ('start', 'center', 'end', 'auto') and behavior ('auto', 'smooth').
|
|
241
340
|
* Defaults to { align: 'auto', behavior: 'auto' }.
|
|
242
341
|
*/
|
|
243
|
-
|
|
342
|
+
function scrollToIndex(
|
|
244
343
|
rowIndex: number | null | undefined,
|
|
245
344
|
colIndex: number | null | undefined,
|
|
246
345
|
options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions,
|
|
247
|
-
)
|
|
346
|
+
) {
|
|
248
347
|
const isCorrection = typeof options === 'object' && options !== null && 'isCorrection' in options
|
|
249
348
|
? options.isCorrection
|
|
250
349
|
: false;
|
|
251
350
|
|
|
252
351
|
const container = props.value.container || window;
|
|
253
352
|
|
|
254
|
-
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
255
|
-
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
256
|
-
|
|
257
353
|
const { targetX, targetY, effectiveAlignX, effectiveAlignY } = calculateScrollTarget({
|
|
258
354
|
rowIndex,
|
|
259
355
|
colIndex,
|
|
260
356
|
options,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
usableWidth: usableWidth.value,
|
|
265
|
-
usableHeight: usableHeight.value,
|
|
357
|
+
direction: direction.value,
|
|
358
|
+
viewportWidth: viewportWidth.value,
|
|
359
|
+
viewportHeight: viewportHeight.value,
|
|
266
360
|
totalWidth: totalWidth.value,
|
|
267
361
|
totalHeight: totalHeight.value,
|
|
268
362
|
gap: props.value.gap || 0,
|
|
@@ -277,7 +371,21 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
277
371
|
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
278
372
|
getColumnSize: (idx) => columnSizes.get(idx),
|
|
279
373
|
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
374
|
+
scaleX: scaleX.value,
|
|
375
|
+
scaleY: scaleY.value,
|
|
376
|
+
hostOffsetX: componentOffset.x,
|
|
377
|
+
hostOffsetY: componentOffset.y,
|
|
280
378
|
stickyIndices: sortedStickyIndices.value,
|
|
379
|
+
stickyStartX: stickyStartX.value,
|
|
380
|
+
stickyStartY: stickyStartY.value,
|
|
381
|
+
stickyEndX: stickyEndX.value,
|
|
382
|
+
stickyEndY: stickyEndY.value,
|
|
383
|
+
flowPaddingStartX: flowStartX.value,
|
|
384
|
+
flowPaddingStartY: flowStartY.value,
|
|
385
|
+
paddingStartX: paddingStartX.value,
|
|
386
|
+
paddingStartY: paddingStartY.value,
|
|
387
|
+
paddingEndX: paddingEndX.value,
|
|
388
|
+
paddingEndY: paddingEndY.value,
|
|
281
389
|
});
|
|
282
390
|
|
|
283
391
|
if (!isCorrection) {
|
|
@@ -292,8 +400,11 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
292
400
|
};
|
|
293
401
|
}
|
|
294
402
|
|
|
295
|
-
const
|
|
296
|
-
const
|
|
403
|
+
const displayTargetX = virtualToDisplay(targetX, componentOffset.x, scaleX.value);
|
|
404
|
+
const displayTargetY = virtualToDisplay(targetY, componentOffset.y, scaleY.value);
|
|
405
|
+
|
|
406
|
+
const finalX = isRtl.value ? -displayTargetX : displayTargetX;
|
|
407
|
+
const finalY = displayTargetY;
|
|
297
408
|
|
|
298
409
|
let behavior: 'auto' | 'smooth' | undefined;
|
|
299
410
|
if (isScrollToIndexOptions(options)) {
|
|
@@ -303,108 +414,92 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
303
414
|
const scrollBehavior = isCorrection ? 'auto' : (behavior || 'smooth');
|
|
304
415
|
isProgrammaticScroll.value = true;
|
|
305
416
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
behavior: scrollBehavior,
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
if (colIndex !== null && colIndex !== undefined) {
|
|
318
|
-
scrollOptions.left = Math.max(0, finalX);
|
|
319
|
-
}
|
|
320
|
-
if (rowIndex !== null && rowIndex !== undefined) {
|
|
321
|
-
scrollOptions.top = Math.max(0, finalY);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (typeof container.scrollTo === 'function') {
|
|
325
|
-
container.scrollTo(scrollOptions);
|
|
326
|
-
} else {
|
|
327
|
-
if (scrollOptions.left !== undefined) {
|
|
328
|
-
container.scrollLeft = scrollOptions.left;
|
|
329
|
-
}
|
|
330
|
-
if (scrollOptions.top !== undefined) {
|
|
331
|
-
container.scrollTop = scrollOptions.top;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
417
|
+
const scrollOptions: ScrollToOptions = {
|
|
418
|
+
behavior: scrollBehavior,
|
|
419
|
+
};
|
|
420
|
+
if (colIndex !== null && colIndex !== undefined) {
|
|
421
|
+
scrollOptions.left = isRtl.value ? finalX : Math.max(0, finalX);
|
|
422
|
+
}
|
|
423
|
+
if (rowIndex !== null && rowIndex !== undefined) {
|
|
424
|
+
scrollOptions.top = Math.max(0, finalY);
|
|
334
425
|
}
|
|
335
426
|
|
|
427
|
+
scrollTo(container, scrollOptions);
|
|
428
|
+
|
|
336
429
|
if (scrollBehavior === 'auto' || scrollBehavior === undefined) {
|
|
337
430
|
if (colIndex !== null && colIndex !== undefined) {
|
|
338
|
-
scrollX.value = Math.max(0, finalX);
|
|
431
|
+
scrollX.value = (isRtl.value ? finalX : Math.max(0, finalX));
|
|
432
|
+
internalScrollX.value = targetX;
|
|
339
433
|
}
|
|
340
434
|
if (rowIndex !== null && rowIndex !== undefined) {
|
|
341
435
|
scrollY.value = Math.max(0, finalY);
|
|
436
|
+
internalScrollY.value = targetY;
|
|
342
437
|
}
|
|
343
|
-
}
|
|
344
438
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
439
|
+
if (pendingScroll.value) {
|
|
440
|
+
const currentOptions = pendingScroll.value.options;
|
|
441
|
+
if (isScrollToIndexOptions(currentOptions)) {
|
|
442
|
+
currentOptions.behavior = 'auto';
|
|
443
|
+
} else {
|
|
444
|
+
pendingScroll.value.options = {
|
|
445
|
+
align: currentOptions as ScrollAlignment | ScrollAlignmentOptions,
|
|
446
|
+
behavior: 'auto',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
349
452
|
|
|
350
453
|
/**
|
|
351
454
|
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
352
455
|
*
|
|
353
456
|
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
354
457
|
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
355
|
-
* @param options - Scroll options (behavior)
|
|
458
|
+
* @param options - Scroll options (behavior).
|
|
356
459
|
* @param options.behavior - The scroll behavior ('auto' | 'smooth'). Defaults to 'auto'.
|
|
357
460
|
*/
|
|
358
461
|
const scrollToOffset = (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => {
|
|
359
462
|
const container = props.value.container || window;
|
|
360
463
|
isProgrammaticScroll.value = true;
|
|
361
|
-
|
|
362
|
-
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
363
|
-
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
464
|
+
pendingScroll.value = null;
|
|
364
465
|
|
|
365
466
|
const clampedX = (x !== null && x !== undefined)
|
|
366
|
-
?
|
|
467
|
+
? Math.max(0, Math.min(x, totalWidth.value - viewportWidth.value))
|
|
367
468
|
: null;
|
|
368
469
|
const clampedY = (y !== null && y !== undefined)
|
|
369
|
-
?
|
|
470
|
+
? Math.max(0, Math.min(y, totalHeight.value - viewportHeight.value))
|
|
370
471
|
: null;
|
|
371
472
|
|
|
473
|
+
if (clampedX !== null) {
|
|
474
|
+
internalScrollX.value = clampedX;
|
|
475
|
+
}
|
|
476
|
+
if (clampedY !== null) {
|
|
477
|
+
internalScrollY.value = clampedY;
|
|
478
|
+
}
|
|
479
|
+
|
|
372
480
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
373
481
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
374
482
|
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
if (typeof window !== 'undefined' && container === window) {
|
|
379
|
-
window.scrollTo({
|
|
380
|
-
left: (x !== null && x !== undefined) ? targetX : undefined,
|
|
381
|
-
top: (y !== null && y !== undefined) ? targetY : undefined,
|
|
382
|
-
behavior: options?.behavior || 'auto',
|
|
383
|
-
} as ScrollToOptions);
|
|
384
|
-
} else if (isScrollableElement(container)) {
|
|
385
|
-
const scrollOptions: ScrollToOptions = {
|
|
386
|
-
behavior: options?.behavior || 'auto',
|
|
387
|
-
};
|
|
483
|
+
const displayTargetX = (clampedX !== null) ? virtualToDisplay(clampedX, componentOffset.x, scaleX.value) : null;
|
|
484
|
+
const displayTargetY = (clampedY !== null) ? virtualToDisplay(clampedY, componentOffset.y, scaleY.value) : null;
|
|
388
485
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
scrollOptions.top = targetY;
|
|
394
|
-
}
|
|
486
|
+
const targetX = (displayTargetX !== null)
|
|
487
|
+
? (isRtl.value ? -displayTargetX : displayTargetX)
|
|
488
|
+
: currentX;
|
|
489
|
+
const targetY = (displayTargetY !== null) ? displayTargetY : currentY;
|
|
395
490
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
}
|
|
491
|
+
const scrollOptions: ScrollToOptions = {
|
|
492
|
+
behavior: options?.behavior || 'auto',
|
|
493
|
+
};
|
|
494
|
+
if (x !== null && x !== undefined) {
|
|
495
|
+
scrollOptions.left = targetX;
|
|
496
|
+
}
|
|
497
|
+
if (y !== null && y !== undefined) {
|
|
498
|
+
scrollOptions.top = targetY;
|
|
406
499
|
}
|
|
407
500
|
|
|
501
|
+
scrollTo(container, scrollOptions);
|
|
502
|
+
|
|
408
503
|
if (options?.behavior === 'auto' || options?.behavior === undefined) {
|
|
409
504
|
if (x !== null && x !== undefined) {
|
|
410
505
|
scrollX.value = targetX;
|
|
@@ -416,11 +511,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
416
511
|
};
|
|
417
512
|
|
|
418
513
|
// --- Measurement & Initialization ---
|
|
419
|
-
const
|
|
420
|
-
const newItems = props.value.items;
|
|
421
|
-
const len = newItems.length;
|
|
422
|
-
const colCount = props.value.columnCount || 0;
|
|
423
|
-
|
|
514
|
+
const resizeMeasurements = (len: number, colCount: number) => {
|
|
424
515
|
itemSizesX.resize(len);
|
|
425
516
|
itemSizesY.resize(len);
|
|
426
517
|
columnSizes.resize(colCount);
|
|
@@ -440,70 +531,21 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
440
531
|
newMeasuredCols.set(measuredColumns.subarray(0, Math.min(colCount, measuredColumns.length)));
|
|
441
532
|
measuredColumns = newMeasuredCols;
|
|
442
533
|
}
|
|
534
|
+
};
|
|
443
535
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (prependCount > 0) {
|
|
458
|
-
itemSizesX.shift(prependCount);
|
|
459
|
-
itemSizesY.shift(prependCount);
|
|
460
|
-
|
|
461
|
-
if (pendingScroll.value && pendingScroll.value.rowIndex !== null && pendingScroll.value.rowIndex !== undefined) {
|
|
462
|
-
pendingScroll.value.rowIndex += prependCount;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const newMeasuredX = new Uint8Array(len);
|
|
466
|
-
const newMeasuredY = new Uint8Array(len);
|
|
467
|
-
newMeasuredX.set(measuredItemsX.subarray(0, Math.min(len - prependCount, measuredItemsX.length)), prependCount);
|
|
468
|
-
newMeasuredY.set(measuredItemsY.subarray(0, Math.min(len - prependCount, measuredItemsY.length)), prependCount);
|
|
469
|
-
measuredItemsX = newMeasuredX;
|
|
470
|
-
measuredItemsY = newMeasuredY;
|
|
471
|
-
|
|
472
|
-
// Calculate added size
|
|
473
|
-
const gap = props.value.gap || 0;
|
|
474
|
-
const columnGap = props.value.columnGap || 0;
|
|
475
|
-
let addedX = 0;
|
|
476
|
-
let addedY = 0;
|
|
477
|
-
|
|
478
|
-
for (let i = 0; i < prependCount; i++) {
|
|
479
|
-
const size = typeof props.value.itemSize === 'function'
|
|
480
|
-
? props.value.itemSize(newItems[ i ] as T, i)
|
|
481
|
-
: defaultSize.value;
|
|
482
|
-
|
|
483
|
-
if (props.value.direction === 'horizontal') {
|
|
484
|
-
addedX += size + columnGap;
|
|
485
|
-
} else {
|
|
486
|
-
addedY += size + gap;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
536
|
+
const initializeMeasurements = () => {
|
|
537
|
+
const newItems = props.value.items;
|
|
538
|
+
const len = newItems.length;
|
|
539
|
+
const colCount = props.value.columnCount || 0;
|
|
540
|
+
const gap = props.value.gap || 0;
|
|
541
|
+
const columnGap = props.value.columnGap || 0;
|
|
542
|
+
const cw = props.value.columnWidth;
|
|
489
543
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
scrollToOffset(
|
|
493
|
-
addedX > 0 ? relativeScrollX.value + addedX : null,
|
|
494
|
-
addedY > 0 ? relativeScrollY.value + addedY : null,
|
|
495
|
-
{ behavior: 'auto' },
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
544
|
+
let colNeedsRebuild = false;
|
|
545
|
+
let itemsNeedRebuild = false;
|
|
500
546
|
|
|
501
547
|
// Initialize columns
|
|
502
548
|
if (colCount > 0) {
|
|
503
|
-
const columnGap = props.value.columnGap || 0;
|
|
504
|
-
let colNeedsRebuild = false;
|
|
505
|
-
const cw = props.value.columnWidth;
|
|
506
|
-
|
|
507
549
|
for (let i = 0; i < colCount; i++) {
|
|
508
550
|
const currentW = columnSizes.get(i);
|
|
509
551
|
const isMeasured = measuredColumns[ i ] === 1;
|
|
@@ -530,35 +572,20 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
530
572
|
}
|
|
531
573
|
}
|
|
532
574
|
}
|
|
533
|
-
if (colNeedsRebuild) {
|
|
534
|
-
columnSizes.rebuild();
|
|
535
|
-
}
|
|
536
575
|
}
|
|
537
576
|
|
|
538
|
-
|
|
539
|
-
const columnGap = props.value.columnGap || 0;
|
|
540
|
-
let itemsNeedRebuild = false;
|
|
541
|
-
|
|
577
|
+
// Initialize items
|
|
542
578
|
for (let i = 0; i < len; i++) {
|
|
543
579
|
const item = props.value.items[ i ];
|
|
544
580
|
const currentX = itemSizesX.get(i);
|
|
545
581
|
const currentY = itemSizesY.get(i);
|
|
546
|
-
|
|
547
|
-
const isVertical = props.value.direction === 'vertical';
|
|
548
|
-
const isHorizontal = props.value.direction === 'horizontal';
|
|
549
|
-
const isBoth = props.value.direction === 'both';
|
|
550
|
-
|
|
551
582
|
const isMeasuredX = measuredItemsX[ i ] === 1;
|
|
552
583
|
const isMeasuredY = measuredItemsY[ i ] === 1;
|
|
553
584
|
|
|
554
|
-
|
|
555
|
-
if (isHorizontal) {
|
|
585
|
+
if (direction.value === 'horizontal') {
|
|
556
586
|
if (!isDynamicItemSize.value || (!isMeasuredX && currentX === 0)) {
|
|
557
|
-
const baseSize = typeof props.value.itemSize === 'function'
|
|
558
|
-
? props.value.itemSize(item as T, i)
|
|
559
|
-
: defaultSize.value;
|
|
587
|
+
const baseSize = typeof props.value.itemSize === 'function' ? props.value.itemSize(item as T, i) : defaultSize.value;
|
|
560
588
|
const targetX = baseSize + columnGap;
|
|
561
|
-
|
|
562
589
|
if (Math.abs(currentX - targetX) > 0.5) {
|
|
563
590
|
itemSizesX.set(i, targetX);
|
|
564
591
|
measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
@@ -573,14 +600,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
573
600
|
itemsNeedRebuild = true;
|
|
574
601
|
}
|
|
575
602
|
|
|
576
|
-
|
|
577
|
-
if (isVertical || isBoth) {
|
|
603
|
+
if (direction.value !== 'horizontal') {
|
|
578
604
|
if (!isDynamicItemSize.value || (!isMeasuredY && currentY === 0)) {
|
|
579
|
-
const baseSize = typeof props.value.itemSize === 'function'
|
|
580
|
-
? props.value.itemSize(item as T, i)
|
|
581
|
-
: defaultSize.value;
|
|
605
|
+
const baseSize = typeof props.value.itemSize === 'function' ? props.value.itemSize(item as T, i) : defaultSize.value;
|
|
582
606
|
const targetY = baseSize + gap;
|
|
583
|
-
|
|
584
607
|
if (Math.abs(currentY - targetY) > 0.5) {
|
|
585
608
|
itemSizesY.set(i, targetY);
|
|
586
609
|
measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
@@ -596,10 +619,75 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
596
619
|
}
|
|
597
620
|
}
|
|
598
621
|
|
|
622
|
+
if (colNeedsRebuild) {
|
|
623
|
+
columnSizes.rebuild();
|
|
624
|
+
}
|
|
599
625
|
if (itemsNeedRebuild) {
|
|
600
626
|
itemSizesX.rebuild();
|
|
601
627
|
itemSizesY.rebuild();
|
|
602
628
|
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const initializeSizes = () => {
|
|
632
|
+
const newItems = props.value.items;
|
|
633
|
+
const len = newItems.length;
|
|
634
|
+
const colCount = props.value.columnCount || 0;
|
|
635
|
+
|
|
636
|
+
resizeMeasurements(len, colCount);
|
|
637
|
+
|
|
638
|
+
let prependCount = 0;
|
|
639
|
+
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
640
|
+
const oldFirstItem = lastItems[ 0 ];
|
|
641
|
+
if (oldFirstItem !== undefined) {
|
|
642
|
+
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
643
|
+
if (newItems[ i ] === oldFirstItem) {
|
|
644
|
+
prependCount = i;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (prependCount > 0) {
|
|
652
|
+
itemSizesX.shift(prependCount);
|
|
653
|
+
itemSizesY.shift(prependCount);
|
|
654
|
+
|
|
655
|
+
if (pendingScroll.value && pendingScroll.value.rowIndex !== null && pendingScroll.value.rowIndex !== undefined) {
|
|
656
|
+
pendingScroll.value.rowIndex += prependCount;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const newMeasuredX = new Uint8Array(len);
|
|
660
|
+
const newMeasuredY = new Uint8Array(len);
|
|
661
|
+
newMeasuredX.set(measuredItemsX.subarray(0, Math.min(len - prependCount, measuredItemsX.length)), prependCount);
|
|
662
|
+
newMeasuredY.set(measuredItemsY.subarray(0, Math.min(len - prependCount, measuredItemsY.length)), prependCount);
|
|
663
|
+
measuredItemsX = newMeasuredX;
|
|
664
|
+
measuredItemsY = newMeasuredY;
|
|
665
|
+
|
|
666
|
+
// Calculate added size
|
|
667
|
+
const gap = props.value.gap || 0;
|
|
668
|
+
const columnGap = props.value.columnGap || 0;
|
|
669
|
+
let addedX = 0;
|
|
670
|
+
let addedY = 0;
|
|
671
|
+
|
|
672
|
+
for (let i = 0; i < prependCount; i++) {
|
|
673
|
+
const size = typeof props.value.itemSize === 'function' ? props.value.itemSize(newItems[ i ] as T, i) : defaultSize.value;
|
|
674
|
+
if (direction.value === 'horizontal') {
|
|
675
|
+
addedX += size + columnGap;
|
|
676
|
+
} else { addedY += size + gap; }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (addedX > 0 || addedY > 0) {
|
|
680
|
+
nextTick(() => {
|
|
681
|
+
scrollToOffset(
|
|
682
|
+
addedX > 0 ? relativeScrollX.value + addedX : null,
|
|
683
|
+
addedY > 0 ? relativeScrollY.value + addedY : null,
|
|
684
|
+
{ behavior: 'auto', isCorrection: true } as ScrollToIndexOptions,
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
initializeMeasurements();
|
|
603
691
|
|
|
604
692
|
lastItems = [ ...newItems ];
|
|
605
693
|
sizesInitialized.value = true;
|
|
@@ -610,50 +698,149 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
610
698
|
* Updates the host element's offset relative to the scroll container.
|
|
611
699
|
*/
|
|
612
700
|
const updateHostOffset = () => {
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
let newX = 0;
|
|
618
|
-
let newY = 0;
|
|
701
|
+
if (typeof window === 'undefined') {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const container = props.value.container || window;
|
|
619
705
|
|
|
706
|
+
const calculateOffset = (el: HTMLElement) => {
|
|
707
|
+
const rect = el.getBoundingClientRect();
|
|
620
708
|
if (container === window) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
709
|
+
return {
|
|
710
|
+
x: isRtl.value
|
|
711
|
+
? document.documentElement.clientWidth - rect.right - window.scrollX
|
|
712
|
+
: rect.left + window.scrollX,
|
|
713
|
+
y: rect.top + window.scrollY,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (container === el) {
|
|
717
|
+
return { x: 0, y: 0 };
|
|
718
|
+
}
|
|
719
|
+
if (isElement(container)) {
|
|
627
720
|
const containerRect = container.getBoundingClientRect();
|
|
628
|
-
|
|
629
|
-
|
|
721
|
+
return {
|
|
722
|
+
x: isRtl.value
|
|
723
|
+
? containerRect.right - rect.right - container.scrollLeft
|
|
724
|
+
: rect.left - containerRect.left + container.scrollLeft,
|
|
725
|
+
y: rect.top - containerRect.top + container.scrollTop,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { x: 0, y: 0 };
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
if (props.value.hostElement) {
|
|
732
|
+
const newOffset = calculateOffset(props.value.hostElement);
|
|
733
|
+
if (Math.abs(hostOffset.x - newOffset.x) > 0.1 || Math.abs(hostOffset.y - newOffset.y) > 0.1) {
|
|
734
|
+
hostOffset.x = newOffset.x;
|
|
735
|
+
hostOffset.y = newOffset.y;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (props.value.hostRef) {
|
|
740
|
+
const newOffset = calculateOffset(props.value.hostRef);
|
|
741
|
+
if (Math.abs(hostRefOffset.x - newOffset.x) > 0.1 || Math.abs(hostRefOffset.y - newOffset.y) > 0.1) {
|
|
742
|
+
hostRefOffset.x = newOffset.x;
|
|
743
|
+
hostRefOffset.y = newOffset.y;
|
|
630
744
|
}
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
watch([
|
|
749
|
+
() => props.value.items,
|
|
750
|
+
() => props.value.items.length,
|
|
751
|
+
() => props.value.direction,
|
|
752
|
+
() => props.value.columnCount,
|
|
753
|
+
() => props.value.columnWidth,
|
|
754
|
+
() => props.value.itemSize,
|
|
755
|
+
() => props.value.gap,
|
|
756
|
+
() => props.value.columnGap,
|
|
757
|
+
() => props.value.defaultItemSize,
|
|
758
|
+
() => props.value.defaultColumnWidth,
|
|
759
|
+
], initializeSizes, { immediate: true });
|
|
760
|
+
|
|
761
|
+
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
762
|
+
updateHostOffset();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
watch(isRtl, (newRtl, oldRtl) => {
|
|
766
|
+
if (oldRtl === undefined || newRtl === oldRtl || !isMounted.value) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Use the oldRtl to correctly interpret the current scrollX
|
|
771
|
+
if (direction.value === 'vertical') {
|
|
772
|
+
updateHostOffset();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const scrollValue = oldRtl ? Math.abs(scrollX.value) : scrollX.value;
|
|
777
|
+
const oldRelativeScrollX = displayToVirtual(scrollValue, hostOffset.x, scaleX.value);
|
|
778
|
+
|
|
779
|
+
// Update host offset for the new direction
|
|
780
|
+
updateHostOffset();
|
|
781
|
+
|
|
782
|
+
// Maintain logical horizontal position when direction changes
|
|
783
|
+
scrollToOffset(oldRelativeScrollX, null, { behavior: 'auto' });
|
|
784
|
+
}, { flush: 'sync' });
|
|
785
|
+
|
|
786
|
+
watch([ scaleX, scaleY ], () => {
|
|
787
|
+
if (!isMounted.value || isScrolling.value || isProgrammaticScroll.value) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
// Sync display scroll to maintain logical position
|
|
791
|
+
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
watch([ () => props.value.items.length, () => props.value.columnCount ], ([ newLen, newColCount ], [ oldLen, oldColCount ]) => {
|
|
795
|
+
nextTick(() => {
|
|
796
|
+
const maxRelX = Math.max(0, totalWidth.value - viewportWidth.value);
|
|
797
|
+
const maxRelY = Math.max(0, totalHeight.value - viewportHeight.value);
|
|
798
|
+
|
|
799
|
+
if (internalScrollX.value > maxRelX || internalScrollY.value > maxRelY) {
|
|
800
|
+
scrollToOffset(
|
|
801
|
+
Math.min(internalScrollX.value, maxRelX),
|
|
802
|
+
Math.min(internalScrollY.value, maxRelY),
|
|
803
|
+
{ behavior: 'auto' },
|
|
804
|
+
);
|
|
805
|
+
} else if ((newLen !== oldLen && scaleY.value !== 1) || (newColCount !== oldColCount && scaleX.value !== 1)) {
|
|
806
|
+
// Even if within bounds, we must sync the display scroll position
|
|
807
|
+
// because the coordinate scaling factor changed.
|
|
808
|
+
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
809
|
+
}
|
|
810
|
+
updateHostOffset();
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// --- Range & Visible Items ---
|
|
815
|
+
const getRowIndexAt = (offset: number) => {
|
|
816
|
+
const gap = props.value.gap || 0;
|
|
817
|
+
const columnGap = props.value.columnGap || 0;
|
|
818
|
+
const fixedSize = fixedItemSize.value;
|
|
631
819
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
820
|
+
if (direction.value === 'horizontal') {
|
|
821
|
+
const step = (fixedSize || 0) + columnGap;
|
|
822
|
+
if (fixedSize !== null && step > 0) {
|
|
823
|
+
return Math.floor(offset / step);
|
|
635
824
|
}
|
|
825
|
+
return itemSizesX.findLowerBound(offset);
|
|
826
|
+
}
|
|
827
|
+
const step = (fixedSize || 0) + gap;
|
|
828
|
+
if (fixedSize !== null && step > 0) {
|
|
829
|
+
return Math.floor(offset / step);
|
|
636
830
|
}
|
|
831
|
+
return itemSizesY.findLowerBound(offset);
|
|
637
832
|
};
|
|
638
833
|
|
|
639
|
-
|
|
640
|
-
()
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
()
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
() => props.value.defaultItemSize,
|
|
649
|
-
() => props.value.defaultColumnWidth,
|
|
650
|
-
], initializeSizes, { immediate: true });
|
|
651
|
-
|
|
652
|
-
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
653
|
-
updateHostOffset();
|
|
654
|
-
});
|
|
834
|
+
const getColIndexAt = (offset: number) => {
|
|
835
|
+
if (direction.value === 'both') {
|
|
836
|
+
return columnSizes.findLowerBound(offset);
|
|
837
|
+
}
|
|
838
|
+
if (direction.value === 'horizontal') {
|
|
839
|
+
return getRowIndexAt(offset);
|
|
840
|
+
}
|
|
841
|
+
return 0;
|
|
842
|
+
};
|
|
655
843
|
|
|
656
|
-
// --- Range & Visible Items ---
|
|
657
844
|
/**
|
|
658
845
|
* Current range of items that should be rendered.
|
|
659
846
|
*/
|
|
@@ -672,7 +859,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
672
859
|
const bufferAfter = props.value.bufferAfter ?? DEFAULT_BUFFER;
|
|
673
860
|
|
|
674
861
|
return calculateRange({
|
|
675
|
-
direction:
|
|
862
|
+
direction: direction.value,
|
|
676
863
|
relativeScrollX: relativeScrollX.value,
|
|
677
864
|
relativeScrollY: relativeScrollY.value,
|
|
678
865
|
usableWidth: usableWidth.value,
|
|
@@ -697,20 +884,61 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
697
884
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
698
885
|
treeUpdateFlag.value;
|
|
699
886
|
|
|
700
|
-
const
|
|
701
|
-
const
|
|
702
|
-
const
|
|
887
|
+
const offsetX = relativeScrollX.value + stickyStartX.value;
|
|
888
|
+
const offsetY = relativeScrollY.value + stickyStartY.value;
|
|
889
|
+
const offset = direction.value === 'horizontal' ? offsetX : offsetY;
|
|
890
|
+
return getRowIndexAt(offset);
|
|
891
|
+
});
|
|
703
892
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
893
|
+
const columnRange = computed(() => {
|
|
894
|
+
// eslint-disable-next-line ts/no-unused-expressions
|
|
895
|
+
treeUpdateFlag.value;
|
|
896
|
+
|
|
897
|
+
const totalCols = props.value.columnCount || 0;
|
|
898
|
+
|
|
899
|
+
if (!totalCols) {
|
|
900
|
+
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
709
901
|
}
|
|
710
|
-
|
|
711
|
-
|
|
902
|
+
|
|
903
|
+
if ((!isHydrated.value || isHydrating.value) && props.value.ssrRange) {
|
|
904
|
+
const { colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
905
|
+
const safeStart = Math.max(0, colStart);
|
|
906
|
+
const safeEnd = Math.min(totalCols, colEnd || totalCols);
|
|
907
|
+
|
|
908
|
+
const columnGap = props.value.columnGap || 0;
|
|
909
|
+
const padStart = fixedColumnWidth.value !== null
|
|
910
|
+
? safeStart * (fixedColumnWidth.value + columnGap)
|
|
911
|
+
: columnSizes.query(safeStart);
|
|
912
|
+
|
|
913
|
+
const totalColWidth = fixedColumnWidth.value !== null
|
|
914
|
+
? totalCols * (fixedColumnWidth.value + columnGap) - columnGap
|
|
915
|
+
: Math.max(0, columnSizes.query(totalCols) - columnGap);
|
|
916
|
+
|
|
917
|
+
const contentEnd = fixedColumnWidth.value !== null
|
|
918
|
+
? (safeEnd * (fixedColumnWidth.value + columnGap) - (safeEnd > 0 ? columnGap : 0))
|
|
919
|
+
: (columnSizes.query(safeEnd) - (safeEnd > 0 ? columnGap : 0));
|
|
920
|
+
|
|
921
|
+
return {
|
|
922
|
+
start: safeStart,
|
|
923
|
+
end: safeEnd,
|
|
924
|
+
padStart,
|
|
925
|
+
padEnd: Math.max(0, totalColWidth - contentEnd),
|
|
926
|
+
};
|
|
712
927
|
}
|
|
713
|
-
|
|
928
|
+
|
|
929
|
+
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
930
|
+
|
|
931
|
+
return calculateColumnRange({
|
|
932
|
+
columnCount: totalCols,
|
|
933
|
+
relativeScrollX: relativeScrollX.value,
|
|
934
|
+
usableWidth: usableWidth.value,
|
|
935
|
+
colBuffer,
|
|
936
|
+
fixedWidth: fixedColumnWidth.value,
|
|
937
|
+
columnGap: props.value.columnGap || 0,
|
|
938
|
+
findLowerBound: (offset) => columnSizes.findLowerBound(offset),
|
|
939
|
+
query: (idx) => columnSizes.query(idx),
|
|
940
|
+
totalColsQuery: () => columnSizes.query(totalCols),
|
|
941
|
+
});
|
|
714
942
|
});
|
|
715
943
|
|
|
716
944
|
/**
|
|
@@ -731,80 +959,77 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
731
959
|
const stickyIndices = sortedStickyIndices.value;
|
|
732
960
|
const stickySet = stickyIndicesSet.value;
|
|
733
961
|
|
|
734
|
-
|
|
735
|
-
const indicesToRender = new Set<number>();
|
|
736
|
-
for (let i = start; i < end; i++) {
|
|
737
|
-
indicesToRender.add(i);
|
|
738
|
-
}
|
|
962
|
+
const sortedIndices: number[] = [];
|
|
739
963
|
|
|
740
964
|
if (isHydrated.value || !props.value.ssrRange) {
|
|
741
965
|
const activeIdx = currentIndex.value;
|
|
742
|
-
|
|
743
|
-
let prevStickyIdx: number | undefined;
|
|
744
|
-
let low = 0;
|
|
745
|
-
let high = stickyIndices.length - 1;
|
|
746
|
-
while (low <= high) {
|
|
747
|
-
const mid = (low + high) >>> 1;
|
|
748
|
-
if (stickyIndices[ mid ]! < activeIdx) {
|
|
749
|
-
prevStickyIdx = stickyIndices[ mid ];
|
|
750
|
-
low = mid + 1;
|
|
751
|
-
} else {
|
|
752
|
-
high = mid - 1;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (prevStickyIdx !== undefined) {
|
|
757
|
-
indicesToRender.add(prevStickyIdx);
|
|
758
|
-
}
|
|
966
|
+
const prevStickyIdx = findPrevStickyIndex(stickyIndices, activeIdx);
|
|
759
967
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
let stickyHigh = stickyIndices.length - 1;
|
|
763
|
-
let firstInRange = -1;
|
|
764
|
-
|
|
765
|
-
while (stickyLow <= stickyHigh) {
|
|
766
|
-
const mid = (stickyLow + stickyHigh) >>> 1;
|
|
767
|
-
if (stickyIndices[ mid ]! >= start) {
|
|
768
|
-
firstInRange = mid;
|
|
769
|
-
stickyHigh = mid - 1;
|
|
770
|
-
} else {
|
|
771
|
-
stickyLow = mid + 1;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if (firstInRange !== -1) {
|
|
776
|
-
for (let i = firstInRange; i < stickyIndices.length; i++) {
|
|
777
|
-
const idx = stickyIndices[ i ]!;
|
|
778
|
-
if (idx >= end) {
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
indicesToRender.add(idx);
|
|
782
|
-
}
|
|
968
|
+
if (prevStickyIdx !== undefined && prevStickyIdx < start) {
|
|
969
|
+
sortedIndices.push(prevStickyIdx);
|
|
783
970
|
}
|
|
784
971
|
}
|
|
785
972
|
|
|
786
|
-
|
|
973
|
+
for (let i = start; i < end; i++) {
|
|
974
|
+
sortedIndices.push(i);
|
|
975
|
+
}
|
|
787
976
|
|
|
788
977
|
const ssrStartRow = props.value.ssrRange?.start || 0;
|
|
978
|
+
|
|
789
979
|
const ssrStartCol = props.value.ssrRange?.colStart || 0;
|
|
790
980
|
|
|
791
981
|
let ssrOffsetX = 0;
|
|
792
982
|
let ssrOffsetY = 0;
|
|
793
983
|
|
|
794
984
|
if (!isHydrated.value && props.value.ssrRange) {
|
|
795
|
-
ssrOffsetY = (
|
|
985
|
+
ssrOffsetY = (direction.value !== 'horizontal')
|
|
796
986
|
? (fixedSize !== null ? ssrStartRow * (fixedSize + gap) : itemSizesY.query(ssrStartRow))
|
|
797
987
|
: 0;
|
|
798
988
|
|
|
799
|
-
if (
|
|
989
|
+
if (direction.value === 'horizontal') {
|
|
800
990
|
ssrOffsetX = fixedSize !== null ? ssrStartCol * (fixedSize + columnGap) : itemSizesX.query(ssrStartCol);
|
|
801
|
-
} else if (
|
|
991
|
+
} else if (direction.value === 'both') {
|
|
802
992
|
ssrOffsetX = columnSizes.query(ssrStartCol);
|
|
803
993
|
}
|
|
804
994
|
}
|
|
805
995
|
|
|
806
996
|
const lastItemsMap = new Map(lastRenderedItems.map((it) => [ it.index, it ]));
|
|
807
997
|
|
|
998
|
+
// Optimization: Cache sequential queries to avoid O(log N) tree traversal for every item
|
|
999
|
+
let lastIndexX = -1;
|
|
1000
|
+
let lastOffsetX = 0;
|
|
1001
|
+
let lastIndexY = -1;
|
|
1002
|
+
let lastOffsetY = 0;
|
|
1003
|
+
|
|
1004
|
+
const queryXCached = (idx: number) => {
|
|
1005
|
+
if (idx === lastIndexX + 1) {
|
|
1006
|
+
lastOffsetX += itemSizesX.get(lastIndexX);
|
|
1007
|
+
lastIndexX = idx;
|
|
1008
|
+
return lastOffsetX;
|
|
1009
|
+
}
|
|
1010
|
+
lastOffsetX = itemSizesX.query(idx);
|
|
1011
|
+
lastIndexX = idx;
|
|
1012
|
+
return lastOffsetX;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const queryYCached = (idx: number) => {
|
|
1016
|
+
if (idx === lastIndexY + 1) {
|
|
1017
|
+
lastOffsetY += itemSizesY.get(lastIndexY);
|
|
1018
|
+
lastIndexY = idx;
|
|
1019
|
+
return lastOffsetY;
|
|
1020
|
+
}
|
|
1021
|
+
lastOffsetY = itemSizesY.query(idx);
|
|
1022
|
+
lastIndexY = idx;
|
|
1023
|
+
return lastOffsetY;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
1027
|
+
const itemsStartVU_Y = flowStartY.value + stickyStartY.value + paddingStartY.value;
|
|
1028
|
+
const wrapperStartDU_X = flowStartX.value + stickyStartX.value;
|
|
1029
|
+
const wrapperStartDU_Y = flowStartY.value + stickyStartY.value;
|
|
1030
|
+
|
|
1031
|
+
const colRange = columnRange.value;
|
|
1032
|
+
|
|
808
1033
|
for (const i of sortedIndices) {
|
|
809
1034
|
const item = props.value.items[ i ];
|
|
810
1035
|
if (item === undefined) {
|
|
@@ -813,27 +1038,28 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
813
1038
|
|
|
814
1039
|
const { x, y, width, height } = calculateItemPosition({
|
|
815
1040
|
index: i,
|
|
816
|
-
direction:
|
|
1041
|
+
direction: direction.value,
|
|
817
1042
|
fixedSize: fixedItemSize.value,
|
|
818
1043
|
gap: props.value.gap || 0,
|
|
819
1044
|
columnGap: props.value.columnGap || 0,
|
|
820
1045
|
usableWidth: usableWidth.value,
|
|
821
1046
|
usableHeight: usableHeight.value,
|
|
822
|
-
totalWidth:
|
|
823
|
-
queryY:
|
|
824
|
-
queryX:
|
|
1047
|
+
totalWidth: totalSize.value.width,
|
|
1048
|
+
queryY: queryYCached,
|
|
1049
|
+
queryX: queryXCached,
|
|
825
1050
|
getSizeY: (idx) => itemSizesY.get(idx),
|
|
826
1051
|
getSizeX: (idx) => itemSizesX.get(idx),
|
|
1052
|
+
columnRange: colRange,
|
|
827
1053
|
});
|
|
828
1054
|
|
|
829
1055
|
const isSticky = stickySet.has(i);
|
|
830
1056
|
const originalX = x;
|
|
831
1057
|
const originalY = y;
|
|
832
1058
|
|
|
833
|
-
const { isStickyActive, stickyOffset } = calculateStickyItem({
|
|
1059
|
+
const { isStickyActive, isStickyActiveX, isStickyActiveY, stickyOffset } = calculateStickyItem({
|
|
834
1060
|
index: i,
|
|
835
1061
|
isSticky,
|
|
836
|
-
direction:
|
|
1062
|
+
direction: direction.value,
|
|
837
1063
|
relativeScrollX: relativeScrollX.value,
|
|
838
1064
|
relativeScrollY: relativeScrollY.value,
|
|
839
1065
|
originalX,
|
|
@@ -845,14 +1071,18 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
845
1071
|
fixedWidth: fixedColumnWidth.value,
|
|
846
1072
|
gap: props.value.gap || 0,
|
|
847
1073
|
columnGap: props.value.columnGap || 0,
|
|
848
|
-
getItemQueryY:
|
|
849
|
-
getItemQueryX:
|
|
1074
|
+
getItemQueryY: queryYCached,
|
|
1075
|
+
getItemQueryX: queryXCached,
|
|
850
1076
|
});
|
|
851
1077
|
|
|
852
|
-
const offsetX =
|
|
853
|
-
|
|
854
|
-
|
|
1078
|
+
const offsetX = isHydrated.value
|
|
1079
|
+
? (internalScrollX.value / scaleX.value + (x + itemsStartVU_X - internalScrollX.value)) - wrapperStartDU_X
|
|
1080
|
+
: (x - ssrOffsetX);
|
|
1081
|
+
const offsetY = isHydrated.value
|
|
1082
|
+
? (internalScrollY.value / scaleY.value + (y + itemsStartVU_Y - internalScrollY.value)) - wrapperStartDU_Y
|
|
1083
|
+
: (y - ssrOffsetY);
|
|
855
1084
|
|
|
1085
|
+
const last = lastItemsMap.get(i);
|
|
856
1086
|
if (
|
|
857
1087
|
last
|
|
858
1088
|
&& last.item === item
|
|
@@ -862,6 +1092,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
862
1092
|
&& last.size.height === height
|
|
863
1093
|
&& last.isSticky === isSticky
|
|
864
1094
|
&& last.isStickyActive === isStickyActive
|
|
1095
|
+
&& last.isStickyActiveX === isStickyActiveX
|
|
1096
|
+
&& last.isStickyActiveY === isStickyActiveY
|
|
865
1097
|
&& last.stickyOffset.x === stickyOffset.x
|
|
866
1098
|
&& last.stickyOffset.y === stickyOffset.y
|
|
867
1099
|
) {
|
|
@@ -876,6 +1108,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
876
1108
|
originalY,
|
|
877
1109
|
isSticky,
|
|
878
1110
|
isStickyActive,
|
|
1111
|
+
isStickyActiveX,
|
|
1112
|
+
isStickyActiveY,
|
|
879
1113
|
stickyOffset,
|
|
880
1114
|
});
|
|
881
1115
|
}
|
|
@@ -886,71 +1120,47 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
886
1120
|
return items;
|
|
887
1121
|
});
|
|
888
1122
|
|
|
889
|
-
const columnRange = computed(() => {
|
|
890
|
-
// eslint-disable-next-line ts/no-unused-expressions
|
|
891
|
-
treeUpdateFlag.value;
|
|
892
|
-
|
|
893
|
-
const totalCols = props.value.columnCount || 0;
|
|
894
|
-
|
|
895
|
-
if (!totalCols) {
|
|
896
|
-
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if ((!isHydrated.value || isHydrating.value) && props.value.ssrRange) {
|
|
900
|
-
const { colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
901
|
-
const safeStart = Math.max(0, colStart);
|
|
902
|
-
const safeEnd = Math.min(totalCols, colEnd || totalCols);
|
|
903
|
-
return {
|
|
904
|
-
start: safeStart,
|
|
905
|
-
end: safeEnd,
|
|
906
|
-
padStart: 0,
|
|
907
|
-
padEnd: 0,
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
912
|
-
|
|
913
|
-
return calculateColumnRange({
|
|
914
|
-
columnCount: totalCols,
|
|
915
|
-
relativeScrollX: relativeScrollX.value,
|
|
916
|
-
usableWidth: usableWidth.value,
|
|
917
|
-
colBuffer,
|
|
918
|
-
fixedWidth: fixedColumnWidth.value,
|
|
919
|
-
columnGap: props.value.columnGap || 0,
|
|
920
|
-
findLowerBound: (offset) => columnSizes.findLowerBound(offset),
|
|
921
|
-
query: (idx) => columnSizes.query(idx),
|
|
922
|
-
totalColsQuery: () => columnSizes.query(totalCols),
|
|
923
|
-
});
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Detailed information about the current scroll state.
|
|
928
|
-
*/
|
|
929
1123
|
const scrollDetails = computed<ScrollDetails<T>>(() => {
|
|
930
1124
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
931
1125
|
treeUpdateFlag.value;
|
|
932
1126
|
|
|
933
|
-
const
|
|
934
|
-
const
|
|
1127
|
+
const currentScrollX = relativeScrollX.value + stickyStartX.value;
|
|
1128
|
+
const currentScrollY = relativeScrollY.value + stickyStartY.value;
|
|
935
1129
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
} else if (props.value.direction === 'both') {
|
|
944
|
-
currentColIndex = columnSizes.findLowerBound(relativeScrollX.value);
|
|
945
|
-
}
|
|
1130
|
+
const currentEndScrollX = relativeScrollX.value + (viewportWidth.value - stickyEndX.value) - 1;
|
|
1131
|
+
const currentEndScrollY = relativeScrollY.value + (viewportHeight.value - stickyEndY.value) - 1;
|
|
1132
|
+
|
|
1133
|
+
const currentColIndex = getColIndexAt(currentScrollX);
|
|
1134
|
+
const currentRowIndex = getRowIndexAt(currentScrollY);
|
|
1135
|
+
const currentEndIndex = getRowIndexAt(direction.value === 'horizontal' ? currentEndScrollX : currentEndScrollY);
|
|
1136
|
+
const currentEndColIndex = getColIndexAt(currentEndScrollX);
|
|
946
1137
|
|
|
947
1138
|
return {
|
|
948
1139
|
items: renderedItems.value,
|
|
949
|
-
currentIndex:
|
|
1140
|
+
currentIndex: currentRowIndex,
|
|
950
1141
|
currentColIndex,
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1142
|
+
currentEndIndex,
|
|
1143
|
+
currentEndColIndex,
|
|
1144
|
+
scrollOffset: {
|
|
1145
|
+
x: internalScrollX.value,
|
|
1146
|
+
y: internalScrollY.value,
|
|
1147
|
+
},
|
|
1148
|
+
displayScrollOffset: {
|
|
1149
|
+
x: isRtl.value ? Math.abs(scrollX.value + hostRefOffset.x) : Math.max(0, scrollX.value - hostRefOffset.x),
|
|
1150
|
+
y: Math.max(0, scrollY.value - hostRefOffset.y),
|
|
1151
|
+
},
|
|
1152
|
+
viewportSize: {
|
|
1153
|
+
width: viewportWidth.value,
|
|
1154
|
+
height: viewportHeight.value,
|
|
1155
|
+
},
|
|
1156
|
+
displayViewportSize: {
|
|
1157
|
+
width: viewportWidth.value,
|
|
1158
|
+
height: viewportHeight.value,
|
|
1159
|
+
},
|
|
1160
|
+
totalSize: {
|
|
1161
|
+
width: totalWidth.value,
|
|
1162
|
+
height: totalHeight.value,
|
|
1163
|
+
},
|
|
954
1164
|
isScrolling: isScrolling.value,
|
|
955
1165
|
isProgrammaticScroll: isProgrammaticScroll.value,
|
|
956
1166
|
range: range.value,
|
|
@@ -976,6 +1186,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
976
1186
|
return;
|
|
977
1187
|
}
|
|
978
1188
|
|
|
1189
|
+
updateDirection();
|
|
1190
|
+
|
|
979
1191
|
if (target === window || target === document) {
|
|
980
1192
|
scrollX.value = window.scrollX;
|
|
981
1193
|
scrollY.value = window.scrollY;
|
|
@@ -988,6 +1200,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
988
1200
|
viewportHeight.value = target.clientHeight;
|
|
989
1201
|
}
|
|
990
1202
|
|
|
1203
|
+
const scrollValueX = isRtl.value ? Math.abs(scrollX.value) : scrollX.value;
|
|
1204
|
+
internalScrollX.value = displayToVirtual(scrollValueX, componentOffset.x, scaleX.value);
|
|
1205
|
+
internalScrollY.value = displayToVirtual(scrollY.value, componentOffset.y, scaleY.value);
|
|
1206
|
+
|
|
991
1207
|
if (!isScrolling.value) {
|
|
992
1208
|
if (!isProgrammaticScroll.value) {
|
|
993
1209
|
pendingScroll.value = null;
|
|
@@ -1015,20 +1231,36 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1015
1231
|
|
|
1016
1232
|
const currentRelX = relativeScrollX.value;
|
|
1017
1233
|
const currentRelY = relativeScrollY.value;
|
|
1018
|
-
const firstRowIndex = props.value.direction === 'horizontal'
|
|
1019
|
-
? (fixedItemSize.value !== null ? Math.floor(currentRelX / (fixedItemSize.value + columnGap)) : itemSizesX.findLowerBound(currentRelX))
|
|
1020
|
-
: (fixedItemSize.value !== null ? Math.floor(currentRelY / (fixedItemSize.value + gap)) : itemSizesY.findLowerBound(currentRelY));
|
|
1021
|
-
const firstColIndex = props.value.direction === 'both'
|
|
1022
|
-
? columnSizes.findLowerBound(currentRelX)
|
|
1023
|
-
: (props.value.direction === 'horizontal' ? firstRowIndex : 0);
|
|
1024
1234
|
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1235
|
+
const firstRowIndex = getRowIndexAt(direction.value === 'horizontal' ? currentRelX : currentRelY);
|
|
1236
|
+
const firstColIndex = getColIndexAt(currentRelX);
|
|
1237
|
+
|
|
1238
|
+
const isHorizontalMode = direction.value === 'horizontal';
|
|
1239
|
+
const isBothMode = direction.value === 'both';
|
|
1028
1240
|
|
|
1029
1241
|
const processedRows = new Set<number>();
|
|
1030
1242
|
const processedCols = new Set<number>();
|
|
1031
1243
|
|
|
1244
|
+
const tryUpdateColumn = (colIdx: number, width: number) => {
|
|
1245
|
+
if (colIdx >= 0 && colIdx < (props.value.columnCount || 0) && !processedCols.has(colIdx)) {
|
|
1246
|
+
processedCols.add(colIdx);
|
|
1247
|
+
const oldW = columnSizes.get(colIdx);
|
|
1248
|
+
const targetW = width + columnGap;
|
|
1249
|
+
|
|
1250
|
+
if (!measuredColumns[ colIdx ] || Math.abs(oldW - targetW) > 0.1) {
|
|
1251
|
+
const d = targetW - oldW;
|
|
1252
|
+
if (Math.abs(d) > 0.1) {
|
|
1253
|
+
columnSizes.update(colIdx, d);
|
|
1254
|
+
needUpdate = true;
|
|
1255
|
+
if (colIdx < firstColIndex) {
|
|
1256
|
+
deltaX += d;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
measuredColumns[ colIdx ] = 1;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1032
1264
|
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1033
1265
|
// Ignore 0-size measurements as they usually indicate hidden/detached elements
|
|
1034
1266
|
if (inlineSize <= 0 && blockSize <= 0) {
|
|
@@ -1051,7 +1283,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1051
1283
|
}
|
|
1052
1284
|
}
|
|
1053
1285
|
}
|
|
1054
|
-
if (
|
|
1286
|
+
if (!isHorizontalMode) {
|
|
1055
1287
|
const oldHeight = itemSizesY.get(index);
|
|
1056
1288
|
const targetHeight = blockSize + gap;
|
|
1057
1289
|
|
|
@@ -1078,51 +1310,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1078
1310
|
) {
|
|
1079
1311
|
const colIndexAttr = element.dataset.colIndex;
|
|
1080
1312
|
if (colIndexAttr != null) {
|
|
1081
|
-
|
|
1082
|
-
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0) && !processedCols.has(colIndex)) {
|
|
1083
|
-
processedCols.add(colIndex);
|
|
1084
|
-
const oldW = columnSizes.get(colIndex);
|
|
1085
|
-
const targetW = inlineSize + columnGap;
|
|
1086
|
-
|
|
1087
|
-
if (!measuredColumns[ colIndex ] || Math.abs(oldW - targetW) > 0.1) {
|
|
1088
|
-
const d = targetW - oldW;
|
|
1089
|
-
if (Math.abs(d) > 0.1) {
|
|
1090
|
-
columnSizes.update(colIndex, d);
|
|
1091
|
-
needUpdate = true;
|
|
1092
|
-
if (colIndex < firstColIndex) {
|
|
1093
|
-
deltaX += d;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
measuredColumns[ colIndex ] = 1;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1313
|
+
tryUpdateColumn(Number.parseInt(colIndexAttr, 10), inlineSize);
|
|
1099
1314
|
} else {
|
|
1100
1315
|
// If the element is a row, try to find cells with data-col-index
|
|
1101
|
-
const cells = element.
|
|
1102
|
-
? [ element ]
|
|
1103
|
-
: Array.from(element.querySelectorAll('[data-col-index]')) as HTMLElement[];
|
|
1316
|
+
const cells = Array.from(element.querySelectorAll('[data-col-index]')) as HTMLElement[];
|
|
1104
1317
|
|
|
1105
1318
|
for (const child of cells) {
|
|
1106
1319
|
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1107
|
-
|
|
1108
|
-
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0) && !processedCols.has(colIndex)) {
|
|
1109
|
-
processedCols.add(colIndex);
|
|
1110
|
-
const rect = child.getBoundingClientRect();
|
|
1111
|
-
const w = rect.width;
|
|
1112
|
-
const oldW = columnSizes.get(colIndex);
|
|
1113
|
-
const targetW = w + columnGap;
|
|
1114
|
-
if (!measuredColumns[ colIndex ] || Math.abs(oldW - targetW) > 0.1) {
|
|
1115
|
-
const d = targetW - oldW;
|
|
1116
|
-
if (Math.abs(d) > 0.1) {
|
|
1117
|
-
columnSizes.update(colIndex, d);
|
|
1118
|
-
needUpdate = true;
|
|
1119
|
-
if (colIndex < firstColIndex) {
|
|
1120
|
-
deltaX += d;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
measuredColumns[ colIndex ] = 1;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1320
|
+
tryUpdateColumn(colIndex, child.getBoundingClientRect().width);
|
|
1126
1321
|
}
|
|
1127
1322
|
}
|
|
1128
1323
|
}
|
|
@@ -1135,9 +1330,11 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1135
1330
|
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1136
1331
|
|
|
1137
1332
|
if (!hasPendingScroll && (deltaX !== 0 || deltaY !== 0)) {
|
|
1333
|
+
const contentStartLogicalX = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
1334
|
+
const contentStartLogicalY = flowStartY.value + stickyStartY.value + paddingStartY.value;
|
|
1138
1335
|
scrollToOffset(
|
|
1139
|
-
deltaX !== 0 ? currentRelX + deltaX : null,
|
|
1140
|
-
deltaY !== 0 ? currentRelY + deltaY : null,
|
|
1336
|
+
deltaX !== 0 ? currentRelX + deltaX + contentStartLogicalX : null,
|
|
1337
|
+
deltaY !== 0 ? currentRelY + deltaY + contentStartLogicalY : null,
|
|
1141
1338
|
{ behavior: 'auto' },
|
|
1142
1339
|
);
|
|
1143
1340
|
}
|
|
@@ -1157,7 +1354,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1157
1354
|
};
|
|
1158
1355
|
|
|
1159
1356
|
// --- Scroll Queue / Correction Watchers ---
|
|
1160
|
-
|
|
1357
|
+
function checkPendingScroll() {
|
|
1161
1358
|
if (pendingScroll.value && !isHydrating.value) {
|
|
1162
1359
|
const { rowIndex, colIndex, options } = pendingScroll.value;
|
|
1163
1360
|
|
|
@@ -1168,41 +1365,64 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1168
1365
|
return;
|
|
1169
1366
|
}
|
|
1170
1367
|
|
|
1368
|
+
const container = props.value.container || window;
|
|
1369
|
+
const actualScrollX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
1370
|
+
const actualScrollY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
1371
|
+
|
|
1372
|
+
const scrollValueX = isRtl.value ? Math.abs(actualScrollX) : actualScrollX;
|
|
1373
|
+
const scrollValueY = actualScrollY;
|
|
1374
|
+
|
|
1375
|
+
const currentRelX = displayToVirtual(scrollValueX, 0, scaleX.value);
|
|
1376
|
+
const currentRelY = displayToVirtual(scrollValueY, 0, scaleY.value);
|
|
1377
|
+
|
|
1171
1378
|
const { targetX, targetY } = calculateScrollTarget({
|
|
1172
1379
|
rowIndex,
|
|
1173
1380
|
colIndex,
|
|
1174
1381
|
options,
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
totalWidth: totalWidth.value,
|
|
1181
|
-
totalHeight: totalHeight.value,
|
|
1382
|
+
direction: direction.value,
|
|
1383
|
+
viewportWidth: viewportWidth.value,
|
|
1384
|
+
viewportHeight: viewportHeight.value,
|
|
1385
|
+
totalWidth: virtualWidth.value,
|
|
1386
|
+
totalHeight: virtualHeight.value,
|
|
1182
1387
|
gap: props.value.gap || 0,
|
|
1183
1388
|
columnGap: props.value.columnGap || 0,
|
|
1184
1389
|
fixedSize: fixedItemSize.value,
|
|
1185
1390
|
fixedWidth: fixedColumnWidth.value,
|
|
1186
|
-
relativeScrollX:
|
|
1187
|
-
relativeScrollY:
|
|
1391
|
+
relativeScrollX: currentRelX,
|
|
1392
|
+
relativeScrollY: currentRelY,
|
|
1188
1393
|
getItemSizeY: (idx) => itemSizesY.get(idx),
|
|
1189
1394
|
getItemSizeX: (idx) => itemSizesX.get(idx),
|
|
1190
1395
|
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
1191
1396
|
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
1192
1397
|
getColumnSize: (idx) => columnSizes.get(idx),
|
|
1193
1398
|
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
1399
|
+
scaleX: scaleX.value,
|
|
1400
|
+
scaleY: scaleY.value,
|
|
1401
|
+
hostOffsetX: componentOffset.x,
|
|
1402
|
+
hostOffsetY: componentOffset.y,
|
|
1194
1403
|
stickyIndices: sortedStickyIndices.value,
|
|
1404
|
+
stickyStartX: stickyStartX.value,
|
|
1405
|
+
stickyStartY: stickyStartY.value,
|
|
1406
|
+
stickyEndX: stickyEndX.value,
|
|
1407
|
+
stickyEndY: stickyEndY.value,
|
|
1408
|
+
flowPaddingStartX: flowStartX.value,
|
|
1409
|
+
flowPaddingStartY: flowStartY.value,
|
|
1410
|
+
paddingStartX: paddingStartX.value,
|
|
1411
|
+
paddingStartY: paddingStartY.value,
|
|
1412
|
+
paddingEndX: paddingEndX.value,
|
|
1413
|
+
paddingEndY: paddingEndY.value,
|
|
1195
1414
|
});
|
|
1196
1415
|
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1416
|
+
const toleranceX = 2;
|
|
1417
|
+
const toleranceY = 2;
|
|
1418
|
+
const reachedX = (colIndex === null || colIndex === undefined) || Math.abs(currentRelX - targetX) < toleranceX;
|
|
1419
|
+
const reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(currentRelY - targetY) < toleranceY;
|
|
1200
1420
|
|
|
1201
1421
|
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns[ colIndex ] === 1;
|
|
1202
1422
|
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY[ rowIndex ] === 1;
|
|
1203
1423
|
|
|
1204
1424
|
if (reachedX && reachedY) {
|
|
1205
|
-
if (isMeasuredX && isMeasuredY) {
|
|
1425
|
+
if (isMeasuredX && isMeasuredY && !isScrolling.value && !isProgrammaticScroll.value) {
|
|
1206
1426
|
pendingScroll.value = null;
|
|
1207
1427
|
}
|
|
1208
1428
|
} else {
|
|
@@ -1212,7 +1432,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1212
1432
|
scrollToIndex(rowIndex, colIndex, correctionOptions);
|
|
1213
1433
|
}
|
|
1214
1434
|
}
|
|
1215
|
-
}
|
|
1435
|
+
}
|
|
1216
1436
|
|
|
1217
1437
|
watch([ treeUpdateFlag, viewportWidth, viewportHeight ], checkPendingScroll);
|
|
1218
1438
|
|
|
@@ -1223,49 +1443,69 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1223
1443
|
});
|
|
1224
1444
|
|
|
1225
1445
|
let resizeObserver: ResizeObserver | null = null;
|
|
1446
|
+
let directionObserver: MutationObserver | null = null;
|
|
1447
|
+
let directionInterval: ReturnType<typeof setInterval> | undefined;
|
|
1226
1448
|
|
|
1227
1449
|
const attachEvents = (container: HTMLElement | Window | null) => {
|
|
1228
|
-
if (
|
|
1450
|
+
if (typeof window === 'undefined') {
|
|
1229
1451
|
return;
|
|
1230
1452
|
}
|
|
1231
|
-
const
|
|
1453
|
+
const effectiveContainer = container || window;
|
|
1454
|
+
const scrollTarget = (effectiveContainer === window || (isElement(effectiveContainer) && effectiveContainer === document.documentElement)) ? document : effectiveContainer;
|
|
1455
|
+
|
|
1232
1456
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
1233
1457
|
|
|
1234
|
-
|
|
1458
|
+
computedStyle = null;
|
|
1459
|
+
updateDirection();
|
|
1460
|
+
|
|
1461
|
+
if (isElement(effectiveContainer)) {
|
|
1462
|
+
directionObserver = new MutationObserver(() => updateDirection());
|
|
1463
|
+
directionObserver.observe(effectiveContainer, { attributes: true, attributeFilter: [ 'dir', 'style' ] });
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
directionInterval = setInterval(updateDirection, 1000);
|
|
1467
|
+
|
|
1468
|
+
if (effectiveContainer === window) {
|
|
1235
1469
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
1236
1470
|
viewportHeight.value = document.documentElement.clientHeight;
|
|
1237
1471
|
scrollX.value = window.scrollX;
|
|
1238
1472
|
scrollY.value = window.scrollY;
|
|
1239
1473
|
|
|
1240
1474
|
const onResize = () => {
|
|
1475
|
+
updateDirection();
|
|
1241
1476
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
1242
1477
|
viewportHeight.value = document.documentElement.clientHeight;
|
|
1243
1478
|
updateHostOffset();
|
|
1244
1479
|
};
|
|
1245
1480
|
window.addEventListener('resize', onResize);
|
|
1481
|
+
|
|
1246
1482
|
return () => {
|
|
1247
1483
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1248
1484
|
window.removeEventListener('resize', onResize);
|
|
1485
|
+
directionObserver?.disconnect();
|
|
1486
|
+
clearInterval(directionInterval);
|
|
1487
|
+
computedStyle = null;
|
|
1249
1488
|
};
|
|
1250
1489
|
} else {
|
|
1251
|
-
viewportWidth.value = (
|
|
1252
|
-
viewportHeight.value = (
|
|
1253
|
-
scrollX.value = (
|
|
1254
|
-
scrollY.value = (
|
|
1255
|
-
|
|
1256
|
-
resizeObserver = new ResizeObserver((
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
updateHostOffset();
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1490
|
+
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1491
|
+
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1492
|
+
scrollX.value = (effectiveContainer as HTMLElement).scrollLeft;
|
|
1493
|
+
scrollY.value = (effectiveContainer as HTMLElement).scrollTop;
|
|
1494
|
+
|
|
1495
|
+
resizeObserver = new ResizeObserver(() => {
|
|
1496
|
+
updateDirection();
|
|
1497
|
+
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1498
|
+
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1499
|
+
updateHostOffset();
|
|
1264
1500
|
});
|
|
1265
|
-
resizeObserver.observe(
|
|
1501
|
+
resizeObserver.observe(effectiveContainer as HTMLElement);
|
|
1502
|
+
|
|
1266
1503
|
return () => {
|
|
1267
1504
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1268
1505
|
resizeObserver?.disconnect();
|
|
1506
|
+
directionObserver?.disconnect();
|
|
1507
|
+
clearInterval(directionInterval);
|
|
1508
|
+
computedStyle = null;
|
|
1269
1509
|
};
|
|
1270
1510
|
}
|
|
1271
1511
|
};
|
|
@@ -1275,6 +1515,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1275
1515
|
if (getCurrentInstance()) {
|
|
1276
1516
|
onMounted(() => {
|
|
1277
1517
|
isMounted.value = true;
|
|
1518
|
+
updateDirection();
|
|
1278
1519
|
|
|
1279
1520
|
watch(() => props.value.container, (newContainer) => {
|
|
1280
1521
|
cleanup?.();
|
|
@@ -1283,9 +1524,11 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1283
1524
|
|
|
1284
1525
|
updateHostOffset();
|
|
1285
1526
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1527
|
+
// Ensure we have a layout cycle before considering it hydrated
|
|
1528
|
+
// and starting virtualization. This avoids issues with 0-size viewports.
|
|
1529
|
+
nextTick(() => {
|
|
1530
|
+
updateHostOffset();
|
|
1531
|
+
if (props.value.ssrRange || props.value.initialScrollIndex !== undefined) {
|
|
1289
1532
|
const initialIndex = props.value.initialScrollIndex !== undefined
|
|
1290
1533
|
? props.value.initialScrollIndex
|
|
1291
1534
|
: props.value.ssrRange?.start;
|
|
@@ -1300,10 +1543,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1300
1543
|
nextTick(() => {
|
|
1301
1544
|
isHydrating.value = false;
|
|
1302
1545
|
});
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1546
|
+
} else {
|
|
1547
|
+
isHydrated.value = true;
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1307
1550
|
});
|
|
1308
1551
|
|
|
1309
1552
|
onUnmounted(() => {
|
|
@@ -1314,6 +1557,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1314
1557
|
/**
|
|
1315
1558
|
* The list of items currently rendered in the DOM.
|
|
1316
1559
|
*/
|
|
1560
|
+
/**
|
|
1561
|
+
* Resets all dynamic measurements and re-initializes from current props.
|
|
1562
|
+
* Useful if item source data has changed in a way that affects sizes without changing the items array reference.
|
|
1563
|
+
*/
|
|
1317
1564
|
const refresh = () => {
|
|
1318
1565
|
itemSizesX.resize(0);
|
|
1319
1566
|
itemSizesY.resize(0);
|
|
@@ -1327,27 +1574,99 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1327
1574
|
return {
|
|
1328
1575
|
/**
|
|
1329
1576
|
* Array of items currently rendered in the DOM with their calculated offsets and sizes.
|
|
1577
|
+
* Offsets are in Display Units (DU), sizes are in Virtual Units (VU).
|
|
1330
1578
|
* @see RenderedItem
|
|
1331
1579
|
*/
|
|
1332
1580
|
renderedItems,
|
|
1333
1581
|
|
|
1334
1582
|
/**
|
|
1335
|
-
* Total calculated width of all items including gaps (in
|
|
1583
|
+
* Total calculated width of all items including gaps (in VU).
|
|
1336
1584
|
*/
|
|
1337
1585
|
totalWidth,
|
|
1338
1586
|
|
|
1339
1587
|
/**
|
|
1340
|
-
* Total calculated height of all items including gaps (in
|
|
1588
|
+
* Total calculated height of all items including gaps (in VU).
|
|
1341
1589
|
*/
|
|
1342
1590
|
totalHeight,
|
|
1343
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Total width to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1594
|
+
*/
|
|
1595
|
+
renderedWidth,
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Total height to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1599
|
+
*/
|
|
1600
|
+
renderedHeight,
|
|
1601
|
+
|
|
1344
1602
|
/**
|
|
1345
1603
|
* Detailed information about the current scroll state.
|
|
1346
|
-
* Includes currentIndex, scrollOffset, viewportSize, totalSize, and scrolling status.
|
|
1604
|
+
* Includes currentIndex, scrollOffset (VU), displayScrollOffset (DU), viewportSize (DU), totalSize (VU), and scrolling status.
|
|
1347
1605
|
* @see ScrollDetails
|
|
1348
1606
|
*/
|
|
1349
1607
|
scrollDetails,
|
|
1350
1608
|
|
|
1609
|
+
/**
|
|
1610
|
+
* Helper to get the height of a specific row based on current configuration and measurements.
|
|
1611
|
+
*
|
|
1612
|
+
* @param index - The row index.
|
|
1613
|
+
* @returns The height in VU (excluding gap).
|
|
1614
|
+
*/
|
|
1615
|
+
getRowHeight,
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Helper to get the width of a specific column based on current configuration and measurements.
|
|
1619
|
+
*
|
|
1620
|
+
* @param index - The column index.
|
|
1621
|
+
* @returns The width in VU (excluding gap).
|
|
1622
|
+
*/
|
|
1623
|
+
getColumnWidth,
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Helper to get the virtual offset of a specific row.
|
|
1627
|
+
*
|
|
1628
|
+
* @param index - The row index.
|
|
1629
|
+
* @returns The virtual offset in VU.
|
|
1630
|
+
*/
|
|
1631
|
+
getRowOffset: (index: number) => (flowStartY.value + stickyStartY.value + paddingStartY.value) + itemSizesY.query(index),
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Helper to get the virtual offset of a specific column.
|
|
1635
|
+
*
|
|
1636
|
+
* @param index - The column index.
|
|
1637
|
+
* @returns The virtual offset in VU.
|
|
1638
|
+
*/
|
|
1639
|
+
getColumnOffset: (index: number) => (flowStartX.value + stickyStartX.value + paddingStartX.value) + columnSizes.query(index),
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Helper to get the virtual offset of a specific item along the scroll axis.
|
|
1643
|
+
*
|
|
1644
|
+
* @param index - The item index.
|
|
1645
|
+
* @returns The virtual offset in VU.
|
|
1646
|
+
*/
|
|
1647
|
+
getItemOffset: (index: number) => (direction.value === 'horizontal' ? (flowStartX.value + stickyStartX.value + paddingStartX.value) + itemSizesX.query(index) : (flowStartY.value + stickyStartY.value + paddingStartY.value) + itemSizesY.query(index)),
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Helper to get the size of a specific item along the scroll axis.
|
|
1651
|
+
*
|
|
1652
|
+
* @param index - The item index.
|
|
1653
|
+
* @returns The size in VU (excluding gap).
|
|
1654
|
+
*/
|
|
1655
|
+
getItemSize: (index: number) => {
|
|
1656
|
+
if (direction.value === 'horizontal') {
|
|
1657
|
+
return Math.max(0, itemSizesX.get(index) - (props.value.columnGap || 0));
|
|
1658
|
+
}
|
|
1659
|
+
const itemSize = props.value.itemSize;
|
|
1660
|
+
if (typeof itemSize === 'number' && itemSize > 0) {
|
|
1661
|
+
return itemSize;
|
|
1662
|
+
}
|
|
1663
|
+
if (typeof itemSize === 'function') {
|
|
1664
|
+
const item = props.value.items[ index ];
|
|
1665
|
+
return item !== undefined ? itemSize(item, index) : (props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
|
|
1666
|
+
}
|
|
1667
|
+
return Math.max(0, itemSizesY.get(index) - (props.value.gap || 0));
|
|
1668
|
+
},
|
|
1669
|
+
|
|
1351
1670
|
/**
|
|
1352
1671
|
* Programmatically scroll to a specific row and/or column.
|
|
1353
1672
|
*
|
|
@@ -1362,8 +1681,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1362
1681
|
/**
|
|
1363
1682
|
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
1364
1683
|
*
|
|
1365
|
-
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
1366
|
-
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
1684
|
+
* @param x - The pixel offset to scroll to on the X axis (VU). Pass null to keep current position.
|
|
1685
|
+
* @param y - The pixel offset to scroll to on the Y axis (VU). Pass null to keep current position.
|
|
1367
1686
|
* @param options - Scroll options (behavior).
|
|
1368
1687
|
*/
|
|
1369
1688
|
scrollToOffset,
|
|
@@ -1377,8 +1696,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1377
1696
|
* Updates the stored size of an item. Should be called when an item is measured (e.g., via ResizeObserver).
|
|
1378
1697
|
*
|
|
1379
1698
|
* @param index - The item index.
|
|
1380
|
-
* @param width - The measured inlineSize (width).
|
|
1381
|
-
* @param height - The measured blockSize (height).
|
|
1699
|
+
* @param width - The measured inlineSize (width in DU).
|
|
1700
|
+
* @param height - The measured blockSize (height in DU).
|
|
1382
1701
|
* @param element - The measured element (optional, used for robust grid column detection).
|
|
1383
1702
|
*/
|
|
1384
1703
|
updateItemSize,
|
|
@@ -1386,7 +1705,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1386
1705
|
/**
|
|
1387
1706
|
* Updates the stored size of multiple items simultaneously.
|
|
1388
1707
|
*
|
|
1389
|
-
* @param updates - Array of measurement updates.
|
|
1708
|
+
* @param updates - Array of measurement updates (sizes in DU).
|
|
1390
1709
|
*/
|
|
1391
1710
|
updateItemSizes,
|
|
1392
1711
|
|
|
@@ -1397,17 +1716,15 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1397
1716
|
updateHostOffset,
|
|
1398
1717
|
|
|
1399
1718
|
/**
|
|
1400
|
-
*
|
|
1401
|
-
* @see ColumnRange
|
|
1719
|
+
* Detects the current direction (LTR/RTL) of the scroll container.
|
|
1402
1720
|
*/
|
|
1403
|
-
|
|
1721
|
+
updateDirection,
|
|
1404
1722
|
|
|
1405
1723
|
/**
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
* @param index - The column index.
|
|
1724
|
+
* Information about the current visible range of columns and their paddings.
|
|
1725
|
+
* @see ColumnRange
|
|
1409
1726
|
*/
|
|
1410
|
-
|
|
1727
|
+
columnRange,
|
|
1411
1728
|
|
|
1412
1729
|
/**
|
|
1413
1730
|
* Resets all dynamic measurements and re-initializes from props.
|
|
@@ -1419,5 +1736,40 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1419
1736
|
* Whether the component has finished its first client-side mount and hydration.
|
|
1420
1737
|
*/
|
|
1421
1738
|
isHydrated,
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Whether the container is the window or body.
|
|
1742
|
+
*/
|
|
1743
|
+
isWindowContainer,
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Whether the scroll container is in Right-to-Left (RTL) mode.
|
|
1747
|
+
*/
|
|
1748
|
+
isRtl,
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Coordinate scaling factor for X axis (VU/DU).
|
|
1752
|
+
*/
|
|
1753
|
+
scaleX,
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Coordinate scaling factor for Y axis (VU/DU).
|
|
1757
|
+
*/
|
|
1758
|
+
scaleY,
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Absolute offset of the component within its container (DU).
|
|
1762
|
+
*/
|
|
1763
|
+
componentOffset,
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Physical width of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1767
|
+
*/
|
|
1768
|
+
renderedVirtualWidth,
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* Physical height of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1772
|
+
*/
|
|
1773
|
+
renderedVirtualHeight,
|
|
1422
1774
|
};
|
|
1423
1775
|
}
|