@pdanpdan/virtual-scroll 0.9.1 → 0.10.1
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 +82 -4
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +192 -156
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +862 -695
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +1 -1
- package/src/components/VirtualScroll.vue +30 -20
- package/src/composables/useVirtualScroll.ts +394 -813
- package/src/composables/useVirtualScrollSizes.ts +28 -37
- package/src/composables/useVirtualScrollbar.ts +16 -0
- package/src/extensions/all.ts +7 -0
- package/src/extensions/coordinate-scaling.ts +30 -0
- package/src/extensions/index.ts +88 -0
- package/src/extensions/infinite-loading.ts +47 -0
- package/src/extensions/prepend-restoration.ts +49 -0
- package/src/extensions/rtl.ts +42 -0
- package/src/extensions/snapping.ts +95 -0
- package/src/extensions/sticky.ts +43 -0
- package/src/types.ts +47 -8
- package/src/utils/scroll.ts +2 -2
- package/src/utils/virtual-scroll-logic.ts +42 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExtensionContext, VirtualScrollExtension } from '../extensions';
|
|
1
2
|
import type {
|
|
2
3
|
RenderedItem,
|
|
3
4
|
ScrollAlignment,
|
|
@@ -5,11 +6,12 @@ import type {
|
|
|
5
6
|
ScrollDetails,
|
|
6
7
|
ScrollDirection,
|
|
7
8
|
ScrollToIndexOptions,
|
|
9
|
+
ScrollToIndexResult,
|
|
8
10
|
VirtualScrollProps,
|
|
9
11
|
} from '../types';
|
|
10
|
-
import type { MaybeRefOrGetter } from 'vue';
|
|
11
|
-
|
|
12
12
|
/* global ScrollToOptions */
|
|
13
|
+
import type { Ref } from 'vue';
|
|
14
|
+
|
|
13
15
|
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, toValue, watch } from 'vue';
|
|
14
16
|
|
|
15
17
|
import {
|
|
@@ -26,14 +28,12 @@ import {
|
|
|
26
28
|
calculateRange,
|
|
27
29
|
calculateRangeSize,
|
|
28
30
|
calculateRenderedSize,
|
|
29
|
-
calculateScale,
|
|
30
31
|
calculateScrollTarget,
|
|
31
32
|
calculateSSROffsets,
|
|
32
33
|
calculateStickyItem,
|
|
33
34
|
calculateTotalSize,
|
|
34
35
|
displayToVirtual,
|
|
35
36
|
findPrevStickyIndex,
|
|
36
|
-
resolveSnap,
|
|
37
37
|
virtualToDisplay,
|
|
38
38
|
} from '../utils/virtual-scroll-logic';
|
|
39
39
|
import { useVirtualScrollSizes } from './useVirtualScrollSizes';
|
|
@@ -43,79 +43,107 @@ import { useVirtualScrollSizes } from './useVirtualScrollSizes';
|
|
|
43
43
|
* Handles calculation of visible items, scroll events, dynamic item sizes, and programmatic scrolling.
|
|
44
44
|
*
|
|
45
45
|
* @param propsInput - The configuration properties. Can be a plain object, a Ref, or a getter function.
|
|
46
|
+
* @param extensions - Optional list of extensions to enhance functionality (RTL, Snapping, Sticky, etc.).
|
|
46
47
|
* @see VirtualScrollProps
|
|
47
48
|
*/
|
|
48
|
-
export function useVirtualScroll<T = unknown>(
|
|
49
|
+
export function useVirtualScroll<T = unknown>(
|
|
50
|
+
propsInput: Ref<VirtualScrollProps<T>> | (() => VirtualScrollProps<T>),
|
|
51
|
+
extensions: VirtualScrollExtension<T>[] = [],
|
|
52
|
+
) {
|
|
49
53
|
const props = computed(() => toValue(propsInput));
|
|
50
54
|
|
|
51
55
|
// --- State ---
|
|
56
|
+
/** Current horizontal display scroll position (DU). */
|
|
52
57
|
const scrollX = ref(0);
|
|
58
|
+
/** Current vertical display scroll position (DU). */
|
|
53
59
|
const scrollY = ref(0);
|
|
60
|
+
/** Current horizontal virtual scroll position (VU). */
|
|
61
|
+
const internalScrollX = ref(0);
|
|
62
|
+
/** Current vertical virtual scroll position (VU). */
|
|
63
|
+
const internalScrollY = ref(0);
|
|
64
|
+
/** Whether the container is currently being scrolled. */
|
|
54
65
|
const isScrolling = ref(false);
|
|
66
|
+
/** Whether the component has finished its first client-side mount. */
|
|
55
67
|
const isHydrated = ref(false);
|
|
68
|
+
/** Whether the component is in the process of initial hydration. */
|
|
56
69
|
const isHydrating = ref(false);
|
|
70
|
+
/** Whether the component is currently mounted in the DOM. */
|
|
57
71
|
const isMounted = ref(false);
|
|
72
|
+
/** Whether the current text direction is Right-to-Left. */
|
|
58
73
|
const isRtl = ref(false);
|
|
74
|
+
/** Current physical width of the visible viewport area (DU). */
|
|
59
75
|
const viewportWidth = ref(0);
|
|
76
|
+
/** Current physical height of the visible viewport area (DU). */
|
|
60
77
|
const viewportHeight = ref(0);
|
|
78
|
+
/** Current offset of the items wrapper relative to the scroll container (DU). */
|
|
61
79
|
const hostOffset = reactive({ x: 0, y: 0 });
|
|
80
|
+
/** Current offset of the root host element relative to the scroll container (DU). */
|
|
62
81
|
const hostRefOffset = reactive({ x: 0, y: 0 });
|
|
82
|
+
/** Timeout handle for the scroll end detection. */
|
|
63
83
|
let scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let lastInternalX = 0;
|
|
70
|
-
/** The last recorded virtual Y position, used to detect scroll direction for snapping. */
|
|
71
|
-
let lastInternalY = 0;
|
|
72
|
-
/** The current horizontal scroll direction ('start' towards left/logical start, 'end' towards right/logical end). */
|
|
73
|
-
let scrollDirectionX: 'start' | 'end' | null = null;
|
|
74
|
-
/** The current vertical scroll direction ('start' towards top, 'end' towards bottom). */
|
|
75
|
-
let scrollDirectionY: 'start' | 'end' | null = null;
|
|
85
|
+
/** Scaling factor for horizontal virtual coordinates. */
|
|
86
|
+
const scaleX = ref(1);
|
|
87
|
+
/** Scaling factor for vertical virtual coordinates. */
|
|
88
|
+
const scaleY = ref(1);
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
/** Current horizontal scroll direction. */
|
|
91
|
+
const scrollDirectionX = ref<'start' | 'end' | null>(null);
|
|
92
|
+
/** Current vertical scroll direction. */
|
|
93
|
+
const scrollDirectionY = ref<'start' | 'end' | null>(null);
|
|
78
94
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const container = props.value.container || props.value.hostRef || window;
|
|
87
|
-
const el = isElement(container) ? container : document.documentElement;
|
|
95
|
+
/** Information about a scroll operation that is waiting for measurements. */
|
|
96
|
+
const pendingScroll = ref<{
|
|
97
|
+
rowIndex: number | null | undefined;
|
|
98
|
+
colIndex: number | null | undefined;
|
|
99
|
+
options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
100
|
+
} | null>(null);
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
/** Whether the current scroll operation was initiated programmatically. */
|
|
103
|
+
const isProgrammaticScroll = ref(false);
|
|
104
|
+
/** Timeout handle for smooth programmatic scroll completion. */
|
|
105
|
+
let programmaticScrollTimer: ReturnType<typeof setTimeout> | undefined;
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Immediately stops any currently active smooth scroll animation and clears pending corrections.
|
|
109
|
+
*/
|
|
110
|
+
const stopProgrammaticScroll = () => {
|
|
111
|
+
isProgrammaticScroll.value = false;
|
|
112
|
+
clearTimeout(programmaticScrollTimer);
|
|
113
|
+
pendingScroll.value = null;
|
|
95
114
|
};
|
|
96
115
|
|
|
116
|
+
/** Horizontal virtual scroll position at the start of the current scroll interaction (VU). */
|
|
117
|
+
let scrollStartX = 0;
|
|
118
|
+
/** Vertical virtual scroll position at the start of the current scroll interaction (VU). */
|
|
119
|
+
let scrollStartY = 0;
|
|
120
|
+
|
|
97
121
|
// --- Computed Config ---
|
|
122
|
+
/** Validated scroll direction. */
|
|
98
123
|
const direction = computed(() => [ 'vertical', 'horizontal', 'both' ].includes(props.value.direction as string) ? props.value.direction as ScrollDirection : 'vertical' as ScrollDirection);
|
|
99
124
|
|
|
125
|
+
/** Whether the items have dynamic height or width. */
|
|
100
126
|
const isDynamicItemSize = computed(() =>
|
|
101
127
|
props.value.itemSize === undefined || props.value.itemSize === null || props.value.itemSize === 0,
|
|
102
128
|
);
|
|
103
129
|
|
|
130
|
+
/** Whether the columns have dynamic widths. */
|
|
104
131
|
const isDynamicColumnWidth = computed(() =>
|
|
105
132
|
props.value.columnWidth === undefined || props.value.columnWidth === null || props.value.columnWidth === 0,
|
|
106
133
|
);
|
|
107
134
|
|
|
135
|
+
/** Fixed pixel size of items if configured as a number. */
|
|
108
136
|
const fixedItemSize = computed(() =>
|
|
109
137
|
(typeof props.value.itemSize === 'number' && props.value.itemSize > 0) ? props.value.itemSize : null,
|
|
110
138
|
);
|
|
111
139
|
|
|
140
|
+
/** Fixed pixel width of columns if configured as a number. */
|
|
112
141
|
const fixedColumnWidth = computed(() =>
|
|
113
142
|
(typeof props.value.columnWidth === 'number' && props.value.columnWidth > 0) ? props.value.columnWidth : null,
|
|
114
143
|
);
|
|
115
144
|
|
|
145
|
+
/** Fallback size for items before they are measured. */
|
|
116
146
|
const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
|
|
117
|
-
|
|
118
|
-
// --- Size Management ---
|
|
119
147
|
const {
|
|
120
148
|
itemSizesX,
|
|
121
149
|
itemSizesY,
|
|
@@ -124,6 +152,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
124
152
|
measuredItemsY,
|
|
125
153
|
treeUpdateFlag,
|
|
126
154
|
getSizeAt,
|
|
155
|
+
getItemBaseSize,
|
|
127
156
|
initializeSizes,
|
|
128
157
|
updateItemSizes: coreUpdateItemSizes,
|
|
129
158
|
refresh: coreRefresh,
|
|
@@ -136,19 +165,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
136
165
|
direction: direction.value,
|
|
137
166
|
})));
|
|
138
167
|
|
|
139
|
-
// --- Scroll Queue / Correction ---
|
|
140
|
-
const pendingScroll = ref<{
|
|
141
|
-
rowIndex: number | null | undefined;
|
|
142
|
-
colIndex: number | null | undefined;
|
|
143
|
-
options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
144
|
-
} | null>(null);
|
|
145
|
-
|
|
146
|
-
const sortedStickyIndices = computed(() =>
|
|
147
|
-
[ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const stickyIndicesSet = computed(() => new Set(sortedStickyIndices.value));
|
|
151
|
-
|
|
152
168
|
const paddingStartX = computed(() => getPaddingX(props.value.scrollPaddingStart, props.value.direction));
|
|
153
169
|
const paddingEndX = computed(() => getPaddingX(props.value.scrollPaddingEnd, props.value.direction));
|
|
154
170
|
const paddingStartY = computed(() => getPaddingY(props.value.scrollPaddingStart, props.value.direction));
|
|
@@ -165,13 +181,8 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
165
181
|
const flowEndY = computed(() => getPaddingY(props.value.flowPaddingEnd, props.value.direction));
|
|
166
182
|
|
|
167
183
|
const usableWidth = computed(() => viewportWidth.value - (direction.value !== 'vertical' ? (stickyStartX.value + stickyEndX.value) : 0));
|
|
168
|
-
|
|
169
184
|
const usableHeight = computed(() => viewportHeight.value - (direction.value !== 'horizontal' ? (stickyStartY.value + stickyEndY.value) : 0));
|
|
170
185
|
|
|
171
|
-
// --- Size Calculations ---
|
|
172
|
-
/**
|
|
173
|
-
* Total size (width and height) of all items in the scrollable area.
|
|
174
|
-
*/
|
|
175
186
|
const totalSize = computed(() => {
|
|
176
187
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
177
188
|
treeUpdateFlag.value;
|
|
@@ -193,12 +204,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
193
204
|
});
|
|
194
205
|
|
|
195
206
|
const isWindowContainer = computed(() => isWindowLike(props.value.container));
|
|
196
|
-
|
|
197
207
|
const virtualWidth = computed(() => totalSize.value.width + paddingStartX.value + paddingEndX.value);
|
|
198
208
|
const virtualHeight = computed(() => totalSize.value.height + paddingStartY.value + paddingEndY.value);
|
|
199
|
-
|
|
200
209
|
const totalWidth = computed(() => (flowStartX.value + stickyStartX.value + stickyEndX.value + flowEndX.value + virtualWidth.value));
|
|
201
|
-
|
|
202
210
|
const totalHeight = computed(() => (flowStartY.value + stickyStartY.value + stickyEndY.value + flowEndY.value + virtualHeight.value));
|
|
203
211
|
|
|
204
212
|
const componentOffset = reactive({
|
|
@@ -208,13 +216,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
208
216
|
|
|
209
217
|
const renderedWidth = computed(() => calculateRenderedSize(isWindowContainer.value, totalWidth.value));
|
|
210
218
|
const renderedHeight = computed(() => calculateRenderedSize(isWindowContainer.value, totalHeight.value));
|
|
211
|
-
|
|
212
219
|
const renderedVirtualWidth = computed(() => calculateRenderedSize(isWindowContainer.value, virtualWidth.value));
|
|
213
220
|
const renderedVirtualHeight = computed(() => calculateRenderedSize(isWindowContainer.value, virtualHeight.value));
|
|
214
221
|
|
|
215
|
-
const scaleX = computed(() => calculateScale(isWindowContainer.value, totalWidth.value, viewportWidth.value));
|
|
216
|
-
const scaleY = computed(() => calculateScale(isWindowContainer.value, totalHeight.value, viewportHeight.value));
|
|
217
|
-
|
|
218
222
|
const relativeScrollX = computed(() => {
|
|
219
223
|
if (direction.value === 'vertical') {
|
|
220
224
|
return 0;
|
|
@@ -232,10 +236,41 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
232
236
|
});
|
|
233
237
|
|
|
234
238
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
239
|
+
* Helper to get the row (or item) index at a specific vertical (or horizontal in horizontal mode) virtual offset (VU).
|
|
240
|
+
* @param offset - The virtual pixel offset.
|
|
241
|
+
*/
|
|
242
|
+
const getRowIndexAt = (offset: number) => {
|
|
243
|
+
const isHorizontal = direction.value === 'horizontal';
|
|
244
|
+
return calculateIndexAt(
|
|
245
|
+
offset,
|
|
246
|
+
fixedItemSize.value,
|
|
247
|
+
isHorizontal ? (props.value.columnGap || 0) : (props.value.gap || 0),
|
|
248
|
+
(off) => (isHorizontal ? itemSizesX.findLowerBound(off) : itemSizesY.findLowerBound(off)),
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Helper to get the column index at a specific horizontal virtual offset (VU).
|
|
254
|
+
* @param offset - The virtual pixel offset.
|
|
255
|
+
*/
|
|
256
|
+
const getColIndexAt = (offset: number) => {
|
|
257
|
+
if (direction.value === 'both') {
|
|
258
|
+
return calculateIndexAt(
|
|
259
|
+
offset,
|
|
260
|
+
fixedColumnWidth.value,
|
|
261
|
+
props.value.columnGap || 0,
|
|
262
|
+
(off) => columnSizes.findLowerBound(off),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (direction.value === 'horizontal') {
|
|
266
|
+
return getRowIndexAt(offset);
|
|
267
|
+
}
|
|
268
|
+
return 0;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Helper to get the width of a specific column.
|
|
237
273
|
* @param index - The column index.
|
|
238
|
-
* @returns The width in pixels (excluding gap).
|
|
239
274
|
*/
|
|
240
275
|
const getColumnWidth = (index: number) => {
|
|
241
276
|
if (direction.value === 'both') {
|
|
@@ -259,16 +294,13 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
259
294
|
};
|
|
260
295
|
|
|
261
296
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
297
|
+
* Helper to get the height of a specific row.
|
|
264
298
|
* @param index - The row index.
|
|
265
|
-
* @returns The height in pixels (excluding gap).
|
|
266
299
|
*/
|
|
267
300
|
const getRowHeight = (index: number) => {
|
|
268
301
|
if (direction.value === 'horizontal') {
|
|
269
302
|
return usableHeight.value;
|
|
270
303
|
}
|
|
271
|
-
|
|
272
304
|
return getSizeAt(
|
|
273
305
|
index,
|
|
274
306
|
props.value.itemSize,
|
|
@@ -279,23 +311,47 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
279
311
|
);
|
|
280
312
|
};
|
|
281
313
|
|
|
282
|
-
// --- Public Scroll API ---
|
|
283
314
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
315
|
+
* Helper to get the virtual offset of a specific item.
|
|
316
|
+
* @param index - The item index.
|
|
317
|
+
*/
|
|
318
|
+
const getItemOffset = (index: number) => (direction.value === 'horizontal' ? (flowStartX.value + stickyStartX.value + paddingStartX.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.columnGap || 0, (idx) => itemSizesX.query(idx)) : (flowStartY.value + stickyStartY.value + paddingStartY.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.gap || 0, (idx) => itemSizesY.query(idx)));
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Helper to get the size of a specific item along the scroll axis.
|
|
322
|
+
* @param index - The item index.
|
|
290
323
|
*/
|
|
324
|
+
const getItemSize = (index: number) => (direction.value === 'horizontal' ? getColumnWidth(index) : getRowHeight(index));
|
|
325
|
+
const updateDirection = () => {
|
|
326
|
+
if (typeof window === 'undefined') {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const container = props.value.container || props.value.hostRef || window;
|
|
330
|
+
const el = isElement(container) ? container : document.documentElement;
|
|
331
|
+
const computedStyle = window.getComputedStyle(el);
|
|
332
|
+
const newRtl = computedStyle.direction === 'rtl';
|
|
333
|
+
if (isRtl.value !== newRtl) {
|
|
334
|
+
isRtl.value = newRtl;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleScrollCorrection = (addedX: number, addedY: number) => {
|
|
339
|
+
nextTick(() => {
|
|
340
|
+
scrollToOffset(
|
|
341
|
+
addedX > 0 ? relativeScrollX.value + addedX : null,
|
|
342
|
+
addedY > 0 ? relativeScrollY.value + addedY : null,
|
|
343
|
+
{ behavior: 'auto' },
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
|
|
291
348
|
function scrollToIndex(
|
|
292
349
|
rowIndex?: number | null,
|
|
293
350
|
colIndex?: number | null,
|
|
294
351
|
options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions,
|
|
295
|
-
) {
|
|
296
|
-
const isCorrection =
|
|
297
|
-
|
|
298
|
-
: false;
|
|
352
|
+
): ScrollToIndexResult {
|
|
353
|
+
const isCorrection = isScrollToIndexOptions(options) ? options.isCorrection : false;
|
|
354
|
+
const dryRun = isScrollToIndexOptions(options) ? options.dryRun : false;
|
|
299
355
|
|
|
300
356
|
const container = props.value.container || window;
|
|
301
357
|
|
|
@@ -324,7 +380,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
324
380
|
scaleY: scaleY.value,
|
|
325
381
|
hostOffsetX: componentOffset.x,
|
|
326
382
|
hostOffsetY: componentOffset.y,
|
|
327
|
-
stickyIndices:
|
|
383
|
+
stickyIndices: (props.value.stickyIndices || []),
|
|
328
384
|
stickyStartX: stickyStartX.value,
|
|
329
385
|
stickyStartY: stickyStartY.value,
|
|
330
386
|
stickyEndX: stickyEndX.value,
|
|
@@ -337,7 +393,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
337
393
|
paddingEndY: paddingEndY.value,
|
|
338
394
|
});
|
|
339
395
|
|
|
340
|
-
if (!isCorrection) {
|
|
396
|
+
if (!isCorrection && !dryRun) {
|
|
341
397
|
const behavior = isScrollToIndexOptions(options) ? options.behavior : undefined;
|
|
342
398
|
pendingScroll.value = {
|
|
343
399
|
rowIndex,
|
|
@@ -351,7 +407,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
351
407
|
|
|
352
408
|
const displayTargetX = virtualToDisplay(targetX, componentOffset.x, scaleX.value);
|
|
353
409
|
const displayTargetY = virtualToDisplay(targetY, componentOffset.y, scaleY.value);
|
|
354
|
-
|
|
355
410
|
const finalX = isRtl.value ? -displayTargetX : displayTargetX;
|
|
356
411
|
const finalY = displayTargetY;
|
|
357
412
|
|
|
@@ -359,13 +414,23 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
359
414
|
if (isScrollToIndexOptions(options)) {
|
|
360
415
|
behavior = options.behavior;
|
|
361
416
|
}
|
|
362
|
-
|
|
363
417
|
const scrollBehavior = isCorrection ? 'auto' : (behavior || 'smooth');
|
|
364
|
-
isProgrammaticScroll.value = true;
|
|
365
418
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
419
|
+
if (!dryRun) {
|
|
420
|
+
if (scrollBehavior === 'smooth' || !isCorrection) {
|
|
421
|
+
isProgrammaticScroll.value = true;
|
|
422
|
+
clearTimeout(programmaticScrollTimer);
|
|
423
|
+
if (scrollBehavior === 'smooth') {
|
|
424
|
+
programmaticScrollTimer = setTimeout(() => {
|
|
425
|
+
isProgrammaticScroll.value = false;
|
|
426
|
+
programmaticScrollTimer = undefined;
|
|
427
|
+
checkPendingScroll();
|
|
428
|
+
}, 1000);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const scrollOptions: ScrollToOptions = { behavior: scrollBehavior };
|
|
369
434
|
if (colIndex !== null && colIndex !== undefined) {
|
|
370
435
|
scrollOptions.left = isRtl.value ? finalX : Math.max(0, finalX);
|
|
371
436
|
}
|
|
@@ -373,9 +438,11 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
373
438
|
scrollOptions.top = Math.max(0, finalY);
|
|
374
439
|
}
|
|
375
440
|
|
|
376
|
-
|
|
441
|
+
if (!isCorrection && !dryRun) {
|
|
442
|
+
scrollTo(container, scrollOptions);
|
|
443
|
+
}
|
|
377
444
|
|
|
378
|
-
if (scrollBehavior === 'auto' || scrollBehavior === undefined) {
|
|
445
|
+
if (!dryRun && (scrollBehavior === 'auto' || scrollBehavior === undefined)) {
|
|
379
446
|
if (colIndex !== null && colIndex !== undefined) {
|
|
380
447
|
scrollX.value = (isRtl.value ? finalX : Math.max(0, finalX));
|
|
381
448
|
internalScrollX.value = targetX;
|
|
@@ -384,35 +451,26 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
384
451
|
scrollY.value = Math.max(0, finalY);
|
|
385
452
|
internalScrollY.value = targetY;
|
|
386
453
|
}
|
|
387
|
-
|
|
388
|
-
if (pendingScroll.value) {
|
|
389
|
-
const currentOptions = pendingScroll.value.options;
|
|
390
|
-
if (isScrollToIndexOptions(currentOptions)) {
|
|
391
|
-
currentOptions.behavior = 'auto';
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
454
|
}
|
|
455
|
+
|
|
456
|
+
return { targetX, targetY, displayTargetX, displayTargetY };
|
|
395
457
|
}
|
|
396
458
|
|
|
397
|
-
|
|
398
|
-
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
399
|
-
*
|
|
400
|
-
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
401
|
-
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
402
|
-
* @param options - Scroll options (behavior).
|
|
403
|
-
* @param options.behavior - The scroll behavior ('auto' | 'smooth'). Defaults to 'auto'.
|
|
404
|
-
*/
|
|
405
|
-
const scrollToOffset = (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => {
|
|
459
|
+
function scrollToOffset(x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) {
|
|
406
460
|
const container = props.value.container || window;
|
|
407
461
|
isProgrammaticScroll.value = true;
|
|
462
|
+
clearTimeout(programmaticScrollTimer);
|
|
463
|
+
if (options?.behavior === 'smooth') {
|
|
464
|
+
programmaticScrollTimer = setTimeout(() => {
|
|
465
|
+
isProgrammaticScroll.value = false;
|
|
466
|
+
programmaticScrollTimer = undefined;
|
|
467
|
+
checkPendingScroll();
|
|
468
|
+
}, 1000);
|
|
469
|
+
}
|
|
408
470
|
pendingScroll.value = null;
|
|
409
471
|
|
|
410
|
-
const clampedX = (x !== null && x !== undefined)
|
|
411
|
-
|
|
412
|
-
: null;
|
|
413
|
-
const clampedY = (y !== null && y !== undefined)
|
|
414
|
-
? Math.max(0, Math.min(y, totalHeight.value - viewportHeight.value))
|
|
415
|
-
: null;
|
|
472
|
+
const clampedX = (x !== null && x !== undefined) ? Math.max(0, Math.min(x, totalWidth.value - viewportWidth.value)) : null;
|
|
473
|
+
const clampedY = (y !== null && y !== undefined) ? Math.max(0, Math.min(y, totalHeight.value - viewportHeight.value)) : null;
|
|
416
474
|
|
|
417
475
|
if (clampedX !== null) {
|
|
418
476
|
internalScrollX.value = clampedX;
|
|
@@ -423,25 +481,18 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
423
481
|
|
|
424
482
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
425
483
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
426
|
-
|
|
427
484
|
const displayTargetX = (clampedX !== null) ? virtualToDisplay(clampedX, componentOffset.x, scaleX.value) : null;
|
|
428
485
|
const displayTargetY = (clampedY !== null) ? virtualToDisplay(clampedY, componentOffset.y, scaleY.value) : null;
|
|
429
|
-
|
|
430
|
-
const targetX = (displayTargetX !== null)
|
|
431
|
-
? (isRtl.value ? -displayTargetX : displayTargetX)
|
|
432
|
-
: currentX;
|
|
486
|
+
const targetX = (displayTargetX !== null) ? (isRtl.value ? -displayTargetX : displayTargetX) : currentX;
|
|
433
487
|
const targetY = (displayTargetY !== null) ? displayTargetY : currentY;
|
|
434
488
|
|
|
435
|
-
const scrollOptions: ScrollToOptions = {
|
|
436
|
-
behavior: options?.behavior || 'auto',
|
|
437
|
-
};
|
|
489
|
+
const scrollOptions: ScrollToOptions = { behavior: options?.behavior || 'auto' };
|
|
438
490
|
if (x !== null && x !== undefined) {
|
|
439
491
|
scrollOptions.left = targetX;
|
|
440
492
|
}
|
|
441
493
|
if (y !== null && y !== undefined) {
|
|
442
494
|
scrollOptions.top = targetY;
|
|
443
495
|
}
|
|
444
|
-
|
|
445
496
|
scrollTo(container, scrollOptions);
|
|
446
497
|
|
|
447
498
|
if (options?.behavior === 'auto' || options?.behavior === undefined) {
|
|
@@ -452,193 +503,16 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
452
503
|
scrollY.value = targetY;
|
|
453
504
|
}
|
|
454
505
|
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// --- Measurement & Initialization ---
|
|
458
|
-
const handleScrollCorrection = (addedX: number, addedY: number) => {
|
|
459
|
-
nextTick(() => {
|
|
460
|
-
scrollToOffset(
|
|
461
|
-
addedX > 0 ? relativeScrollX.value + addedX : null,
|
|
462
|
-
addedY > 0 ? relativeScrollY.value + addedY : null,
|
|
463
|
-
{ behavior: 'auto', isCorrection: true } as ScrollToIndexOptions,
|
|
464
|
-
);
|
|
465
|
-
});
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const initialize = () => initializeSizes(handleScrollCorrection);
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Updates the host element's offset relative to the scroll container.
|
|
472
|
-
*/
|
|
473
|
-
const updateHostOffset = () => {
|
|
474
|
-
if (typeof window === 'undefined') {
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
const container = props.value.container || window;
|
|
478
|
-
|
|
479
|
-
const calculateOffset = (el: HTMLElement) => {
|
|
480
|
-
const rect = el.getBoundingClientRect();
|
|
481
|
-
if (container === window) {
|
|
482
|
-
return {
|
|
483
|
-
x: isRtl.value
|
|
484
|
-
? document.documentElement.clientWidth - rect.right - window.scrollX
|
|
485
|
-
: rect.left + window.scrollX,
|
|
486
|
-
y: rect.top + window.scrollY,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
if (container === el) {
|
|
490
|
-
return { x: 0, y: 0 };
|
|
491
|
-
}
|
|
492
|
-
if (isElement(container)) {
|
|
493
|
-
const containerRect = container.getBoundingClientRect();
|
|
494
|
-
return {
|
|
495
|
-
x: isRtl.value
|
|
496
|
-
? containerRect.right - rect.right - container.scrollLeft
|
|
497
|
-
: rect.left - containerRect.left + container.scrollLeft,
|
|
498
|
-
y: rect.top - containerRect.top + container.scrollTop,
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
return { x: 0, y: 0 };
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
if (props.value.hostElement) {
|
|
505
|
-
const newOffset = calculateOffset(props.value.hostElement);
|
|
506
|
-
if (Math.abs(hostOffset.x - newOffset.x) > 0.1 || Math.abs(hostOffset.y - newOffset.y) > 0.1) {
|
|
507
|
-
hostOffset.x = newOffset.x;
|
|
508
|
-
hostOffset.y = newOffset.y;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (props.value.hostRef) {
|
|
513
|
-
const newOffset = calculateOffset(props.value.hostRef);
|
|
514
|
-
if (Math.abs(hostRefOffset.x - newOffset.x) > 0.1 || Math.abs(hostRefOffset.y - newOffset.y) > 0.1) {
|
|
515
|
-
hostRefOffset.x = newOffset.x;
|
|
516
|
-
hostRefOffset.y = newOffset.y;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
watch([
|
|
522
|
-
() => props.value.items,
|
|
523
|
-
() => props.value.items.length,
|
|
524
|
-
() => props.value.direction,
|
|
525
|
-
() => props.value.columnCount,
|
|
526
|
-
() => props.value.columnWidth,
|
|
527
|
-
() => props.value.itemSize,
|
|
528
|
-
() => props.value.gap,
|
|
529
|
-
() => props.value.columnGap,
|
|
530
|
-
() => props.value.defaultItemSize,
|
|
531
|
-
() => props.value.defaultColumnWidth,
|
|
532
|
-
], initialize, { immediate: true });
|
|
533
|
-
|
|
534
|
-
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
535
|
-
updateHostOffset();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
watch(isRtl, (newRtl, oldRtl) => {
|
|
539
|
-
if (oldRtl === undefined || newRtl === oldRtl || !isMounted.value) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Use the oldRtl to correctly interpret the current scrollX
|
|
544
|
-
if (direction.value === 'vertical') {
|
|
545
|
-
updateHostOffset();
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const scrollValue = oldRtl ? Math.abs(scrollX.value) : scrollX.value;
|
|
550
|
-
const oldRelativeScrollX = displayToVirtual(scrollValue, hostOffset.x, scaleX.value);
|
|
551
|
-
|
|
552
|
-
// Update host offset for the new direction
|
|
553
|
-
updateHostOffset();
|
|
554
|
-
|
|
555
|
-
// Maintain logical horizontal position when direction changes
|
|
556
|
-
scrollToOffset(oldRelativeScrollX, null, { behavior: 'auto' });
|
|
557
|
-
}, { flush: 'sync' });
|
|
558
|
-
|
|
559
|
-
watch([ scaleX, scaleY ], () => {
|
|
560
|
-
if (!isMounted.value || isScrolling.value || isProgrammaticScroll.value) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
// Sync display scroll to maintain logical position
|
|
564
|
-
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
watch([ () => props.value.items.length, () => props.value.columnCount ], ([ newLen, newColCount ], [ oldLen, oldColCount ]) => {
|
|
568
|
-
nextTick(() => {
|
|
569
|
-
const maxRelX = Math.max(0, totalWidth.value - viewportWidth.value);
|
|
570
|
-
const maxRelY = Math.max(0, totalHeight.value - viewportHeight.value);
|
|
571
|
-
|
|
572
|
-
if (internalScrollX.value > maxRelX || internalScrollY.value > maxRelY) {
|
|
573
|
-
scrollToOffset(
|
|
574
|
-
Math.min(internalScrollX.value, maxRelX),
|
|
575
|
-
Math.min(internalScrollY.value, maxRelY),
|
|
576
|
-
{ behavior: 'auto' },
|
|
577
|
-
);
|
|
578
|
-
} else if ((newLen !== oldLen && scaleY.value !== 1) || (newColCount !== oldColCount && scaleX.value !== 1)) {
|
|
579
|
-
// Even if within bounds, we must sync the display scroll position
|
|
580
|
-
// because the coordinate scaling factor changed.
|
|
581
|
-
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
582
|
-
}
|
|
583
|
-
updateHostOffset();
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// --- Range & Visible Items ---
|
|
588
|
-
/**
|
|
589
|
-
* Helper to get the row index (or item index in list mode) at a specific virtual offset.
|
|
590
|
-
*
|
|
591
|
-
* @param offset - The virtual pixel offset (VU).
|
|
592
|
-
* @returns The index at that position.
|
|
593
|
-
*/
|
|
594
|
-
const getRowIndexAt = (offset: number) => {
|
|
595
|
-
const isHorizontal = direction.value === 'horizontal';
|
|
596
|
-
return calculateIndexAt(
|
|
597
|
-
offset,
|
|
598
|
-
fixedItemSize.value,
|
|
599
|
-
isHorizontal ? (props.value.columnGap || 0) : (props.value.gap || 0),
|
|
600
|
-
(off) => (isHorizontal ? itemSizesX.findLowerBound(off) : itemSizesY.findLowerBound(off)),
|
|
601
|
-
);
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Helper to get the column index at a specific virtual offset.
|
|
606
|
-
*
|
|
607
|
-
* @param offset - The virtual pixel offset (VU).
|
|
608
|
-
* @returns The column index at that position.
|
|
609
|
-
*/
|
|
610
|
-
const getColIndexAt = (offset: number) => {
|
|
611
|
-
if (direction.value === 'both') {
|
|
612
|
-
return calculateIndexAt(
|
|
613
|
-
offset,
|
|
614
|
-
fixedColumnWidth.value,
|
|
615
|
-
props.value.columnGap || 0,
|
|
616
|
-
(off) => columnSizes.findLowerBound(off),
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
if (direction.value === 'horizontal') {
|
|
620
|
-
return getRowIndexAt(offset);
|
|
621
|
-
}
|
|
622
|
-
return 0;
|
|
623
|
-
};
|
|
506
|
+
}
|
|
624
507
|
|
|
625
|
-
/**
|
|
626
|
-
* Current range of items that should be rendered.
|
|
627
|
-
*/
|
|
628
508
|
const range = computed(() => {
|
|
629
509
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
630
510
|
treeUpdateFlag.value;
|
|
631
|
-
|
|
632
511
|
if ((!isHydrated.value || isHydrating.value) && props.value.ssrRange) {
|
|
633
|
-
return {
|
|
634
|
-
start: props.value.ssrRange.start,
|
|
635
|
-
end: props.value.ssrRange.end,
|
|
636
|
-
};
|
|
512
|
+
return { start: props.value.ssrRange.start, end: props.value.ssrRange.end };
|
|
637
513
|
}
|
|
638
|
-
|
|
639
514
|
const bufferBefore = (props.value.ssrRange && !isScrolling.value) ? 0 : (props.value.bufferBefore ?? DEFAULT_BUFFER);
|
|
640
515
|
const bufferAfter = props.value.bufferAfter ?? DEFAULT_BUFFER;
|
|
641
|
-
|
|
642
516
|
return calculateRange({
|
|
643
517
|
direction: direction.value,
|
|
644
518
|
relativeScrollX: relativeScrollX.value,
|
|
@@ -658,13 +532,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
658
532
|
});
|
|
659
533
|
});
|
|
660
534
|
|
|
661
|
-
/**
|
|
662
|
-
* Index of the first visible item in the viewport.
|
|
663
|
-
*/
|
|
664
535
|
const currentIndex = computed(() => {
|
|
665
536
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
666
537
|
treeUpdateFlag.value;
|
|
667
|
-
|
|
668
538
|
const offsetX = relativeScrollX.value + stickyStartX.value;
|
|
669
539
|
const offsetY = relativeScrollY.value + stickyStartY.value;
|
|
670
540
|
const offset = direction.value === 'horizontal' ? offsetX : offsetY;
|
|
@@ -674,18 +544,14 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
674
544
|
const columnRange = computed(() => {
|
|
675
545
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
676
546
|
treeUpdateFlag.value;
|
|
677
|
-
|
|
678
547
|
const totalCols = props.value.columnCount || 0;
|
|
679
|
-
|
|
680
548
|
if (!totalCols) {
|
|
681
549
|
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
682
550
|
}
|
|
683
|
-
|
|
684
551
|
if ((!isHydrated.value || isHydrating.value) && props.value.ssrRange) {
|
|
685
552
|
const { colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
686
553
|
const safeStart = Math.max(0, colStart);
|
|
687
554
|
const safeEnd = Math.min(totalCols, colEnd || totalCols);
|
|
688
|
-
|
|
689
555
|
return calculateColumnRange({
|
|
690
556
|
columnCount: totalCols,
|
|
691
557
|
relativeScrollX: calculateOffsetAt(safeStart, fixedColumnWidth.value, props.value.columnGap || 0, (idx) => columnSizes.query(idx)),
|
|
@@ -698,9 +564,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
698
564
|
totalColsQuery: () => columnSizes.query(totalCols),
|
|
699
565
|
});
|
|
700
566
|
}
|
|
701
|
-
|
|
702
567
|
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
703
|
-
|
|
704
568
|
return calculateColumnRange({
|
|
705
569
|
columnCount: totalCols,
|
|
706
570
|
relativeScrollX: relativeScrollX.value,
|
|
@@ -714,61 +578,72 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
714
578
|
});
|
|
715
579
|
});
|
|
716
580
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
581
|
+
const ctx: ExtensionContext<T> = {
|
|
582
|
+
props,
|
|
583
|
+
scrollDetails: null as unknown as Ref<ScrollDetails<T>>,
|
|
584
|
+
totalSize: computed(() => ({ width: totalWidth.value, height: totalHeight.value })),
|
|
585
|
+
range,
|
|
586
|
+
currentIndex,
|
|
587
|
+
internalState: {
|
|
588
|
+
scrollX,
|
|
589
|
+
scrollY,
|
|
590
|
+
internalScrollX,
|
|
591
|
+
internalScrollY,
|
|
592
|
+
isRtl,
|
|
593
|
+
isScrolling,
|
|
594
|
+
isProgrammaticScroll,
|
|
595
|
+
viewportWidth,
|
|
596
|
+
viewportHeight,
|
|
597
|
+
scaleX,
|
|
598
|
+
scaleY,
|
|
599
|
+
scrollDirectionX,
|
|
600
|
+
scrollDirectionY,
|
|
601
|
+
relativeScrollX,
|
|
602
|
+
relativeScrollY,
|
|
603
|
+
},
|
|
604
|
+
methods: {
|
|
605
|
+
scrollToIndex,
|
|
606
|
+
scrollToOffset,
|
|
607
|
+
updateDirection,
|
|
608
|
+
getRowIndexAt,
|
|
609
|
+
getColIndexAt,
|
|
610
|
+
getItemSize,
|
|
611
|
+
getItemBaseSize,
|
|
612
|
+
getItemOffset,
|
|
613
|
+
handleScrollCorrection,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
720
616
|
|
|
721
617
|
let lastRenderedItems: RenderedItem<T>[] = [];
|
|
722
|
-
|
|
723
618
|
const renderedItems = computed<RenderedItem<T>[]>(() => {
|
|
724
619
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
725
620
|
treeUpdateFlag.value;
|
|
726
|
-
|
|
727
621
|
const { start, end } = range.value;
|
|
728
622
|
const items: RenderedItem<T>[] = [];
|
|
729
|
-
const stickyIndices =
|
|
730
|
-
const stickySet =
|
|
731
|
-
|
|
623
|
+
const stickyIndices = [ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b);
|
|
624
|
+
const stickySet = new Set(stickyIndices);
|
|
732
625
|
const sortedIndices: number[] = [];
|
|
733
|
-
|
|
734
626
|
if (isHydrated.value || !props.value.ssrRange) {
|
|
735
627
|
const activeIdx = currentIndex.value;
|
|
736
628
|
const prevStickyIdx = findPrevStickyIndex(stickyIndices, activeIdx);
|
|
737
|
-
|
|
738
629
|
if (prevStickyIdx !== undefined && prevStickyIdx < start) {
|
|
739
630
|
sortedIndices.push(prevStickyIdx);
|
|
740
631
|
}
|
|
741
632
|
}
|
|
742
|
-
|
|
743
633
|
for (let i = start; i < end; i++) {
|
|
744
634
|
sortedIndices.push(i);
|
|
745
635
|
}
|
|
746
|
-
|
|
747
636
|
const { x: ssrOffsetX, y: ssrOffsetY } = (!isHydrated.value && props.value.ssrRange)
|
|
748
|
-
? calculateSSROffsets(
|
|
749
|
-
direction.value,
|
|
750
|
-
props.value.ssrRange,
|
|
751
|
-
fixedItemSize.value,
|
|
752
|
-
fixedColumnWidth.value,
|
|
753
|
-
props.value.gap || 0,
|
|
754
|
-
props.value.columnGap || 0,
|
|
755
|
-
(idx) => itemSizesY.query(idx),
|
|
756
|
-
(idx) => itemSizesX.query(idx),
|
|
757
|
-
(idx) => columnSizes.query(idx),
|
|
758
|
-
)
|
|
637
|
+
? calculateSSROffsets(direction.value, props.value.ssrRange, fixedItemSize.value, fixedColumnWidth.value, props.value.gap || 0, props.value.columnGap || 0, (idx) => itemSizesY.query(idx), (idx) => itemSizesX.query(idx), (idx) => columnSizes.query(idx))
|
|
759
638
|
: { x: 0, y: 0 };
|
|
760
|
-
|
|
761
639
|
const lastItemsMap = new Map<number, RenderedItem<T>>();
|
|
762
640
|
for (const item of lastRenderedItems) {
|
|
763
641
|
lastItemsMap.set(item.index, item);
|
|
764
642
|
}
|
|
765
|
-
|
|
766
|
-
// Optimization: Cache sequential queries to avoid O(log N) tree traversal for every item
|
|
767
643
|
let lastIndexX = -1;
|
|
768
644
|
let lastOffsetX = 0;
|
|
769
645
|
let lastIndexY = -1;
|
|
770
646
|
let lastOffsetY = 0;
|
|
771
|
-
|
|
772
647
|
const queryXCached = (idx: number) => {
|
|
773
648
|
if (idx === lastIndexX + 1) {
|
|
774
649
|
lastOffsetX += itemSizesX.get(lastIndexX);
|
|
@@ -779,7 +654,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
779
654
|
lastIndexX = idx;
|
|
780
655
|
return lastOffsetX;
|
|
781
656
|
};
|
|
782
|
-
|
|
783
657
|
const queryYCached = (idx: number) => {
|
|
784
658
|
if (idx === lastIndexY + 1) {
|
|
785
659
|
lastOffsetY += itemSizesY.get(lastIndexY);
|
|
@@ -790,91 +664,30 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
790
664
|
lastIndexY = idx;
|
|
791
665
|
return lastOffsetY;
|
|
792
666
|
};
|
|
793
|
-
|
|
794
667
|
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
795
668
|
const itemsStartVU_Y = flowStartY.value + stickyStartY.value + paddingStartY.value;
|
|
796
669
|
const wrapperStartDU_X = flowStartX.value + stickyStartX.value;
|
|
797
670
|
const wrapperStartDU_Y = flowStartY.value + stickyStartY.value;
|
|
798
|
-
|
|
799
671
|
const colRange = columnRange.value;
|
|
800
|
-
|
|
801
|
-
// Optimization: track sticky index pointer
|
|
802
672
|
let currentStickyIndexPtr = 0;
|
|
803
|
-
|
|
804
673
|
for (const i of sortedIndices) {
|
|
805
674
|
const item = props.value.items[ i ];
|
|
806
675
|
if (item === undefined) {
|
|
807
676
|
continue;
|
|
808
677
|
}
|
|
809
|
-
|
|
810
|
-
const { x, y, width, height } = calculateItemPosition({
|
|
811
|
-
index: i,
|
|
812
|
-
direction: direction.value,
|
|
813
|
-
fixedSize: fixedItemSize.value,
|
|
814
|
-
gap: props.value.gap || 0,
|
|
815
|
-
columnGap: props.value.columnGap || 0,
|
|
816
|
-
usableWidth: usableWidth.value,
|
|
817
|
-
usableHeight: usableHeight.value,
|
|
818
|
-
totalWidth: totalSize.value.width,
|
|
819
|
-
queryY: queryYCached,
|
|
820
|
-
queryX: queryXCached,
|
|
821
|
-
getSizeY: (idx) => itemSizesY.get(idx),
|
|
822
|
-
getSizeX: (idx) => itemSizesX.get(idx),
|
|
823
|
-
columnRange: colRange,
|
|
824
|
-
});
|
|
825
|
-
|
|
678
|
+
const { x, y, width, height } = calculateItemPosition({ index: i, direction: direction.value, fixedSize: fixedItemSize.value, gap: props.value.gap || 0, columnGap: props.value.columnGap || 0, usableWidth: usableWidth.value, usableHeight: usableHeight.value, totalWidth: totalSize.value.width, queryY: queryYCached, queryX: queryXCached, getSizeY: (idx) => itemSizesY.get(idx), getSizeX: (idx) => itemSizesX.get(idx), columnRange: colRange });
|
|
826
679
|
const isSticky = stickySet.has(i);
|
|
827
680
|
const originalX = x;
|
|
828
681
|
const originalY = y;
|
|
829
|
-
|
|
830
|
-
// Find next sticky index for optimization
|
|
831
682
|
while (currentStickyIndexPtr < stickyIndices.length && stickyIndices[ currentStickyIndexPtr ]! <= i) {
|
|
832
683
|
currentStickyIndexPtr++;
|
|
833
684
|
}
|
|
834
685
|
const nextStickyIndex = currentStickyIndexPtr < stickyIndices.length ? stickyIndices[ currentStickyIndexPtr ] : undefined;
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
isSticky,
|
|
839
|
-
direction: direction.value,
|
|
840
|
-
relativeScrollX: relativeScrollX.value,
|
|
841
|
-
relativeScrollY: relativeScrollY.value,
|
|
842
|
-
originalX,
|
|
843
|
-
originalY,
|
|
844
|
-
width,
|
|
845
|
-
height,
|
|
846
|
-
stickyIndices,
|
|
847
|
-
fixedSize: fixedItemSize.value,
|
|
848
|
-
fixedWidth: fixedColumnWidth.value,
|
|
849
|
-
gap: props.value.gap || 0,
|
|
850
|
-
columnGap: props.value.columnGap || 0,
|
|
851
|
-
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
852
|
-
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
853
|
-
nextStickyIndex,
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
const offsetX = isHydrated.value
|
|
857
|
-
? (internalScrollX.value / scaleX.value + (x + itemsStartVU_X - internalScrollX.value)) - wrapperStartDU_X
|
|
858
|
-
: (x - ssrOffsetX);
|
|
859
|
-
const offsetY = isHydrated.value
|
|
860
|
-
? (internalScrollY.value / scaleY.value + (y + itemsStartVU_Y - internalScrollY.value)) - wrapperStartDU_Y
|
|
861
|
-
: (y - ssrOffsetY);
|
|
862
|
-
|
|
686
|
+
const { isStickyActive, isStickyActiveX, isStickyActiveY, stickyOffset } = calculateStickyItem({ index: i, isSticky, direction: direction.value, relativeScrollX: relativeScrollX.value, relativeScrollY: relativeScrollY.value, originalX, originalY, width, height, stickyIndices, fixedSize: fixedItemSize.value, fixedWidth: fixedColumnWidth.value, gap: props.value.gap || 0, columnGap: props.value.columnGap || 0, getItemQueryY: (idx) => itemSizesY.query(idx), getItemQueryX: (idx) => itemSizesX.query(idx), nextStickyIndex });
|
|
687
|
+
const offsetX = isHydrated.value ? (internalScrollX.value / scaleX.value + (x + itemsStartVU_X - internalScrollX.value)) - wrapperStartDU_X : (x - ssrOffsetX);
|
|
688
|
+
const offsetY = isHydrated.value ? (internalScrollY.value / scaleY.value + (y + itemsStartVU_Y - internalScrollY.value)) - wrapperStartDU_Y : (y - ssrOffsetY);
|
|
863
689
|
const last = lastItemsMap.get(i);
|
|
864
|
-
if (
|
|
865
|
-
last
|
|
866
|
-
&& last.item === item
|
|
867
|
-
&& last.offset.x === offsetX
|
|
868
|
-
&& last.offset.y === offsetY
|
|
869
|
-
&& last.size.width === width
|
|
870
|
-
&& last.size.height === height
|
|
871
|
-
&& last.isSticky === isSticky
|
|
872
|
-
&& last.isStickyActive === isStickyActive
|
|
873
|
-
&& last.isStickyActiveX === isStickyActiveX
|
|
874
|
-
&& last.isStickyActiveY === isStickyActiveY
|
|
875
|
-
&& last.stickyOffset.x === stickyOffset.x
|
|
876
|
-
&& last.stickyOffset.y === stickyOffset.y
|
|
877
|
-
) {
|
|
690
|
+
if (last && last.item === item && last.offset.x === offsetX && last.offset.y === offsetY && last.size.width === width && last.size.height === height && last.isSticky === isSticky && last.isStickyActive === isStickyActive && last.isStickyActiveX === isStickyActiveX && last.isStickyActiveY === isStickyActiveY && last.stickyOffset.x === stickyOffset.x && last.stickyOffset.y === stickyOffset.y) {
|
|
878
691
|
items.push(last);
|
|
879
692
|
} else {
|
|
880
693
|
items.push({
|
|
@@ -892,53 +705,38 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
892
705
|
});
|
|
893
706
|
}
|
|
894
707
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
708
|
+
let finalItems = items;
|
|
709
|
+
extensions.forEach((ext) => {
|
|
710
|
+
if (ext.transformRenderedItems) {
|
|
711
|
+
finalItems = ext.transformRenderedItems(finalItems, ctx);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
lastRenderedItems = finalItems;
|
|
715
|
+
return finalItems;
|
|
899
716
|
});
|
|
900
717
|
|
|
901
|
-
const
|
|
718
|
+
const computedScrollDetails = computed<ScrollDetails<T>>(() => {
|
|
902
719
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
903
720
|
treeUpdateFlag.value;
|
|
904
|
-
|
|
905
721
|
const currentScrollX = relativeScrollX.value + stickyStartX.value;
|
|
906
722
|
const currentScrollY = relativeScrollY.value + stickyStartY.value;
|
|
907
|
-
|
|
908
723
|
const currentEndScrollX = relativeScrollX.value + (viewportWidth.value - stickyEndX.value) - 1;
|
|
909
724
|
const currentEndScrollY = relativeScrollY.value + (viewportHeight.value - stickyEndY.value) - 1;
|
|
910
|
-
|
|
911
725
|
const currentColIndex = getColIndexAt(currentScrollX);
|
|
912
726
|
const currentRowIndex = getRowIndexAt(currentScrollY);
|
|
913
727
|
const currentEndIndex = getRowIndexAt(direction.value === 'horizontal' ? currentEndScrollX : currentEndScrollY);
|
|
914
728
|
const currentEndColIndex = getColIndexAt(currentEndScrollX);
|
|
915
|
-
|
|
916
729
|
return {
|
|
917
730
|
items: renderedItems.value,
|
|
918
731
|
currentIndex: currentRowIndex,
|
|
919
732
|
currentColIndex,
|
|
920
733
|
currentEndIndex,
|
|
921
734
|
currentEndColIndex,
|
|
922
|
-
scrollOffset: {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
},
|
|
926
|
-
|
|
927
|
-
x: isRtl.value ? Math.abs(scrollX.value + hostRefOffset.x) : Math.max(0, scrollX.value - hostRefOffset.x),
|
|
928
|
-
y: Math.max(0, scrollY.value - hostRefOffset.y),
|
|
929
|
-
},
|
|
930
|
-
viewportSize: {
|
|
931
|
-
width: viewportWidth.value,
|
|
932
|
-
height: viewportHeight.value,
|
|
933
|
-
},
|
|
934
|
-
displayViewportSize: {
|
|
935
|
-
width: viewportWidth.value,
|
|
936
|
-
height: viewportHeight.value,
|
|
937
|
-
},
|
|
938
|
-
totalSize: {
|
|
939
|
-
width: totalWidth.value,
|
|
940
|
-
height: totalHeight.value,
|
|
941
|
-
},
|
|
735
|
+
scrollOffset: { x: internalScrollX.value, y: internalScrollY.value },
|
|
736
|
+
displayScrollOffset: { x: isRtl.value ? Math.abs(scrollX.value + hostRefOffset.x) : Math.max(0, scrollX.value - hostRefOffset.x), y: Math.max(0, scrollY.value - hostRefOffset.y) },
|
|
737
|
+
viewportSize: { width: viewportWidth.value, height: viewportHeight.value },
|
|
738
|
+
displayViewportSize: { width: viewportWidth.value, height: viewportHeight.value },
|
|
739
|
+
totalSize: { width: totalWidth.value, height: totalHeight.value },
|
|
942
740
|
isScrolling: isScrolling.value,
|
|
943
741
|
isProgrammaticScroll: isProgrammaticScroll.value,
|
|
944
742
|
range: range.value,
|
|
@@ -946,26 +744,16 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
946
744
|
};
|
|
947
745
|
});
|
|
948
746
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
*/
|
|
953
|
-
const stopProgrammaticScroll = () => {
|
|
954
|
-
isProgrammaticScroll.value = false;
|
|
955
|
-
pendingScroll.value = null;
|
|
956
|
-
};
|
|
747
|
+
ctx.scrollDetails = computedScrollDetails;
|
|
748
|
+
|
|
749
|
+
extensions.forEach((ext) => ext.onInit?.(ctx));
|
|
957
750
|
|
|
958
|
-
/**
|
|
959
|
-
* Event handler for scroll events.
|
|
960
|
-
*/
|
|
961
751
|
const handleScroll = (e: Event) => {
|
|
962
752
|
const target = e.target;
|
|
963
753
|
if (typeof window === 'undefined') {
|
|
964
754
|
return;
|
|
965
755
|
}
|
|
966
|
-
|
|
967
756
|
updateDirection();
|
|
968
|
-
|
|
969
757
|
if (target === window || target === document) {
|
|
970
758
|
scrollX.value = window.scrollX;
|
|
971
759
|
scrollY.value = window.scrollY;
|
|
@@ -977,228 +765,136 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
977
765
|
viewportWidth.value = target.clientWidth;
|
|
978
766
|
viewportHeight.value = target.clientHeight;
|
|
979
767
|
}
|
|
980
|
-
|
|
981
768
|
const scrollValueX = isRtl.value ? Math.abs(scrollX.value) : scrollX.value;
|
|
982
769
|
const virtualX = displayToVirtual(scrollValueX, componentOffset.x, scaleX.value);
|
|
983
770
|
const virtualY = displayToVirtual(scrollY.value, componentOffset.y, scaleY.value);
|
|
984
771
|
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
772
|
+
if (!isProgrammaticScroll.value) {
|
|
773
|
+
if (!isScrolling.value) {
|
|
774
|
+
scrollStartX = internalScrollX.value;
|
|
775
|
+
scrollStartY = internalScrollY.value;
|
|
776
|
+
}
|
|
777
|
+
const deltaX = virtualX - scrollStartX;
|
|
778
|
+
const deltaY = virtualY - scrollStartY;
|
|
779
|
+
|
|
780
|
+
if (Math.abs(deltaX) > 0.5) {
|
|
781
|
+
scrollDirectionX.value = deltaX > 0 ? 'end' : 'start';
|
|
782
|
+
}
|
|
783
|
+
if (Math.abs(deltaY) > 0.5) {
|
|
784
|
+
scrollDirectionY.value = deltaY > 0 ? 'end' : 'start';
|
|
785
|
+
}
|
|
992
786
|
}
|
|
993
787
|
|
|
994
788
|
internalScrollX.value = virtualX;
|
|
995
789
|
internalScrollY.value = virtualY;
|
|
996
|
-
|
|
997
790
|
if (!isProgrammaticScroll.value) {
|
|
998
791
|
pendingScroll.value = null;
|
|
999
792
|
}
|
|
1000
|
-
|
|
1001
793
|
if (!isScrolling.value) {
|
|
1002
794
|
isScrolling.value = true;
|
|
1003
795
|
}
|
|
796
|
+
extensions.forEach((ext) => ext.onScroll?.(ctx, e));
|
|
1004
797
|
clearTimeout(scrollTimeout);
|
|
1005
798
|
scrollTimeout = setTimeout(() => {
|
|
1006
|
-
const wasProgrammatic = isProgrammaticScroll.value;
|
|
1007
799
|
isScrolling.value = false;
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (props.value.snap && !wasProgrammatic) {
|
|
1012
|
-
const snapProp = props.value.snap;
|
|
1013
|
-
const snapMode = snapProp === true ? 'auto' : snapProp;
|
|
1014
|
-
const details = scrollDetails.value;
|
|
1015
|
-
const itemsLen = props.value.items.length;
|
|
1016
|
-
|
|
1017
|
-
let targetRow: number | null = details.currentIndex;
|
|
1018
|
-
let targetCol: number | null = details.currentColIndex;
|
|
1019
|
-
let alignY: ScrollAlignment = 'start';
|
|
1020
|
-
let alignX: ScrollAlignment = 'start';
|
|
1021
|
-
let shouldSnap = false;
|
|
1022
|
-
|
|
1023
|
-
// Handle Y Axis (Vertical)
|
|
1024
|
-
if (direction.value !== 'horizontal') {
|
|
1025
|
-
const res = resolveSnap(
|
|
1026
|
-
snapMode,
|
|
1027
|
-
scrollDirectionY,
|
|
1028
|
-
details.currentIndex,
|
|
1029
|
-
details.currentEndIndex,
|
|
1030
|
-
relativeScrollY.value,
|
|
1031
|
-
viewportHeight.value,
|
|
1032
|
-
itemsLen,
|
|
1033
|
-
(i) => itemSizesY.get(i),
|
|
1034
|
-
(i) => itemSizesY.query(i),
|
|
1035
|
-
getRowIndexAt,
|
|
1036
|
-
);
|
|
1037
|
-
if (res) {
|
|
1038
|
-
targetRow = res.index;
|
|
1039
|
-
alignY = res.align;
|
|
1040
|
-
shouldSnap = true;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Handle X Axis (Horizontal)
|
|
1045
|
-
if (direction.value !== 'vertical') {
|
|
1046
|
-
const isGrid = direction.value === 'both';
|
|
1047
|
-
const colCount = isGrid ? (props.value.columnCount || 0) : itemsLen;
|
|
1048
|
-
const res = resolveSnap(
|
|
1049
|
-
snapMode,
|
|
1050
|
-
scrollDirectionX,
|
|
1051
|
-
details.currentColIndex,
|
|
1052
|
-
details.currentEndColIndex,
|
|
1053
|
-
relativeScrollX.value,
|
|
1054
|
-
viewportWidth.value,
|
|
1055
|
-
colCount,
|
|
1056
|
-
(i) => (isGrid ? columnSizes.get(i) : itemSizesX.get(i)),
|
|
1057
|
-
(i) => (isGrid ? columnSizes.query(i) : itemSizesX.query(i)),
|
|
1058
|
-
getColIndexAt,
|
|
1059
|
-
);
|
|
1060
|
-
if (res) {
|
|
1061
|
-
targetCol = res.index;
|
|
1062
|
-
alignX = res.align;
|
|
1063
|
-
shouldSnap = true;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (shouldSnap) {
|
|
1068
|
-
scrollToIndex(targetRow, targetCol, {
|
|
1069
|
-
align: { x: alignX, y: alignY },
|
|
1070
|
-
behavior: 'smooth',
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
800
|
+
extensions.forEach((ext) => ext.onScrollEnd?.(ctx));
|
|
801
|
+
if (programmaticScrollTimer === undefined) {
|
|
802
|
+
isProgrammaticScroll.value = false;
|
|
1073
803
|
}
|
|
1074
|
-
},
|
|
804
|
+
}, 150);
|
|
1075
805
|
};
|
|
1076
806
|
|
|
1077
|
-
/**
|
|
1078
|
-
* Updates the size of multiple items in the Fenwick tree.
|
|
1079
|
-
*
|
|
1080
|
-
* @param updates - Array of updates
|
|
1081
|
-
*/
|
|
1082
807
|
const updateItemSizes = (updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement | undefined; }>) => {
|
|
1083
|
-
coreUpdateItemSizes(
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
relativeScrollY.value,
|
|
1089
|
-
(dx, dy) => {
|
|
1090
|
-
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1091
|
-
if (!hasPendingScroll) {
|
|
1092
|
-
handleScrollCorrection(dx, dy);
|
|
1093
|
-
}
|
|
1094
|
-
},
|
|
1095
|
-
);
|
|
808
|
+
coreUpdateItemSizes(updates, getRowIndexAt, getColIndexAt, relativeScrollX.value, relativeScrollY.value, (dx, dy) => {
|
|
809
|
+
if (!pendingScroll.value && !isProgrammaticScroll.value) {
|
|
810
|
+
handleScrollCorrection(dx, dy);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
1096
813
|
};
|
|
1097
814
|
|
|
1098
|
-
/**
|
|
1099
|
-
* Updates the size of a specific item in the Fenwick tree.
|
|
1100
|
-
*
|
|
1101
|
-
* @param index - Index of the item
|
|
1102
|
-
* @param inlineSize - New inlineSize
|
|
1103
|
-
* @param blockSize - New blockSize
|
|
1104
|
-
* @param element - The element that was measured (optional)
|
|
1105
|
-
*/
|
|
1106
815
|
const updateItemSize = (index: number, inlineSize: number, blockSize: number, element?: HTMLElement) => {
|
|
1107
816
|
updateItemSizes([ { index, inlineSize, blockSize, element } ]);
|
|
1108
817
|
};
|
|
1109
818
|
|
|
1110
|
-
// --- Scroll Queue / Correction Watchers ---
|
|
1111
819
|
function checkPendingScroll() {
|
|
1112
820
|
if (pendingScroll.value && !isHydrating.value) {
|
|
1113
821
|
const { rowIndex, colIndex, options } = pendingScroll.value;
|
|
1114
|
-
|
|
1115
822
|
const isSmooth = isScrollToIndexOptions(options) && options.behavior === 'smooth';
|
|
1116
|
-
|
|
1117
|
-
// If it's a smooth scroll, we wait until it's finished before correcting.
|
|
1118
|
-
if (isSmooth && isScrolling.value) {
|
|
823
|
+
if (isSmooth && (isScrolling.value || isProgrammaticScroll.value)) {
|
|
1119
824
|
return;
|
|
1120
825
|
}
|
|
1121
|
-
|
|
1122
826
|
const container = props.value.container || window;
|
|
1123
827
|
const actualScrollX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
1124
828
|
const actualScrollY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
1125
|
-
|
|
1126
829
|
const scrollValueX = isRtl.value ? Math.abs(actualScrollX) : actualScrollX;
|
|
1127
830
|
const scrollValueY = actualScrollY;
|
|
1128
|
-
|
|
1129
831
|
const currentRelX = displayToVirtual(scrollValueX, 0, scaleX.value);
|
|
1130
832
|
const currentRelY = displayToVirtual(scrollValueY, 0, scaleY.value);
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
colIndex,
|
|
1135
|
-
options,
|
|
1136
|
-
direction: direction.value,
|
|
1137
|
-
viewportWidth: viewportWidth.value,
|
|
1138
|
-
viewportHeight: viewportHeight.value,
|
|
1139
|
-
totalWidth: virtualWidth.value,
|
|
1140
|
-
totalHeight: virtualHeight.value,
|
|
1141
|
-
gap: props.value.gap || 0,
|
|
1142
|
-
columnGap: props.value.columnGap || 0,
|
|
1143
|
-
fixedSize: fixedItemSize.value,
|
|
1144
|
-
fixedWidth: fixedColumnWidth.value,
|
|
1145
|
-
relativeScrollX: currentRelX,
|
|
1146
|
-
relativeScrollY: currentRelY,
|
|
1147
|
-
getItemSizeY: (idx) => itemSizesY.get(idx),
|
|
1148
|
-
getItemSizeX: (idx) => itemSizesX.get(idx),
|
|
1149
|
-
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
1150
|
-
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
1151
|
-
getColumnSize: (idx) => columnSizes.get(idx),
|
|
1152
|
-
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
1153
|
-
scaleX: scaleX.value,
|
|
1154
|
-
scaleY: scaleY.value,
|
|
1155
|
-
hostOffsetX: componentOffset.x,
|
|
1156
|
-
hostOffsetY: componentOffset.y,
|
|
1157
|
-
stickyIndices: sortedStickyIndices.value,
|
|
1158
|
-
stickyStartX: stickyStartX.value,
|
|
1159
|
-
stickyStartY: stickyStartY.value,
|
|
1160
|
-
stickyEndX: stickyEndX.value,
|
|
1161
|
-
stickyEndY: stickyEndY.value,
|
|
1162
|
-
flowPaddingStartX: flowStartX.value,
|
|
1163
|
-
flowPaddingStartY: flowStartY.value,
|
|
1164
|
-
paddingStartX: paddingStartX.value,
|
|
1165
|
-
paddingStartY: paddingStartY.value,
|
|
1166
|
-
paddingEndX: paddingEndX.value,
|
|
1167
|
-
paddingEndY: paddingEndY.value,
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
const toleranceX = 2;
|
|
1171
|
-
const toleranceY = 2;
|
|
833
|
+
const { targetX, targetY } = calculateScrollTarget({ rowIndex, colIndex, options, direction: direction.value, viewportWidth: viewportWidth.value, viewportHeight: viewportHeight.value, totalWidth: virtualWidth.value, totalHeight: virtualHeight.value, gap: props.value.gap || 0, columnGap: props.value.columnGap || 0, fixedSize: fixedItemSize.value, fixedWidth: fixedColumnWidth.value, relativeScrollX: currentRelX, relativeScrollY: currentRelY, getItemSizeY: (idx) => itemSizesY.get(idx), getItemSizeX: (idx) => itemSizesX.get(idx), getItemQueryY: (idx) => itemSizesY.query(idx), getItemQueryX: (idx) => itemSizesX.query(idx), getColumnSize: (idx) => columnSizes.get(idx), getColumnQuery: (idx) => columnSizes.query(idx), scaleX: scaleX.value, scaleY: scaleY.value, hostOffsetX: componentOffset.x, hostOffsetY: componentOffset.y, stickyIndices: (props.value.stickyIndices || []), stickyStartX: stickyStartX.value, stickyStartY: stickyStartY.value, stickyEndX: stickyEndX.value, stickyEndY: stickyEndY.value, flowPaddingStartX: flowStartX.value, flowPaddingStartY: flowStartY.value, paddingStartX: paddingStartX.value, paddingStartY: paddingStartY.value, paddingEndX: paddingEndX.value, paddingEndY: paddingEndY.value });
|
|
834
|
+
const toleranceX = 2 * scaleX.value;
|
|
835
|
+
const toleranceY = 2 * scaleY.value;
|
|
1172
836
|
const reachedX = (colIndex === null || colIndex === undefined) || Math.abs(currentRelX - targetX) < toleranceX;
|
|
1173
837
|
const reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(currentRelY - targetY) < toleranceY;
|
|
1174
|
-
|
|
1175
|
-
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns.value[ colIndex ] === 1;
|
|
1176
|
-
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY.value[ rowIndex ] === 1;
|
|
1177
|
-
|
|
1178
838
|
if (reachedX && reachedY) {
|
|
839
|
+
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns.value[ colIndex ] === 1;
|
|
840
|
+
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY.value[ rowIndex ] === 1;
|
|
1179
841
|
if (isMeasuredX && isMeasuredY && !isScrolling.value && !isProgrammaticScroll.value) {
|
|
1180
842
|
pendingScroll.value = null;
|
|
1181
843
|
}
|
|
1182
844
|
} else {
|
|
1183
|
-
const correctionOptions: ScrollToIndexOptions = isScrollToIndexOptions(options)
|
|
1184
|
-
? { ...options, isCorrection: true }
|
|
1185
|
-
: { align: options as ScrollAlignment | ScrollAlignmentOptions, isCorrection: true };
|
|
845
|
+
const correctionOptions: ScrollToIndexOptions = isScrollToIndexOptions(options) ? { ...options, isCorrection: true } : { align: options as ScrollAlignment | ScrollAlignmentOptions, isCorrection: true };
|
|
1186
846
|
scrollToIndex(rowIndex, colIndex, correctionOptions);
|
|
1187
847
|
}
|
|
1188
848
|
}
|
|
1189
849
|
}
|
|
1190
850
|
|
|
1191
851
|
watch([ treeUpdateFlag, viewportWidth, viewportHeight ], checkPendingScroll);
|
|
1192
|
-
|
|
1193
852
|
watch(isScrolling, (scrolling) => {
|
|
1194
853
|
if (!scrolling) {
|
|
1195
854
|
checkPendingScroll();
|
|
1196
855
|
}
|
|
1197
856
|
});
|
|
1198
857
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
858
|
+
const updateHostOffset = () => {
|
|
859
|
+
if (typeof window === 'undefined') {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const container = props.value.container || window;
|
|
863
|
+
const calculateOffset = (el: HTMLElement) => {
|
|
864
|
+
const rect = el.getBoundingClientRect();
|
|
865
|
+
if (container === window) {
|
|
866
|
+
return {
|
|
867
|
+
x: isRtl.value ? document.documentElement.clientWidth - rect.right - window.scrollX : rect.left + window.scrollX,
|
|
868
|
+
y: rect.top + window.scrollY,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (container === el) {
|
|
872
|
+
return { x: 0, y: 0 };
|
|
873
|
+
}
|
|
874
|
+
if (isElement(container)) {
|
|
875
|
+
const containerRect = container.getBoundingClientRect();
|
|
876
|
+
return {
|
|
877
|
+
x: isRtl.value ? containerRect.right - rect.right - container.scrollLeft : rect.left - containerRect.left + container.scrollLeft,
|
|
878
|
+
y: rect.top - containerRect.top + container.scrollTop,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
return { x: 0, y: 0 };
|
|
882
|
+
};
|
|
883
|
+
if (props.value.hostElement) {
|
|
884
|
+
const newOffset = calculateOffset(props.value.hostElement);
|
|
885
|
+
if (Math.abs(hostOffset.x - newOffset.x) > 0.1 || Math.abs(hostOffset.y - newOffset.y) > 0.1) {
|
|
886
|
+
hostOffset.x = newOffset.x;
|
|
887
|
+
hostOffset.y = newOffset.y;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (props.value.hostRef) {
|
|
891
|
+
const newOffset = calculateOffset(props.value.hostRef);
|
|
892
|
+
if (Math.abs(hostRefOffset.x - newOffset.x) > 0.1 || Math.abs(hostRefOffset.y - newOffset.y) > 0.1) {
|
|
893
|
+
hostRefOffset.x = newOffset.x;
|
|
894
|
+
hostRefOffset.y = newOffset.y;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
};
|
|
1202
898
|
|
|
1203
899
|
const attachEvents = (container: HTMLElement | Window | null) => {
|
|
1204
900
|
if (typeof window === 'undefined') {
|
|
@@ -1206,25 +902,19 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1206
902
|
}
|
|
1207
903
|
const effectiveContainer = container || window;
|
|
1208
904
|
const scrollTarget = (effectiveContainer === window || (isElement(effectiveContainer) && effectiveContainer === document.documentElement)) ? document : effectiveContainer;
|
|
1209
|
-
|
|
1210
905
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
1211
|
-
|
|
1212
|
-
computedStyle = null;
|
|
1213
906
|
updateDirection();
|
|
1214
|
-
|
|
907
|
+
let directionObserver: MutationObserver | null = null;
|
|
1215
908
|
if (isElement(effectiveContainer)) {
|
|
1216
909
|
directionObserver = new MutationObserver(() => updateDirection());
|
|
1217
910
|
directionObserver.observe(effectiveContainer, { attributes: true, attributeFilter: [ 'dir', 'style' ] });
|
|
1218
911
|
}
|
|
1219
|
-
|
|
1220
|
-
directionInterval = setInterval(updateDirection, 1000);
|
|
1221
|
-
|
|
912
|
+
const directionInterval = setInterval(updateDirection, 1000);
|
|
1222
913
|
if (effectiveContainer === window) {
|
|
1223
914
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
1224
915
|
viewportHeight.value = document.documentElement.clientHeight;
|
|
1225
916
|
scrollX.value = window.scrollX;
|
|
1226
917
|
scrollY.value = window.scrollY;
|
|
1227
|
-
|
|
1228
918
|
const onResize = () => {
|
|
1229
919
|
updateDirection();
|
|
1230
920
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
@@ -1232,66 +922,51 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1232
922
|
updateHostOffset();
|
|
1233
923
|
};
|
|
1234
924
|
window.addEventListener('resize', onResize);
|
|
1235
|
-
|
|
1236
925
|
return () => {
|
|
1237
926
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1238
927
|
window.removeEventListener('resize', onResize);
|
|
1239
928
|
directionObserver?.disconnect();
|
|
1240
929
|
clearInterval(directionInterval);
|
|
1241
|
-
computedStyle = null;
|
|
1242
930
|
};
|
|
1243
931
|
} else {
|
|
1244
932
|
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1245
933
|
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1246
934
|
scrollX.value = (effectiveContainer as HTMLElement).scrollLeft;
|
|
1247
935
|
scrollY.value = (effectiveContainer as HTMLElement).scrollTop;
|
|
1248
|
-
|
|
1249
|
-
resizeObserver = new ResizeObserver(() => {
|
|
936
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1250
937
|
updateDirection();
|
|
1251
938
|
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1252
939
|
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1253
940
|
updateHostOffset();
|
|
1254
941
|
});
|
|
1255
942
|
resizeObserver.observe(effectiveContainer as HTMLElement);
|
|
1256
|
-
|
|
1257
943
|
return () => {
|
|
1258
944
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1259
|
-
resizeObserver
|
|
945
|
+
resizeObserver.disconnect();
|
|
1260
946
|
directionObserver?.disconnect();
|
|
1261
947
|
clearInterval(directionInterval);
|
|
1262
|
-
computedStyle = null;
|
|
1263
948
|
};
|
|
1264
949
|
}
|
|
1265
950
|
};
|
|
1266
951
|
|
|
1267
952
|
let cleanup: (() => void) | undefined;
|
|
1268
|
-
|
|
1269
953
|
if (getCurrentInstance()) {
|
|
1270
954
|
onMounted(() => {
|
|
1271
955
|
isMounted.value = true;
|
|
1272
956
|
updateDirection();
|
|
1273
|
-
|
|
1274
957
|
watch(() => props.value.container, (newContainer) => {
|
|
1275
958
|
cleanup?.();
|
|
1276
959
|
cleanup = attachEvents(newContainer || null);
|
|
1277
960
|
}, { immediate: true });
|
|
1278
|
-
|
|
1279
961
|
updateHostOffset();
|
|
1280
|
-
|
|
1281
|
-
// Ensure we have a layout cycle before considering it hydrated
|
|
1282
|
-
// and starting virtualization. This avoids issues with 0-size viewports.
|
|
1283
962
|
nextTick(() => {
|
|
1284
963
|
updateHostOffset();
|
|
1285
964
|
if (props.value.ssrRange || props.value.initialScrollIndex !== undefined) {
|
|
1286
|
-
const initialIndex = props.value.initialScrollIndex !== undefined
|
|
1287
|
-
? props.value.initialScrollIndex
|
|
1288
|
-
: props.value.ssrRange?.start;
|
|
965
|
+
const initialIndex = props.value.initialScrollIndex !== undefined ? props.value.initialScrollIndex : props.value.ssrRange?.start;
|
|
1289
966
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1290
|
-
|
|
1291
967
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1292
968
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1293
969
|
}
|
|
1294
|
-
|
|
1295
970
|
isHydrated.value = true;
|
|
1296
971
|
isHydrating.value = true;
|
|
1297
972
|
nextTick(() => {
|
|
@@ -1302,88 +977,81 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1302
977
|
}
|
|
1303
978
|
});
|
|
1304
979
|
});
|
|
1305
|
-
|
|
1306
980
|
onUnmounted(() => {
|
|
1307
981
|
cleanup?.();
|
|
1308
982
|
});
|
|
1309
983
|
}
|
|
1310
984
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
985
|
+
watch([
|
|
986
|
+
() => props.value.items,
|
|
987
|
+
() => props.value.items.length,
|
|
988
|
+
() => props.value.direction,
|
|
989
|
+
() => props.value.columnCount,
|
|
990
|
+
() => props.value.columnWidth,
|
|
991
|
+
() => props.value.itemSize,
|
|
992
|
+
() => props.value.gap,
|
|
993
|
+
() => props.value.columnGap,
|
|
994
|
+
() => props.value.defaultItemSize,
|
|
995
|
+
() => props.value.defaultColumnWidth,
|
|
996
|
+
], () => initializeSizes(), { immediate: true });
|
|
997
|
+
|
|
998
|
+
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
999
|
+
updateHostOffset();
|
|
1000
|
+
});
|
|
1001
|
+
watch(isRtl, (newRtl, oldRtl) => {
|
|
1002
|
+
if (oldRtl === undefined || newRtl === oldRtl || !isMounted.value) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (direction.value === 'vertical') {
|
|
1006
|
+
updateHostOffset();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const scrollValue = oldRtl ? Math.abs(scrollX.value) : scrollX.value;
|
|
1010
|
+
const oldRelativeScrollX = displayToVirtual(scrollValue, hostOffset.x, scaleX.value);
|
|
1011
|
+
updateHostOffset();
|
|
1012
|
+
scrollToOffset(oldRelativeScrollX, null, { behavior: 'auto' });
|
|
1013
|
+
}, { flush: 'sync' });
|
|
1014
|
+
|
|
1015
|
+
watch([ scaleX, scaleY ], () => {
|
|
1016
|
+
if (!isMounted.value || isScrolling.value || isProgrammaticScroll.value) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
watch([ () => props.value.items.length, () => props.value.columnCount ], ([ newLen, newColCount ], [ oldLen, oldColCount ]) => {
|
|
1023
|
+
nextTick(() => {
|
|
1024
|
+
const maxRelX = Math.max(0, totalWidth.value - viewportWidth.value);
|
|
1025
|
+
const maxRelY = Math.max(0, totalHeight.value - viewportHeight.value);
|
|
1026
|
+
if (internalScrollX.value > maxRelX || internalScrollY.value > maxRelY) {
|
|
1027
|
+
scrollToOffset(Math.min(internalScrollX.value, maxRelX), Math.min(internalScrollY.value, maxRelY), { behavior: 'auto' });
|
|
1028
|
+
} else if ((newLen !== oldLen && scaleY.value !== 1) || (newColCount !== oldColCount && scaleX.value !== 1)) {
|
|
1029
|
+
scrollToOffset(internalScrollX.value, internalScrollY.value, { behavior: 'auto' });
|
|
1030
|
+
}
|
|
1031
|
+
updateHostOffset();
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1321
1034
|
|
|
1322
1035
|
return {
|
|
1323
|
-
/**
|
|
1324
|
-
* Array of items currently rendered in the DOM with their calculated offsets and sizes.
|
|
1325
|
-
* Offsets are in Display Units (DU), sizes are in Virtual Units (VU).
|
|
1326
|
-
* @see RenderedItem
|
|
1327
|
-
*/
|
|
1036
|
+
/** Reactive list of items to render in the current viewport. */
|
|
1328
1037
|
renderedItems,
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* Total calculated width of all items including gaps (in VU).
|
|
1332
|
-
*/
|
|
1038
|
+
/** Total calculated width of the scrollable content area (DU). */
|
|
1333
1039
|
totalWidth,
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Total calculated height of all items including gaps (in VU).
|
|
1337
|
-
*/
|
|
1040
|
+
/** Total calculated height of the scrollable content area (DU). */
|
|
1338
1041
|
totalHeight,
|
|
1339
|
-
|
|
1340
|
-
/**
|
|
1341
|
-
* Total width to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1342
|
-
*/
|
|
1042
|
+
/** Physical width of the content in the DOM (clamped to browser limits). */
|
|
1343
1043
|
renderedWidth,
|
|
1344
|
-
|
|
1345
|
-
/**
|
|
1346
|
-
* Total height to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1347
|
-
*/
|
|
1044
|
+
/** Physical height of the content in the DOM (clamped to browser limits). */
|
|
1348
1045
|
renderedHeight,
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
* Includes currentIndex, scrollOffset (VU), displayScrollOffset (DU), viewportSize (DU), totalSize (VU), and scrolling status.
|
|
1353
|
-
* @see ScrollDetails
|
|
1354
|
-
*/
|
|
1355
|
-
scrollDetails,
|
|
1356
|
-
|
|
1357
|
-
/**
|
|
1358
|
-
* Helper to get the height of a specific row based on current configuration and measurements.
|
|
1359
|
-
*
|
|
1360
|
-
* @param index - The row index.
|
|
1361
|
-
* @returns The height in VU (excluding gap).
|
|
1362
|
-
*/
|
|
1046
|
+
/** Detailed information about the current scroll state. */
|
|
1047
|
+
scrollDetails: computedScrollDetails,
|
|
1048
|
+
/** Helper to get the height of a specific row. */
|
|
1363
1049
|
getRowHeight,
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Helper to get the width of a specific column based on current configuration and measurements.
|
|
1367
|
-
*
|
|
1368
|
-
* @param index - The column index.
|
|
1369
|
-
* @returns The width in VU (excluding gap).
|
|
1370
|
-
*/
|
|
1050
|
+
/** Helper to get the width of a specific column. */
|
|
1371
1051
|
getColumnWidth,
|
|
1372
|
-
|
|
1373
|
-
/**
|
|
1374
|
-
* Helper to get the virtual offset of a specific row.
|
|
1375
|
-
*
|
|
1376
|
-
* @param index - The row index.
|
|
1377
|
-
* @returns The virtual offset in VU.
|
|
1378
|
-
*/
|
|
1052
|
+
/** Helper to get the virtual offset of a specific row. */
|
|
1379
1053
|
getRowOffset: (index: number) => (flowStartY.value + stickyStartY.value + paddingStartY.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.gap || 0, (idx) => itemSizesY.query(idx)),
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* Helper to get the virtual offset of a specific column.
|
|
1383
|
-
*
|
|
1384
|
-
* @param index - The column index.
|
|
1385
|
-
* @returns The virtual offset in VU.
|
|
1386
|
-
*/
|
|
1054
|
+
/** Helper to get the virtual offset of a specific column. */
|
|
1387
1055
|
getColumnOffset: (index: number) => {
|
|
1388
1056
|
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
1389
1057
|
if (direction.value === 'both') {
|
|
@@ -1391,138 +1059,51 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1391
1059
|
}
|
|
1392
1060
|
return itemsStartVU_X + calculateOffsetAt(index, fixedItemSize.value, props.value.columnGap || 0, (idx) => itemSizesX.query(idx));
|
|
1393
1061
|
},
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
* @returns The virtual offset in VU.
|
|
1400
|
-
*/
|
|
1401
|
-
getItemOffset: (index: number) => (direction.value === 'horizontal' ? (flowStartX.value + stickyStartX.value + paddingStartX.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.columnGap || 0, (idx) => itemSizesX.query(idx)) : (flowStartY.value + stickyStartY.value + paddingStartY.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.gap || 0, (idx) => itemSizesY.query(idx))),
|
|
1402
|
-
|
|
1403
|
-
/**
|
|
1404
|
-
* Helper to get the size of a specific item along the scroll axis.
|
|
1405
|
-
*
|
|
1406
|
-
* @param index - The item index.
|
|
1407
|
-
* @returns The size in VU (excluding gap).
|
|
1408
|
-
*/
|
|
1409
|
-
getItemSize: (index: number) => (direction.value === 'horizontal'
|
|
1410
|
-
? getColumnWidth(index)
|
|
1411
|
-
: getRowHeight(index)),
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Programmatically scroll to a specific row and/or column.
|
|
1415
|
-
*
|
|
1416
|
-
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
1417
|
-
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
1418
|
-
* @param options - Alignment and behavior options.
|
|
1419
|
-
* @see ScrollAlignment
|
|
1420
|
-
* @see ScrollToIndexOptions
|
|
1421
|
-
*/
|
|
1062
|
+
/** Helper to get the virtual offset of a specific item. */
|
|
1063
|
+
getItemOffset,
|
|
1064
|
+
/** Helper to get the size of a specific item along the scroll axis. */
|
|
1065
|
+
getItemSize,
|
|
1066
|
+
/** Programmatically scroll to a specific row and/or column. */
|
|
1422
1067
|
scrollToIndex,
|
|
1423
|
-
|
|
1424
|
-
/**
|
|
1425
|
-
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
1426
|
-
*
|
|
1427
|
-
* @param x - The pixel offset to scroll to on the X axis (VU). Pass null to keep current position.
|
|
1428
|
-
* @param y - The pixel offset to scroll to on the Y axis (VU). Pass null to keep current position.
|
|
1429
|
-
* @param options - Scroll options (behavior).
|
|
1430
|
-
*/
|
|
1068
|
+
/** Programmatically scroll to a specific virtual pixel offset. */
|
|
1431
1069
|
scrollToOffset,
|
|
1432
|
-
|
|
1433
|
-
/**
|
|
1434
|
-
* Stops any currently active smooth scroll animation and clears pending corrections.
|
|
1435
|
-
*/
|
|
1070
|
+
/** Immediately stops any currently active smooth scroll animation and clears pending corrections. */
|
|
1436
1071
|
stopProgrammaticScroll,
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
*
|
|
1441
|
-
* @param index - The item index.
|
|
1442
|
-
* @param width - The measured inlineSize (width in DU).
|
|
1443
|
-
* @param height - The measured blockSize (height in DU).
|
|
1444
|
-
* @param element - The measured element (optional, used for robust grid column detection).
|
|
1445
|
-
*/
|
|
1072
|
+
/** Adjusts the scroll position to compensate for measurement changes. */
|
|
1073
|
+
handleScrollCorrection,
|
|
1074
|
+
/** Updates the size of a single item from measurements. */
|
|
1446
1075
|
updateItemSize,
|
|
1447
|
-
|
|
1448
|
-
/**
|
|
1449
|
-
* Updates the stored size of multiple items simultaneously.
|
|
1450
|
-
*
|
|
1451
|
-
* @param updates - Array of measurement updates (sizes in DU).
|
|
1452
|
-
*/
|
|
1076
|
+
/** Updates the size of multiple items from measurements. */
|
|
1453
1077
|
updateItemSizes,
|
|
1454
|
-
|
|
1455
|
-
/**
|
|
1456
|
-
* Recalculates the host element's offset relative to the scroll container.
|
|
1457
|
-
* Useful if the container or host moves without a resize event.
|
|
1458
|
-
*/
|
|
1078
|
+
/** Updates the physical offset of the component relative to its scroll container. */
|
|
1459
1079
|
updateHostOffset,
|
|
1460
|
-
|
|
1461
|
-
/**
|
|
1462
|
-
* Detects the current direction (LTR/RTL) of the scroll container.
|
|
1463
|
-
*/
|
|
1080
|
+
/** Detects the current direction (LTR/RTL) of the scroll container. */
|
|
1464
1081
|
updateDirection,
|
|
1465
|
-
|
|
1466
|
-
/**
|
|
1467
|
-
* Information about the current visible range of columns and their paddings.
|
|
1468
|
-
* @see ColumnRange
|
|
1469
|
-
*/
|
|
1082
|
+
/** Information about the currently visible range of columns. */
|
|
1470
1083
|
columnRange,
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
* Useful if item sizes have changed externally.
|
|
1475
|
-
*/
|
|
1476
|
-
refresh,
|
|
1477
|
-
|
|
1478
|
-
/**
|
|
1479
|
-
* Whether the component has finished its first client-side mount and hydration.
|
|
1480
|
-
*/
|
|
1084
|
+
/** Resets all dynamic measurements and re-initializes from current props. */
|
|
1085
|
+
refresh: () => coreRefresh(),
|
|
1086
|
+
/** Whether the component has finished its first client-side mount. */
|
|
1481
1087
|
isHydrated,
|
|
1482
|
-
|
|
1483
|
-
/**
|
|
1484
|
-
* Whether the container is the window or body.
|
|
1485
|
-
*/
|
|
1088
|
+
/** Whether the scroll container is the window object. */
|
|
1486
1089
|
isWindowContainer,
|
|
1487
|
-
|
|
1488
|
-
/**
|
|
1489
|
-
* Whether the scroll container is in Right-to-Left (RTL) mode.
|
|
1490
|
-
*/
|
|
1090
|
+
/** Whether the scroll container is in Right-to-Left (RTL) mode. */
|
|
1491
1091
|
isRtl,
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* Coordinate scaling factor for X axis (VU/DU).
|
|
1495
|
-
*/
|
|
1092
|
+
/** Coordinate scaling factor for X axis. */
|
|
1496
1093
|
scaleX,
|
|
1497
|
-
|
|
1498
|
-
/**
|
|
1499
|
-
* Coordinate scaling factor for Y axis (VU/DU).
|
|
1500
|
-
*/
|
|
1094
|
+
/** Coordinate scaling factor for Y axis. */
|
|
1501
1095
|
scaleY,
|
|
1502
|
-
|
|
1503
|
-
/**
|
|
1504
|
-
* Absolute offset of the component within its container (DU).
|
|
1505
|
-
*/
|
|
1096
|
+
/** Absolute offset of the component within its container. */
|
|
1506
1097
|
componentOffset,
|
|
1507
|
-
|
|
1508
|
-
/**
|
|
1509
|
-
* Physical width of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1510
|
-
*/
|
|
1098
|
+
/** Physical width of the virtualized content area (clamped). */
|
|
1511
1099
|
renderedVirtualWidth,
|
|
1512
|
-
|
|
1513
|
-
/**
|
|
1514
|
-
* Physical height of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1515
|
-
*/
|
|
1100
|
+
/** Physical height of the virtualized content area (clamped). */
|
|
1516
1101
|
renderedVirtualHeight,
|
|
1517
|
-
|
|
1518
|
-
/**
|
|
1519
|
-
* Helper to get the row index at a specific virtual offset.
|
|
1520
|
-
*/
|
|
1102
|
+
/** Helper to get the row (or item) index at a specific virtual offset (VU). */
|
|
1521
1103
|
getRowIndexAt,
|
|
1522
|
-
|
|
1523
|
-
/**
|
|
1524
|
-
* Helper to get the column index at a specific virtual offset.
|
|
1525
|
-
*/
|
|
1104
|
+
/** Helper to get the column index at a specific virtual offset (VU). */
|
|
1526
1105
|
getColIndexAt,
|
|
1106
|
+
/** @internal */
|
|
1107
|
+
__internalState: ctx.internalState,
|
|
1527
1108
|
};
|
|
1528
1109
|
}
|