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