@pdanpdan/virtual-scroll 0.9.0 → 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 +90 -12
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +174 -154
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +863 -742
- 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 +374 -800
- package/src/composables/useVirtualScrollSizes.ts +144 -142
- 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 +44 -2
|
@@ -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,58 +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
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
630
|
+
const lastItemsMap = new Map<number, RenderedItem<T>>();
|
|
631
|
+
for (const item of lastRenderedItems) {
|
|
632
|
+
lastItemsMap.set(item.index, item);
|
|
633
|
+
}
|
|
764
634
|
let lastIndexX = -1;
|
|
765
635
|
let lastOffsetX = 0;
|
|
766
636
|
let lastIndexY = -1;
|
|
767
637
|
let lastOffsetY = 0;
|
|
768
|
-
|
|
769
638
|
const queryXCached = (idx: number) => {
|
|
770
639
|
if (idx === lastIndexX + 1) {
|
|
771
640
|
lastOffsetX += itemSizesX.get(lastIndexX);
|
|
@@ -776,7 +645,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
776
645
|
lastIndexX = idx;
|
|
777
646
|
return lastOffsetX;
|
|
778
647
|
};
|
|
779
|
-
|
|
780
648
|
const queryYCached = (idx: number) => {
|
|
781
649
|
if (idx === lastIndexY + 1) {
|
|
782
650
|
lastOffsetY += itemSizesY.get(lastIndexY);
|
|
@@ -787,81 +655,30 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
787
655
|
lastIndexY = idx;
|
|
788
656
|
return lastOffsetY;
|
|
789
657
|
};
|
|
790
|
-
|
|
791
658
|
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
792
659
|
const itemsStartVU_Y = flowStartY.value + stickyStartY.value + paddingStartY.value;
|
|
793
660
|
const wrapperStartDU_X = flowStartX.value + stickyStartX.value;
|
|
794
661
|
const wrapperStartDU_Y = flowStartY.value + stickyStartY.value;
|
|
795
|
-
|
|
796
662
|
const colRange = columnRange.value;
|
|
797
|
-
|
|
663
|
+
let currentStickyIndexPtr = 0;
|
|
798
664
|
for (const i of sortedIndices) {
|
|
799
665
|
const item = props.value.items[ i ];
|
|
800
666
|
if (item === undefined) {
|
|
801
667
|
continue;
|
|
802
668
|
}
|
|
803
|
-
|
|
804
|
-
const { x, y, width, height } = calculateItemPosition({
|
|
805
|
-
index: i,
|
|
806
|
-
direction: direction.value,
|
|
807
|
-
fixedSize: fixedItemSize.value,
|
|
808
|
-
gap: props.value.gap || 0,
|
|
809
|
-
columnGap: props.value.columnGap || 0,
|
|
810
|
-
usableWidth: usableWidth.value,
|
|
811
|
-
usableHeight: usableHeight.value,
|
|
812
|
-
totalWidth: totalSize.value.width,
|
|
813
|
-
queryY: queryYCached,
|
|
814
|
-
queryX: queryXCached,
|
|
815
|
-
getSizeY: (idx) => itemSizesY.get(idx),
|
|
816
|
-
getSizeX: (idx) => itemSizesX.get(idx),
|
|
817
|
-
columnRange: colRange,
|
|
818
|
-
});
|
|
819
|
-
|
|
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 });
|
|
820
670
|
const isSticky = stickySet.has(i);
|
|
821
671
|
const originalX = x;
|
|
822
672
|
const originalY = y;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
originalX,
|
|
831
|
-
originalY,
|
|
832
|
-
width,
|
|
833
|
-
height,
|
|
834
|
-
stickyIndices,
|
|
835
|
-
fixedSize: fixedItemSize.value,
|
|
836
|
-
fixedWidth: fixedColumnWidth.value,
|
|
837
|
-
gap: props.value.gap || 0,
|
|
838
|
-
columnGap: props.value.columnGap || 0,
|
|
839
|
-
getItemQueryY: queryYCached,
|
|
840
|
-
getItemQueryX: queryXCached,
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
const offsetX = isHydrated.value
|
|
844
|
-
? (internalScrollX.value / scaleX.value + (x + itemsStartVU_X - internalScrollX.value)) - wrapperStartDU_X
|
|
845
|
-
: (x - ssrOffsetX);
|
|
846
|
-
const offsetY = isHydrated.value
|
|
847
|
-
? (internalScrollY.value / scaleY.value + (y + itemsStartVU_Y - internalScrollY.value)) - wrapperStartDU_Y
|
|
848
|
-
: (y - ssrOffsetY);
|
|
849
|
-
|
|
673
|
+
while (currentStickyIndexPtr < stickyIndices.length && stickyIndices[ currentStickyIndexPtr ]! <= i) {
|
|
674
|
+
currentStickyIndexPtr++;
|
|
675
|
+
}
|
|
676
|
+
const nextStickyIndex = currentStickyIndexPtr < stickyIndices.length ? stickyIndices[ currentStickyIndexPtr ] : undefined;
|
|
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);
|
|
850
680
|
const last = lastItemsMap.get(i);
|
|
851
|
-
if (
|
|
852
|
-
last
|
|
853
|
-
&& last.item === item
|
|
854
|
-
&& last.offset.x === offsetX
|
|
855
|
-
&& last.offset.y === offsetY
|
|
856
|
-
&& last.size.width === width
|
|
857
|
-
&& last.size.height === height
|
|
858
|
-
&& last.isSticky === isSticky
|
|
859
|
-
&& last.isStickyActive === isStickyActive
|
|
860
|
-
&& last.isStickyActiveX === isStickyActiveX
|
|
861
|
-
&& last.isStickyActiveY === isStickyActiveY
|
|
862
|
-
&& last.stickyOffset.x === stickyOffset.x
|
|
863
|
-
&& last.stickyOffset.y === stickyOffset.y
|
|
864
|
-
) {
|
|
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) {
|
|
865
682
|
items.push(last);
|
|
866
683
|
} else {
|
|
867
684
|
items.push({
|
|
@@ -879,53 +696,38 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
879
696
|
});
|
|
880
697
|
}
|
|
881
698
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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;
|
|
886
707
|
});
|
|
887
708
|
|
|
888
|
-
const
|
|
709
|
+
const computedScrollDetails = computed<ScrollDetails<T>>(() => {
|
|
889
710
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
890
711
|
treeUpdateFlag.value;
|
|
891
|
-
|
|
892
712
|
const currentScrollX = relativeScrollX.value + stickyStartX.value;
|
|
893
713
|
const currentScrollY = relativeScrollY.value + stickyStartY.value;
|
|
894
|
-
|
|
895
714
|
const currentEndScrollX = relativeScrollX.value + (viewportWidth.value - stickyEndX.value) - 1;
|
|
896
715
|
const currentEndScrollY = relativeScrollY.value + (viewportHeight.value - stickyEndY.value) - 1;
|
|
897
|
-
|
|
898
716
|
const currentColIndex = getColIndexAt(currentScrollX);
|
|
899
717
|
const currentRowIndex = getRowIndexAt(currentScrollY);
|
|
900
718
|
const currentEndIndex = getRowIndexAt(direction.value === 'horizontal' ? currentEndScrollX : currentEndScrollY);
|
|
901
719
|
const currentEndColIndex = getColIndexAt(currentEndScrollX);
|
|
902
|
-
|
|
903
720
|
return {
|
|
904
721
|
items: renderedItems.value,
|
|
905
722
|
currentIndex: currentRowIndex,
|
|
906
723
|
currentColIndex,
|
|
907
724
|
currentEndIndex,
|
|
908
725
|
currentEndColIndex,
|
|
909
|
-
scrollOffset: {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
},
|
|
913
|
-
|
|
914
|
-
x: isRtl.value ? Math.abs(scrollX.value + hostRefOffset.x) : Math.max(0, scrollX.value - hostRefOffset.x),
|
|
915
|
-
y: Math.max(0, scrollY.value - hostRefOffset.y),
|
|
916
|
-
},
|
|
917
|
-
viewportSize: {
|
|
918
|
-
width: viewportWidth.value,
|
|
919
|
-
height: viewportHeight.value,
|
|
920
|
-
},
|
|
921
|
-
displayViewportSize: {
|
|
922
|
-
width: viewportWidth.value,
|
|
923
|
-
height: viewportHeight.value,
|
|
924
|
-
},
|
|
925
|
-
totalSize: {
|
|
926
|
-
width: totalWidth.value,
|
|
927
|
-
height: totalHeight.value,
|
|
928
|
-
},
|
|
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 },
|
|
929
731
|
isScrolling: isScrolling.value,
|
|
930
732
|
isProgrammaticScroll: isProgrammaticScroll.value,
|
|
931
733
|
range: range.value,
|
|
@@ -933,26 +735,16 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
933
735
|
};
|
|
934
736
|
});
|
|
935
737
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
*/
|
|
940
|
-
const stopProgrammaticScroll = () => {
|
|
941
|
-
isProgrammaticScroll.value = false;
|
|
942
|
-
pendingScroll.value = null;
|
|
943
|
-
};
|
|
738
|
+
ctx.scrollDetails = computedScrollDetails;
|
|
739
|
+
|
|
740
|
+
extensions.forEach((ext) => ext.onInit?.(ctx));
|
|
944
741
|
|
|
945
|
-
/**
|
|
946
|
-
* Event handler for scroll events.
|
|
947
|
-
*/
|
|
948
742
|
const handleScroll = (e: Event) => {
|
|
949
743
|
const target = e.target;
|
|
950
744
|
if (typeof window === 'undefined') {
|
|
951
745
|
return;
|
|
952
746
|
}
|
|
953
|
-
|
|
954
747
|
updateDirection();
|
|
955
|
-
|
|
956
748
|
if (target === window || target === document) {
|
|
957
749
|
scrollX.value = window.scrollX;
|
|
958
750
|
scrollY.value = window.scrollY;
|
|
@@ -964,228 +756,127 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
964
756
|
viewportWidth.value = target.clientWidth;
|
|
965
757
|
viewportHeight.value = target.clientHeight;
|
|
966
758
|
}
|
|
967
|
-
|
|
968
759
|
const scrollValueX = isRtl.value ? Math.abs(scrollX.value) : scrollX.value;
|
|
969
760
|
const virtualX = displayToVirtual(scrollValueX, componentOffset.x, scaleX.value);
|
|
970
761
|
const virtualY = displayToVirtual(scrollY.value, componentOffset.y, scaleY.value);
|
|
971
|
-
|
|
972
762
|
if (Math.abs(virtualX - lastInternalX) > 0.5) {
|
|
973
|
-
scrollDirectionX = virtualX > lastInternalX ? 'end' : 'start';
|
|
763
|
+
scrollDirectionX.value = virtualX > lastInternalX ? 'end' : 'start';
|
|
974
764
|
lastInternalX = virtualX;
|
|
975
765
|
}
|
|
976
766
|
if (Math.abs(virtualY - lastInternalY) > 0.5) {
|
|
977
|
-
scrollDirectionY = virtualY > lastInternalY ? 'end' : 'start';
|
|
767
|
+
scrollDirectionY.value = virtualY > lastInternalY ? 'end' : 'start';
|
|
978
768
|
lastInternalY = virtualY;
|
|
979
769
|
}
|
|
980
|
-
|
|
981
770
|
internalScrollX.value = virtualX;
|
|
982
771
|
internalScrollY.value = virtualY;
|
|
983
|
-
|
|
984
772
|
if (!isProgrammaticScroll.value) {
|
|
985
773
|
pendingScroll.value = null;
|
|
986
774
|
}
|
|
987
|
-
|
|
988
775
|
if (!isScrolling.value) {
|
|
989
776
|
isScrolling.value = true;
|
|
990
777
|
}
|
|
778
|
+
extensions.forEach((ext) => ext.onScroll?.(ctx, e));
|
|
991
779
|
clearTimeout(scrollTimeout);
|
|
992
780
|
scrollTimeout = setTimeout(() => {
|
|
993
|
-
const wasProgrammatic = isProgrammaticScroll.value;
|
|
994
781
|
isScrolling.value = false;
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (props.value.snap && !wasProgrammatic) {
|
|
999
|
-
const snapProp = props.value.snap;
|
|
1000
|
-
const snapMode = snapProp === true ? 'auto' : snapProp;
|
|
1001
|
-
const details = scrollDetails.value;
|
|
1002
|
-
const itemsLen = props.value.items.length;
|
|
1003
|
-
|
|
1004
|
-
let targetRow: number | null = details.currentIndex;
|
|
1005
|
-
let targetCol: number | null = details.currentColIndex;
|
|
1006
|
-
let alignY: ScrollAlignment = 'start';
|
|
1007
|
-
let alignX: ScrollAlignment = 'start';
|
|
1008
|
-
let shouldSnap = false;
|
|
1009
|
-
|
|
1010
|
-
// Handle Y Axis (Vertical)
|
|
1011
|
-
if (direction.value !== 'horizontal') {
|
|
1012
|
-
const res = resolveSnap(
|
|
1013
|
-
snapMode,
|
|
1014
|
-
scrollDirectionY,
|
|
1015
|
-
details.currentIndex,
|
|
1016
|
-
details.currentEndIndex,
|
|
1017
|
-
relativeScrollY.value,
|
|
1018
|
-
viewportHeight.value,
|
|
1019
|
-
itemsLen,
|
|
1020
|
-
(i) => itemSizesY.get(i),
|
|
1021
|
-
(i) => itemSizesY.query(i),
|
|
1022
|
-
getRowIndexAt,
|
|
1023
|
-
);
|
|
1024
|
-
if (res) {
|
|
1025
|
-
targetRow = res.index;
|
|
1026
|
-
alignY = res.align;
|
|
1027
|
-
shouldSnap = true;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// Handle X Axis (Horizontal)
|
|
1032
|
-
if (direction.value !== 'vertical') {
|
|
1033
|
-
const isGrid = direction.value === 'both';
|
|
1034
|
-
const colCount = isGrid ? (props.value.columnCount || 0) : itemsLen;
|
|
1035
|
-
const res = resolveSnap(
|
|
1036
|
-
snapMode,
|
|
1037
|
-
scrollDirectionX,
|
|
1038
|
-
details.currentColIndex,
|
|
1039
|
-
details.currentEndColIndex,
|
|
1040
|
-
relativeScrollX.value,
|
|
1041
|
-
viewportWidth.value,
|
|
1042
|
-
colCount,
|
|
1043
|
-
(i) => (isGrid ? columnSizes.get(i) : itemSizesX.get(i)),
|
|
1044
|
-
(i) => (isGrid ? columnSizes.query(i) : itemSizesX.query(i)),
|
|
1045
|
-
getColIndexAt,
|
|
1046
|
-
);
|
|
1047
|
-
if (res) {
|
|
1048
|
-
targetCol = res.index;
|
|
1049
|
-
alignX = res.align;
|
|
1050
|
-
shouldSnap = true;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (shouldSnap) {
|
|
1055
|
-
scrollToIndex(targetRow, targetCol, {
|
|
1056
|
-
align: { x: alignX, y: alignY },
|
|
1057
|
-
behavior: 'smooth',
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
782
|
+
extensions.forEach((ext) => ext.onScrollEnd?.(ctx));
|
|
783
|
+
if (programmaticScrollTimer === undefined) {
|
|
784
|
+
isProgrammaticScroll.value = false;
|
|
1060
785
|
}
|
|
1061
|
-
},
|
|
786
|
+
}, 150);
|
|
1062
787
|
};
|
|
1063
788
|
|
|
1064
|
-
/**
|
|
1065
|
-
* Updates the size of multiple items in the Fenwick tree.
|
|
1066
|
-
*
|
|
1067
|
-
* @param updates - Array of updates
|
|
1068
|
-
*/
|
|
1069
789
|
const updateItemSizes = (updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement | undefined; }>) => {
|
|
1070
|
-
coreUpdateItemSizes(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
relativeScrollY.value,
|
|
1076
|
-
(dx, dy) => {
|
|
1077
|
-
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1078
|
-
if (!hasPendingScroll) {
|
|
1079
|
-
handleScrollCorrection(dx, dy);
|
|
1080
|
-
}
|
|
1081
|
-
},
|
|
1082
|
-
);
|
|
790
|
+
coreUpdateItemSizes(updates, getRowIndexAt, getColIndexAt, relativeScrollX.value, relativeScrollY.value, (dx, dy) => {
|
|
791
|
+
if (!pendingScroll.value && !isProgrammaticScroll.value) {
|
|
792
|
+
handleScrollCorrection(dx, dy);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
1083
795
|
};
|
|
1084
796
|
|
|
1085
|
-
/**
|
|
1086
|
-
* Updates the size of a specific item in the Fenwick tree.
|
|
1087
|
-
*
|
|
1088
|
-
* @param index - Index of the item
|
|
1089
|
-
* @param inlineSize - New inlineSize
|
|
1090
|
-
* @param blockSize - New blockSize
|
|
1091
|
-
* @param element - The element that was measured (optional)
|
|
1092
|
-
*/
|
|
1093
797
|
const updateItemSize = (index: number, inlineSize: number, blockSize: number, element?: HTMLElement) => {
|
|
1094
798
|
updateItemSizes([ { index, inlineSize, blockSize, element } ]);
|
|
1095
799
|
};
|
|
1096
800
|
|
|
1097
|
-
// --- Scroll Queue / Correction Watchers ---
|
|
1098
801
|
function checkPendingScroll() {
|
|
1099
802
|
if (pendingScroll.value && !isHydrating.value) {
|
|
1100
803
|
const { rowIndex, colIndex, options } = pendingScroll.value;
|
|
1101
|
-
|
|
1102
804
|
const isSmooth = isScrollToIndexOptions(options) && options.behavior === 'smooth';
|
|
1103
|
-
|
|
1104
|
-
// If it's a smooth scroll, we wait until it's finished before correcting.
|
|
1105
|
-
if (isSmooth && isScrolling.value) {
|
|
805
|
+
if (isSmooth && (isScrolling.value || isProgrammaticScroll.value)) {
|
|
1106
806
|
return;
|
|
1107
807
|
}
|
|
1108
|
-
|
|
1109
808
|
const container = props.value.container || window;
|
|
1110
809
|
const actualScrollX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
1111
810
|
const actualScrollY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
1112
|
-
|
|
1113
811
|
const scrollValueX = isRtl.value ? Math.abs(actualScrollX) : actualScrollX;
|
|
1114
812
|
const scrollValueY = actualScrollY;
|
|
1115
|
-
|
|
1116
813
|
const currentRelX = displayToVirtual(scrollValueX, 0, scaleX.value);
|
|
1117
814
|
const currentRelY = displayToVirtual(scrollValueY, 0, scaleY.value);
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
colIndex,
|
|
1122
|
-
options,
|
|
1123
|
-
direction: direction.value,
|
|
1124
|
-
viewportWidth: viewportWidth.value,
|
|
1125
|
-
viewportHeight: viewportHeight.value,
|
|
1126
|
-
totalWidth: virtualWidth.value,
|
|
1127
|
-
totalHeight: virtualHeight.value,
|
|
1128
|
-
gap: props.value.gap || 0,
|
|
1129
|
-
columnGap: props.value.columnGap || 0,
|
|
1130
|
-
fixedSize: fixedItemSize.value,
|
|
1131
|
-
fixedWidth: fixedColumnWidth.value,
|
|
1132
|
-
relativeScrollX: currentRelX,
|
|
1133
|
-
relativeScrollY: currentRelY,
|
|
1134
|
-
getItemSizeY: (idx) => itemSizesY.get(idx),
|
|
1135
|
-
getItemSizeX: (idx) => itemSizesX.get(idx),
|
|
1136
|
-
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
1137
|
-
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
1138
|
-
getColumnSize: (idx) => columnSizes.get(idx),
|
|
1139
|
-
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
1140
|
-
scaleX: scaleX.value,
|
|
1141
|
-
scaleY: scaleY.value,
|
|
1142
|
-
hostOffsetX: componentOffset.x,
|
|
1143
|
-
hostOffsetY: componentOffset.y,
|
|
1144
|
-
stickyIndices: sortedStickyIndices.value,
|
|
1145
|
-
stickyStartX: stickyStartX.value,
|
|
1146
|
-
stickyStartY: stickyStartY.value,
|
|
1147
|
-
stickyEndX: stickyEndX.value,
|
|
1148
|
-
stickyEndY: stickyEndY.value,
|
|
1149
|
-
flowPaddingStartX: flowStartX.value,
|
|
1150
|
-
flowPaddingStartY: flowStartY.value,
|
|
1151
|
-
paddingStartX: paddingStartX.value,
|
|
1152
|
-
paddingStartY: paddingStartY.value,
|
|
1153
|
-
paddingEndX: paddingEndX.value,
|
|
1154
|
-
paddingEndY: paddingEndY.value,
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
const toleranceX = 2;
|
|
1158
|
-
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;
|
|
1159
818
|
const reachedX = (colIndex === null || colIndex === undefined) || Math.abs(currentRelX - targetX) < toleranceX;
|
|
1160
819
|
const reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(currentRelY - targetY) < toleranceY;
|
|
1161
|
-
|
|
1162
|
-
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns.value[ colIndex ] === 1;
|
|
1163
|
-
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY.value[ rowIndex ] === 1;
|
|
1164
|
-
|
|
1165
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;
|
|
1166
823
|
if (isMeasuredX && isMeasuredY && !isScrolling.value && !isProgrammaticScroll.value) {
|
|
1167
824
|
pendingScroll.value = null;
|
|
1168
825
|
}
|
|
1169
826
|
} else {
|
|
1170
|
-
const correctionOptions: ScrollToIndexOptions = isScrollToIndexOptions(options)
|
|
1171
|
-
? { ...options, isCorrection: true }
|
|
1172
|
-
: { align: options as ScrollAlignment | ScrollAlignmentOptions, isCorrection: true };
|
|
827
|
+
const correctionOptions: ScrollToIndexOptions = isScrollToIndexOptions(options) ? { ...options, isCorrection: true } : { align: options as ScrollAlignment | ScrollAlignmentOptions, isCorrection: true };
|
|
1173
828
|
scrollToIndex(rowIndex, colIndex, correctionOptions);
|
|
1174
829
|
}
|
|
1175
830
|
}
|
|
1176
831
|
}
|
|
1177
832
|
|
|
1178
833
|
watch([ treeUpdateFlag, viewportWidth, viewportHeight ], checkPendingScroll);
|
|
1179
|
-
|
|
1180
834
|
watch(isScrolling, (scrolling) => {
|
|
1181
835
|
if (!scrolling) {
|
|
1182
836
|
checkPendingScroll();
|
|
1183
837
|
}
|
|
1184
838
|
});
|
|
1185
839
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
+
};
|
|
1189
880
|
|
|
1190
881
|
const attachEvents = (container: HTMLElement | Window | null) => {
|
|
1191
882
|
if (typeof window === 'undefined') {
|
|
@@ -1193,25 +884,19 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1193
884
|
}
|
|
1194
885
|
const effectiveContainer = container || window;
|
|
1195
886
|
const scrollTarget = (effectiveContainer === window || (isElement(effectiveContainer) && effectiveContainer === document.documentElement)) ? document : effectiveContainer;
|
|
1196
|
-
|
|
1197
887
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
1198
|
-
|
|
1199
|
-
computedStyle = null;
|
|
1200
888
|
updateDirection();
|
|
1201
|
-
|
|
889
|
+
let directionObserver: MutationObserver | null = null;
|
|
1202
890
|
if (isElement(effectiveContainer)) {
|
|
1203
891
|
directionObserver = new MutationObserver(() => updateDirection());
|
|
1204
892
|
directionObserver.observe(effectiveContainer, { attributes: true, attributeFilter: [ 'dir', 'style' ] });
|
|
1205
893
|
}
|
|
1206
|
-
|
|
1207
|
-
directionInterval = setInterval(updateDirection, 1000);
|
|
1208
|
-
|
|
894
|
+
const directionInterval = setInterval(updateDirection, 1000);
|
|
1209
895
|
if (effectiveContainer === window) {
|
|
1210
896
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
1211
897
|
viewportHeight.value = document.documentElement.clientHeight;
|
|
1212
898
|
scrollX.value = window.scrollX;
|
|
1213
899
|
scrollY.value = window.scrollY;
|
|
1214
|
-
|
|
1215
900
|
const onResize = () => {
|
|
1216
901
|
updateDirection();
|
|
1217
902
|
viewportWidth.value = document.documentElement.clientWidth;
|
|
@@ -1219,66 +904,51 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1219
904
|
updateHostOffset();
|
|
1220
905
|
};
|
|
1221
906
|
window.addEventListener('resize', onResize);
|
|
1222
|
-
|
|
1223
907
|
return () => {
|
|
1224
908
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1225
909
|
window.removeEventListener('resize', onResize);
|
|
1226
910
|
directionObserver?.disconnect();
|
|
1227
911
|
clearInterval(directionInterval);
|
|
1228
|
-
computedStyle = null;
|
|
1229
912
|
};
|
|
1230
913
|
} else {
|
|
1231
914
|
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1232
915
|
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1233
916
|
scrollX.value = (effectiveContainer as HTMLElement).scrollLeft;
|
|
1234
917
|
scrollY.value = (effectiveContainer as HTMLElement).scrollTop;
|
|
1235
|
-
|
|
1236
|
-
resizeObserver = new ResizeObserver(() => {
|
|
918
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1237
919
|
updateDirection();
|
|
1238
920
|
viewportWidth.value = (effectiveContainer as HTMLElement).clientWidth;
|
|
1239
921
|
viewportHeight.value = (effectiveContainer as HTMLElement).clientHeight;
|
|
1240
922
|
updateHostOffset();
|
|
1241
923
|
});
|
|
1242
924
|
resizeObserver.observe(effectiveContainer as HTMLElement);
|
|
1243
|
-
|
|
1244
925
|
return () => {
|
|
1245
926
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
|
1246
|
-
resizeObserver
|
|
927
|
+
resizeObserver.disconnect();
|
|
1247
928
|
directionObserver?.disconnect();
|
|
1248
929
|
clearInterval(directionInterval);
|
|
1249
|
-
computedStyle = null;
|
|
1250
930
|
};
|
|
1251
931
|
}
|
|
1252
932
|
};
|
|
1253
933
|
|
|
1254
934
|
let cleanup: (() => void) | undefined;
|
|
1255
|
-
|
|
1256
935
|
if (getCurrentInstance()) {
|
|
1257
936
|
onMounted(() => {
|
|
1258
937
|
isMounted.value = true;
|
|
1259
938
|
updateDirection();
|
|
1260
|
-
|
|
1261
939
|
watch(() => props.value.container, (newContainer) => {
|
|
1262
940
|
cleanup?.();
|
|
1263
941
|
cleanup = attachEvents(newContainer || null);
|
|
1264
942
|
}, { immediate: true });
|
|
1265
|
-
|
|
1266
943
|
updateHostOffset();
|
|
1267
|
-
|
|
1268
|
-
// Ensure we have a layout cycle before considering it hydrated
|
|
1269
|
-
// and starting virtualization. This avoids issues with 0-size viewports.
|
|
1270
944
|
nextTick(() => {
|
|
1271
945
|
updateHostOffset();
|
|
1272
946
|
if (props.value.ssrRange || props.value.initialScrollIndex !== undefined) {
|
|
1273
|
-
const initialIndex = props.value.initialScrollIndex !== undefined
|
|
1274
|
-
? props.value.initialScrollIndex
|
|
1275
|
-
: props.value.ssrRange?.start;
|
|
947
|
+
const initialIndex = props.value.initialScrollIndex !== undefined ? props.value.initialScrollIndex : props.value.ssrRange?.start;
|
|
1276
948
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1277
|
-
|
|
1278
949
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1279
950
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1280
951
|
}
|
|
1281
|
-
|
|
1282
952
|
isHydrated.value = true;
|
|
1283
953
|
isHydrating.value = true;
|
|
1284
954
|
nextTick(() => {
|
|
@@ -1289,88 +959,81 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1289
959
|
}
|
|
1290
960
|
});
|
|
1291
961
|
});
|
|
1292
|
-
|
|
1293
962
|
onUnmounted(() => {
|
|
1294
963
|
cleanup?.();
|
|
1295
964
|
});
|
|
1296
965
|
}
|
|
1297
966
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
+
});
|
|
1308
1016
|
|
|
1309
1017
|
return {
|
|
1310
|
-
/**
|
|
1311
|
-
* Array of items currently rendered in the DOM with their calculated offsets and sizes.
|
|
1312
|
-
* Offsets are in Display Units (DU), sizes are in Virtual Units (VU).
|
|
1313
|
-
* @see RenderedItem
|
|
1314
|
-
*/
|
|
1018
|
+
/** Reactive list of items to render in the current viewport. */
|
|
1315
1019
|
renderedItems,
|
|
1316
|
-
|
|
1317
|
-
/**
|
|
1318
|
-
* Total calculated width of all items including gaps (in VU).
|
|
1319
|
-
*/
|
|
1020
|
+
/** Total calculated width of the scrollable content area (DU). */
|
|
1320
1021
|
totalWidth,
|
|
1321
|
-
|
|
1322
|
-
/**
|
|
1323
|
-
* Total calculated height of all items including gaps (in VU).
|
|
1324
|
-
*/
|
|
1022
|
+
/** Total calculated height of the scrollable content area (DU). */
|
|
1325
1023
|
totalHeight,
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Total width to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1329
|
-
*/
|
|
1024
|
+
/** Physical width of the content in the DOM (clamped to browser limits). */
|
|
1330
1025
|
renderedWidth,
|
|
1331
|
-
|
|
1332
|
-
/**
|
|
1333
|
-
* Total height to be rendered in the DOM (clamped to browser limits, in DU).
|
|
1334
|
-
*/
|
|
1026
|
+
/** Physical height of the content in the DOM (clamped to browser limits). */
|
|
1335
1027
|
renderedHeight,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
* Includes currentIndex, scrollOffset (VU), displayScrollOffset (DU), viewportSize (DU), totalSize (VU), and scrolling status.
|
|
1340
|
-
* @see ScrollDetails
|
|
1341
|
-
*/
|
|
1342
|
-
scrollDetails,
|
|
1343
|
-
|
|
1344
|
-
/**
|
|
1345
|
-
* Helper to get the height of a specific row based on current configuration and measurements.
|
|
1346
|
-
*
|
|
1347
|
-
* @param index - The row index.
|
|
1348
|
-
* @returns The height in VU (excluding gap).
|
|
1349
|
-
*/
|
|
1028
|
+
/** Detailed information about the current scroll state. */
|
|
1029
|
+
scrollDetails: computedScrollDetails,
|
|
1030
|
+
/** Helper to get the height of a specific row. */
|
|
1350
1031
|
getRowHeight,
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Helper to get the width of a specific column based on current configuration and measurements.
|
|
1354
|
-
*
|
|
1355
|
-
* @param index - The column index.
|
|
1356
|
-
* @returns The width in VU (excluding gap).
|
|
1357
|
-
*/
|
|
1032
|
+
/** Helper to get the width of a specific column. */
|
|
1358
1033
|
getColumnWidth,
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* Helper to get the virtual offset of a specific row.
|
|
1362
|
-
*
|
|
1363
|
-
* @param index - The row index.
|
|
1364
|
-
* @returns The virtual offset in VU.
|
|
1365
|
-
*/
|
|
1034
|
+
/** Helper to get the virtual offset of a specific row. */
|
|
1366
1035
|
getRowOffset: (index: number) => (flowStartY.value + stickyStartY.value + paddingStartY.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.gap || 0, (idx) => itemSizesY.query(idx)),
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* Helper to get the virtual offset of a specific column.
|
|
1370
|
-
*
|
|
1371
|
-
* @param index - The column index.
|
|
1372
|
-
* @returns The virtual offset in VU.
|
|
1373
|
-
*/
|
|
1036
|
+
/** Helper to get the virtual offset of a specific column. */
|
|
1374
1037
|
getColumnOffset: (index: number) => {
|
|
1375
1038
|
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
1376
1039
|
if (direction.value === 'both') {
|
|
@@ -1378,138 +1041,49 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1378
1041
|
}
|
|
1379
1042
|
return itemsStartVU_X + calculateOffsetAt(index, fixedItemSize.value, props.value.columnGap || 0, (idx) => itemSizesX.query(idx));
|
|
1380
1043
|
},
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
* @returns The virtual offset in VU.
|
|
1387
|
-
*/
|
|
1388
|
-
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))),
|
|
1389
|
-
|
|
1390
|
-
/**
|
|
1391
|
-
* Helper to get the size of a specific item along the scroll axis.
|
|
1392
|
-
*
|
|
1393
|
-
* @param index - The item index.
|
|
1394
|
-
* @returns The size in VU (excluding gap).
|
|
1395
|
-
*/
|
|
1396
|
-
getItemSize: (index: number) => (direction.value === 'horizontal'
|
|
1397
|
-
? getColumnWidth(index)
|
|
1398
|
-
: getRowHeight(index)),
|
|
1399
|
-
|
|
1400
|
-
/**
|
|
1401
|
-
* Programmatically scroll to a specific row and/or column.
|
|
1402
|
-
*
|
|
1403
|
-
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
1404
|
-
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
1405
|
-
* @param options - Alignment and behavior options.
|
|
1406
|
-
* @see ScrollAlignment
|
|
1407
|
-
* @see ScrollToIndexOptions
|
|
1408
|
-
*/
|
|
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. */
|
|
1409
1049
|
scrollToIndex,
|
|
1410
|
-
|
|
1411
|
-
/**
|
|
1412
|
-
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
1413
|
-
*
|
|
1414
|
-
* @param x - The pixel offset to scroll to on the X axis (VU). Pass null to keep current position.
|
|
1415
|
-
* @param y - The pixel offset to scroll to on the Y axis (VU). Pass null to keep current position.
|
|
1416
|
-
* @param options - Scroll options (behavior).
|
|
1417
|
-
*/
|
|
1050
|
+
/** Programmatically scroll to a specific virtual pixel offset. */
|
|
1418
1051
|
scrollToOffset,
|
|
1419
|
-
|
|
1420
|
-
/**
|
|
1421
|
-
* Stops any currently active smooth scroll animation and clears pending corrections.
|
|
1422
|
-
*/
|
|
1052
|
+
/** Immediately stops any currently active smooth scroll animation and clears pending corrections. */
|
|
1423
1053
|
stopProgrammaticScroll,
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
*
|
|
1428
|
-
* @param index - The item index.
|
|
1429
|
-
* @param width - The measured inlineSize (width in DU).
|
|
1430
|
-
* @param height - The measured blockSize (height in DU).
|
|
1431
|
-
* @param element - The measured element (optional, used for robust grid column detection).
|
|
1432
|
-
*/
|
|
1054
|
+
/** Adjusts the scroll position to compensate for measurement changes. */
|
|
1055
|
+
handleScrollCorrection,
|
|
1056
|
+
/** Updates the size of a single item from measurements. */
|
|
1433
1057
|
updateItemSize,
|
|
1434
|
-
|
|
1435
|
-
/**
|
|
1436
|
-
* Updates the stored size of multiple items simultaneously.
|
|
1437
|
-
*
|
|
1438
|
-
* @param updates - Array of measurement updates (sizes in DU).
|
|
1439
|
-
*/
|
|
1058
|
+
/** Updates the size of multiple items from measurements. */
|
|
1440
1059
|
updateItemSizes,
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* Recalculates the host element's offset relative to the scroll container.
|
|
1444
|
-
* Useful if the container or host moves without a resize event.
|
|
1445
|
-
*/
|
|
1060
|
+
/** Updates the physical offset of the component relative to its scroll container. */
|
|
1446
1061
|
updateHostOffset,
|
|
1447
|
-
|
|
1448
|
-
/**
|
|
1449
|
-
* Detects the current direction (LTR/RTL) of the scroll container.
|
|
1450
|
-
*/
|
|
1062
|
+
/** Detects the current direction (LTR/RTL) of the scroll container. */
|
|
1451
1063
|
updateDirection,
|
|
1452
|
-
|
|
1453
|
-
/**
|
|
1454
|
-
* Information about the current visible range of columns and their paddings.
|
|
1455
|
-
* @see ColumnRange
|
|
1456
|
-
*/
|
|
1064
|
+
/** Information about the currently visible range of columns. */
|
|
1457
1065
|
columnRange,
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
* Useful if item sizes have changed externally.
|
|
1462
|
-
*/
|
|
1463
|
-
refresh,
|
|
1464
|
-
|
|
1465
|
-
/**
|
|
1466
|
-
* Whether the component has finished its first client-side mount and hydration.
|
|
1467
|
-
*/
|
|
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. */
|
|
1468
1069
|
isHydrated,
|
|
1469
|
-
|
|
1470
|
-
/**
|
|
1471
|
-
* Whether the container is the window or body.
|
|
1472
|
-
*/
|
|
1070
|
+
/** Whether the scroll container is the window object. */
|
|
1473
1071
|
isWindowContainer,
|
|
1474
|
-
|
|
1475
|
-
/**
|
|
1476
|
-
* Whether the scroll container is in Right-to-Left (RTL) mode.
|
|
1477
|
-
*/
|
|
1072
|
+
/** Whether the scroll container is in Right-to-Left (RTL) mode. */
|
|
1478
1073
|
isRtl,
|
|
1479
|
-
|
|
1480
|
-
/**
|
|
1481
|
-
* Coordinate scaling factor for X axis (VU/DU).
|
|
1482
|
-
*/
|
|
1074
|
+
/** Coordinate scaling factor for X axis. */
|
|
1483
1075
|
scaleX,
|
|
1484
|
-
|
|
1485
|
-
/**
|
|
1486
|
-
* Coordinate scaling factor for Y axis (VU/DU).
|
|
1487
|
-
*/
|
|
1076
|
+
/** Coordinate scaling factor for Y axis. */
|
|
1488
1077
|
scaleY,
|
|
1489
|
-
|
|
1490
|
-
/**
|
|
1491
|
-
* Absolute offset of the component within its container (DU).
|
|
1492
|
-
*/
|
|
1078
|
+
/** Absolute offset of the component within its container. */
|
|
1493
1079
|
componentOffset,
|
|
1494
|
-
|
|
1495
|
-
/**
|
|
1496
|
-
* Physical width of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1497
|
-
*/
|
|
1080
|
+
/** Physical width of the virtualized content area (clamped). */
|
|
1498
1081
|
renderedVirtualWidth,
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* Physical height of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1502
|
-
*/
|
|
1082
|
+
/** Physical height of the virtualized content area (clamped). */
|
|
1503
1083
|
renderedVirtualHeight,
|
|
1504
|
-
|
|
1505
|
-
/**
|
|
1506
|
-
* Helper to get the row index at a specific virtual offset.
|
|
1507
|
-
*/
|
|
1084
|
+
/** Helper to get the row (or item) index at a specific virtual offset (VU). */
|
|
1508
1085
|
getRowIndexAt,
|
|
1509
|
-
|
|
1510
|
-
/**
|
|
1511
|
-
* Helper to get the column index at a specific virtual offset.
|
|
1512
|
-
*/
|
|
1086
|
+
/** Helper to get the column index at a specific virtual offset (VU). */
|
|
1513
1087
|
getColIndexAt,
|
|
1514
1088
|
};
|
|
1515
1089
|
}
|