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