@pdanpdan/virtual-scroll 0.7.0 → 0.9.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 +99 -6
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +295 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +954 -878
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +7 -2
- package/src/components/VirtualScroll.vue +301 -209
- package/src/components/VirtualScrollbar.vue +8 -8
- package/src/composables/useVirtualScroll.ts +261 -521
- package/src/composables/useVirtualScrollSizes.ts +448 -0
- package/src/composables/useVirtualScrollbar.ts +30 -35
- package/src/index.ts +1 -0
- package/src/types.ts +25 -2
- package/src/utils/scroll.ts +14 -14
- package/src/utils/virtual-scroll-logic.ts +305 -1
|
@@ -17,19 +17,26 @@ import {
|
|
|
17
17
|
DEFAULT_COLUMN_WIDTH,
|
|
18
18
|
DEFAULT_ITEM_SIZE,
|
|
19
19
|
} from '../types';
|
|
20
|
-
import {
|
|
21
|
-
import { BROWSER_MAX_SIZE, getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions, isWindowLike, scrollTo } from '../utils/scroll';
|
|
20
|
+
import { getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions, isWindowLike, scrollTo } from '../utils/scroll';
|
|
22
21
|
import {
|
|
23
22
|
calculateColumnRange,
|
|
23
|
+
calculateIndexAt,
|
|
24
24
|
calculateItemPosition,
|
|
25
|
+
calculateOffsetAt,
|
|
25
26
|
calculateRange,
|
|
27
|
+
calculateRangeSize,
|
|
28
|
+
calculateRenderedSize,
|
|
29
|
+
calculateScale,
|
|
26
30
|
calculateScrollTarget,
|
|
31
|
+
calculateSSROffsets,
|
|
27
32
|
calculateStickyItem,
|
|
28
33
|
calculateTotalSize,
|
|
29
34
|
displayToVirtual,
|
|
30
35
|
findPrevStickyIndex,
|
|
36
|
+
resolveSnap,
|
|
31
37
|
virtualToDisplay,
|
|
32
38
|
} from '../utils/virtual-scroll-logic';
|
|
39
|
+
import { useVirtualScrollSizes } from './useVirtualScrollSizes';
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* Composable for virtual scrolling logic.
|
|
@@ -58,6 +65,14 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
58
65
|
const isProgrammaticScroll = ref(false);
|
|
59
66
|
const internalScrollX = ref(0);
|
|
60
67
|
const internalScrollY = ref(0);
|
|
68
|
+
/** The last recorded virtual X position, used to detect scroll direction for snapping. */
|
|
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;
|
|
61
76
|
|
|
62
77
|
let computedStyle: CSSStyleDeclaration | null = null;
|
|
63
78
|
|
|
@@ -71,9 +86,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
71
86
|
const container = props.value.container || props.value.hostRef || window;
|
|
72
87
|
const el = isElement(container) ? container : document.documentElement;
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
computedStyle = window.getComputedStyle(el);
|
|
76
|
-
}
|
|
89
|
+
computedStyle = window.getComputedStyle(el);
|
|
77
90
|
|
|
78
91
|
const newRtl = computedStyle.direction === 'rtl';
|
|
79
92
|
if (isRtl.value !== newRtl) {
|
|
@@ -81,28 +94,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
81
94
|
}
|
|
82
95
|
};
|
|
83
96
|
|
|
84
|
-
// --- Fenwick Trees for efficient size and offset management ---
|
|
85
|
-
const itemSizesX = new FenwickTree(props.value.items?.length || 0);
|
|
86
|
-
const itemSizesY = new FenwickTree(props.value.items?.length || 0);
|
|
87
|
-
const columnSizes = new FenwickTree(props.value.columnCount || 0);
|
|
88
|
-
|
|
89
|
-
const treeUpdateFlag = ref(0);
|
|
90
|
-
|
|
91
|
-
let measuredColumns = new Uint8Array(0);
|
|
92
|
-
let measuredItemsX = new Uint8Array(0);
|
|
93
|
-
let measuredItemsY = new Uint8Array(0);
|
|
94
|
-
|
|
95
|
-
// --- Scroll Queue / Correction ---
|
|
96
|
-
const pendingScroll = ref<{
|
|
97
|
-
rowIndex: number | null | undefined;
|
|
98
|
-
colIndex: number | null | undefined;
|
|
99
|
-
options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
100
|
-
} | null>(null);
|
|
101
|
-
|
|
102
|
-
// Track if sizes are initialized
|
|
103
|
-
const sizesInitialized = ref(false);
|
|
104
|
-
let lastItems: T[] = [];
|
|
105
|
-
|
|
106
97
|
// --- Computed Config ---
|
|
107
98
|
const direction = computed(() => [ 'vertical', 'horizontal', 'both' ].includes(props.value.direction as string) ? props.value.direction as ScrollDirection : 'vertical' as ScrollDirection);
|
|
108
99
|
|
|
@@ -124,6 +115,34 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
124
115
|
|
|
125
116
|
const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
|
|
126
117
|
|
|
118
|
+
// --- Size Management ---
|
|
119
|
+
const {
|
|
120
|
+
itemSizesX,
|
|
121
|
+
itemSizesY,
|
|
122
|
+
columnSizes,
|
|
123
|
+
measuredColumns,
|
|
124
|
+
measuredItemsY,
|
|
125
|
+
treeUpdateFlag,
|
|
126
|
+
getSizeAt,
|
|
127
|
+
initializeSizes,
|
|
128
|
+
updateItemSizes: coreUpdateItemSizes,
|
|
129
|
+
refresh: coreRefresh,
|
|
130
|
+
} = useVirtualScrollSizes(computed(() => ({
|
|
131
|
+
props: props.value,
|
|
132
|
+
isDynamicItemSize: isDynamicItemSize.value,
|
|
133
|
+
isDynamicColumnWidth: isDynamicColumnWidth.value,
|
|
134
|
+
defaultSize: defaultSize.value,
|
|
135
|
+
fixedItemSize: fixedItemSize.value,
|
|
136
|
+
direction: direction.value,
|
|
137
|
+
})));
|
|
138
|
+
|
|
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
|
+
|
|
127
146
|
const sortedStickyIndices = computed(() =>
|
|
128
147
|
[ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
|
|
129
148
|
);
|
|
@@ -157,55 +176,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
157
176
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
158
177
|
treeUpdateFlag.value;
|
|
159
178
|
|
|
160
|
-
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
161
|
-
const { start = 0, end = 0, colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
162
|
-
const colCount = props.value.columnCount || 0;
|
|
163
|
-
const gap = props.value.gap || 0;
|
|
164
|
-
const columnGap = props.value.columnGap || 0;
|
|
165
|
-
|
|
166
|
-
let width = 0;
|
|
167
|
-
let height = 0;
|
|
168
|
-
|
|
169
|
-
if (direction.value === 'both') {
|
|
170
|
-
if (colCount > 0) {
|
|
171
|
-
const effectiveColEnd = colEnd || colCount;
|
|
172
|
-
const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
|
|
173
|
-
width = Math.max(0, total - (effectiveColEnd > colStart ? columnGap : 0));
|
|
174
|
-
}
|
|
175
|
-
if (fixedItemSize.value !== null) {
|
|
176
|
-
const len = end - start;
|
|
177
|
-
height = Math.max(0, len * (fixedItemSize.value + gap) - (len > 0 ? gap : 0));
|
|
178
|
-
} else {
|
|
179
|
-
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
180
|
-
height = Math.max(0, total - (end > start ? gap : 0));
|
|
181
|
-
}
|
|
182
|
-
} else if (direction.value === 'horizontal') {
|
|
183
|
-
if (fixedItemSize.value !== null) {
|
|
184
|
-
const len = end - start;
|
|
185
|
-
width = Math.max(0, len * (fixedItemSize.value + columnGap) - (len > 0 ? columnGap : 0));
|
|
186
|
-
} else {
|
|
187
|
-
const total = itemSizesX.query(end) - itemSizesX.query(start);
|
|
188
|
-
width = Math.max(0, total - (end > start ? columnGap : 0));
|
|
189
|
-
}
|
|
190
|
-
height = usableHeight.value;
|
|
191
|
-
} else {
|
|
192
|
-
// vertical
|
|
193
|
-
width = usableWidth.value;
|
|
194
|
-
if (fixedItemSize.value !== null) {
|
|
195
|
-
const len = end - start;
|
|
196
|
-
height = Math.max(0, len * (fixedItemSize.value + gap) - (len > 0 ? gap : 0));
|
|
197
|
-
} else {
|
|
198
|
-
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
199
|
-
height = Math.max(0, total - (end > start ? gap : 0));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
width: Math.max(width, usableWidth.value),
|
|
205
|
-
height: Math.max(height, usableHeight.value),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
179
|
return calculateTotalSize({
|
|
210
180
|
direction: direction.value,
|
|
211
181
|
itemsLength: props.value.items.length,
|
|
@@ -236,29 +206,14 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
236
206
|
y: computed(() => Math.max(0, hostOffset.y - (flowStartY.value + stickyStartY.value))),
|
|
237
207
|
});
|
|
238
208
|
|
|
239
|
-
const renderedWidth = computed(() => (isWindowContainer.value
|
|
240
|
-
const renderedHeight = computed(() => (isWindowContainer.value
|
|
209
|
+
const renderedWidth = computed(() => calculateRenderedSize(isWindowContainer.value, totalWidth.value));
|
|
210
|
+
const renderedHeight = computed(() => calculateRenderedSize(isWindowContainer.value, totalHeight.value));
|
|
241
211
|
|
|
242
|
-
const renderedVirtualWidth = computed(() => (isWindowContainer.value
|
|
243
|
-
const renderedVirtualHeight = computed(() => (isWindowContainer.value
|
|
212
|
+
const renderedVirtualWidth = computed(() => calculateRenderedSize(isWindowContainer.value, virtualWidth.value));
|
|
213
|
+
const renderedVirtualHeight = computed(() => calculateRenderedSize(isWindowContainer.value, virtualHeight.value));
|
|
244
214
|
|
|
245
|
-
const scaleX = computed(() =>
|
|
246
|
-
|
|
247
|
-
return 1;
|
|
248
|
-
}
|
|
249
|
-
const realRange = totalWidth.value - viewportWidth.value;
|
|
250
|
-
const displayRange = renderedWidth.value - viewportWidth.value;
|
|
251
|
-
return displayRange > 0 ? realRange / displayRange : 1;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const scaleY = computed(() => {
|
|
255
|
-
if (isWindowContainer.value || totalHeight.value <= BROWSER_MAX_SIZE) {
|
|
256
|
-
return 1;
|
|
257
|
-
}
|
|
258
|
-
const realRange = totalHeight.value - viewportHeight.value;
|
|
259
|
-
const displayRange = renderedHeight.value - viewportHeight.value;
|
|
260
|
-
return displayRange > 0 ? realRange / displayRange : 1;
|
|
261
|
-
});
|
|
215
|
+
const scaleX = computed(() => calculateScale(isWindowContainer.value, totalWidth.value, viewportWidth.value));
|
|
216
|
+
const scaleY = computed(() => calculateScale(isWindowContainer.value, totalHeight.value, viewportHeight.value));
|
|
262
217
|
|
|
263
218
|
const relativeScrollX = computed(() => {
|
|
264
219
|
if (direction.value === 'vertical') {
|
|
@@ -283,23 +238,24 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
283
238
|
* @returns The width in pixels (excluding gap).
|
|
284
239
|
*/
|
|
285
240
|
const getColumnWidth = (index: number) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const val = cw[ index % cw.length ];
|
|
296
|
-
return (val != null && val > 0) ? val : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
|
|
297
|
-
}
|
|
298
|
-
if (typeof cw === 'function') {
|
|
299
|
-
return cw(index);
|
|
241
|
+
if (direction.value === 'both') {
|
|
242
|
+
return getSizeAt(
|
|
243
|
+
index,
|
|
244
|
+
props.value.columnWidth,
|
|
245
|
+
props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH,
|
|
246
|
+
props.value.columnGap || 0,
|
|
247
|
+
columnSizes,
|
|
248
|
+
true,
|
|
249
|
+
);
|
|
300
250
|
}
|
|
301
|
-
|
|
302
|
-
|
|
251
|
+
return getSizeAt(
|
|
252
|
+
index,
|
|
253
|
+
props.value.itemSize,
|
|
254
|
+
props.value.defaultItemSize || DEFAULT_ITEM_SIZE,
|
|
255
|
+
props.value.columnGap || 0,
|
|
256
|
+
itemSizesX,
|
|
257
|
+
true,
|
|
258
|
+
);
|
|
303
259
|
};
|
|
304
260
|
|
|
305
261
|
/**
|
|
@@ -309,39 +265,32 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
309
265
|
* @returns The height in pixels (excluding gap).
|
|
310
266
|
*/
|
|
311
267
|
const getRowHeight = (index: number) => {
|
|
312
|
-
// eslint-disable-next-line ts/no-unused-expressions
|
|
313
|
-
treeUpdateFlag.value;
|
|
314
|
-
|
|
315
268
|
if (direction.value === 'horizontal') {
|
|
316
269
|
return usableHeight.value;
|
|
317
270
|
}
|
|
318
271
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const val = itemSizesY.get(index);
|
|
330
|
-
return val > 0 ? val - gap : (props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
|
|
272
|
+
return getSizeAt(
|
|
273
|
+
index,
|
|
274
|
+
props.value.itemSize,
|
|
275
|
+
props.value.defaultItemSize || DEFAULT_ITEM_SIZE,
|
|
276
|
+
props.value.gap || 0,
|
|
277
|
+
itemSizesY,
|
|
278
|
+
false,
|
|
279
|
+
);
|
|
331
280
|
};
|
|
332
281
|
|
|
333
282
|
// --- Public Scroll API ---
|
|
334
283
|
/**
|
|
335
284
|
* Scrolls to a specific row and column index.
|
|
336
285
|
*
|
|
337
|
-
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
338
|
-
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
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.
|
|
339
288
|
* @param options - Scroll options including alignment ('start', 'center', 'end', 'auto') and behavior ('auto', 'smooth').
|
|
340
289
|
* Defaults to { align: 'auto', behavior: 'auto' }.
|
|
341
290
|
*/
|
|
342
291
|
function scrollToIndex(
|
|
343
|
-
rowIndex
|
|
344
|
-
colIndex
|
|
292
|
+
rowIndex?: number | null,
|
|
293
|
+
colIndex?: number | null,
|
|
345
294
|
options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions,
|
|
346
295
|
) {
|
|
347
296
|
const isCorrection = typeof options === 'object' && options !== null && 'isCorrection' in options
|
|
@@ -440,11 +389,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
440
389
|
const currentOptions = pendingScroll.value.options;
|
|
441
390
|
if (isScrollToIndexOptions(currentOptions)) {
|
|
442
391
|
currentOptions.behavior = 'auto';
|
|
443
|
-
} else {
|
|
444
|
-
pendingScroll.value.options = {
|
|
445
|
-
align: currentOptions as ScrollAlignment | ScrollAlignmentOptions,
|
|
446
|
-
behavior: 'auto',
|
|
447
|
-
};
|
|
448
392
|
}
|
|
449
393
|
}
|
|
450
394
|
}
|
|
@@ -511,188 +455,17 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
511
455
|
};
|
|
512
456
|
|
|
513
457
|
// --- Measurement & Initialization ---
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
measuredItemsX = newMeasuredX;
|
|
523
|
-
}
|
|
524
|
-
if (measuredItemsY.length !== len) {
|
|
525
|
-
const newMeasuredY = new Uint8Array(len);
|
|
526
|
-
newMeasuredY.set(measuredItemsY.subarray(0, Math.min(len, measuredItemsY.length)));
|
|
527
|
-
measuredItemsY = newMeasuredY;
|
|
528
|
-
}
|
|
529
|
-
if (measuredColumns.length !== colCount) {
|
|
530
|
-
const newMeasuredCols = new Uint8Array(colCount);
|
|
531
|
-
newMeasuredCols.set(measuredColumns.subarray(0, Math.min(colCount, measuredColumns.length)));
|
|
532
|
-
measuredColumns = newMeasuredCols;
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
const initializeMeasurements = () => {
|
|
537
|
-
const newItems = props.value.items;
|
|
538
|
-
const len = newItems.length;
|
|
539
|
-
const colCount = props.value.columnCount || 0;
|
|
540
|
-
const gap = props.value.gap || 0;
|
|
541
|
-
const columnGap = props.value.columnGap || 0;
|
|
542
|
-
const cw = props.value.columnWidth;
|
|
543
|
-
|
|
544
|
-
let colNeedsRebuild = false;
|
|
545
|
-
let itemsNeedRebuild = false;
|
|
546
|
-
|
|
547
|
-
// Initialize columns
|
|
548
|
-
if (colCount > 0) {
|
|
549
|
-
for (let i = 0; i < colCount; i++) {
|
|
550
|
-
const currentW = columnSizes.get(i);
|
|
551
|
-
const isMeasured = measuredColumns[ i ] === 1;
|
|
552
|
-
|
|
553
|
-
if (!isDynamicColumnWidth.value || (!isMeasured && currentW === 0)) {
|
|
554
|
-
let baseWidth = 0;
|
|
555
|
-
if (typeof cw === 'number' && cw > 0) {
|
|
556
|
-
baseWidth = cw;
|
|
557
|
-
} else if (Array.isArray(cw) && cw.length > 0) {
|
|
558
|
-
baseWidth = cw[ i % cw.length ] || props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH;
|
|
559
|
-
} else if (typeof cw === 'function') {
|
|
560
|
-
baseWidth = cw(i);
|
|
561
|
-
} else {
|
|
562
|
-
baseWidth = props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const targetW = baseWidth + columnGap;
|
|
566
|
-
if (Math.abs(currentW - targetW) > 0.5) {
|
|
567
|
-
columnSizes.set(i, targetW);
|
|
568
|
-
measuredColumns[ i ] = isDynamicColumnWidth.value ? 0 : 1;
|
|
569
|
-
colNeedsRebuild = true;
|
|
570
|
-
} else if (!isDynamicColumnWidth.value) {
|
|
571
|
-
measuredColumns[ i ] = 1;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Initialize items
|
|
578
|
-
for (let i = 0; i < len; i++) {
|
|
579
|
-
const item = props.value.items[ i ];
|
|
580
|
-
const currentX = itemSizesX.get(i);
|
|
581
|
-
const currentY = itemSizesY.get(i);
|
|
582
|
-
const isMeasuredX = measuredItemsX[ i ] === 1;
|
|
583
|
-
const isMeasuredY = measuredItemsY[ i ] === 1;
|
|
584
|
-
|
|
585
|
-
if (direction.value === 'horizontal') {
|
|
586
|
-
if (!isDynamicItemSize.value || (!isMeasuredX && currentX === 0)) {
|
|
587
|
-
const baseSize = typeof props.value.itemSize === 'function' ? props.value.itemSize(item as T, i) : defaultSize.value;
|
|
588
|
-
const targetX = baseSize + columnGap;
|
|
589
|
-
if (Math.abs(currentX - targetX) > 0.5) {
|
|
590
|
-
itemSizesX.set(i, targetX);
|
|
591
|
-
measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
592
|
-
itemsNeedRebuild = true;
|
|
593
|
-
} else if (!isDynamicItemSize.value) {
|
|
594
|
-
measuredItemsX[ i ] = 1;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
} else if (currentX !== 0) {
|
|
598
|
-
itemSizesX.set(i, 0);
|
|
599
|
-
measuredItemsX[ i ] = 0;
|
|
600
|
-
itemsNeedRebuild = true;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (direction.value !== 'horizontal') {
|
|
604
|
-
if (!isDynamicItemSize.value || (!isMeasuredY && currentY === 0)) {
|
|
605
|
-
const baseSize = typeof props.value.itemSize === 'function' ? props.value.itemSize(item as T, i) : defaultSize.value;
|
|
606
|
-
const targetY = baseSize + gap;
|
|
607
|
-
if (Math.abs(currentY - targetY) > 0.5) {
|
|
608
|
-
itemSizesY.set(i, targetY);
|
|
609
|
-
measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
610
|
-
itemsNeedRebuild = true;
|
|
611
|
-
} else if (!isDynamicItemSize.value) {
|
|
612
|
-
measuredItemsY[ i ] = 1;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
} else if (currentY !== 0) {
|
|
616
|
-
itemSizesY.set(i, 0);
|
|
617
|
-
measuredItemsY[ i ] = 0;
|
|
618
|
-
itemsNeedRebuild = true;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (colNeedsRebuild) {
|
|
623
|
-
columnSizes.rebuild();
|
|
624
|
-
}
|
|
625
|
-
if (itemsNeedRebuild) {
|
|
626
|
-
itemSizesX.rebuild();
|
|
627
|
-
itemSizesY.rebuild();
|
|
628
|
-
}
|
|
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
|
+
});
|
|
629
466
|
};
|
|
630
467
|
|
|
631
|
-
const
|
|
632
|
-
const newItems = props.value.items;
|
|
633
|
-
const len = newItems.length;
|
|
634
|
-
const colCount = props.value.columnCount || 0;
|
|
635
|
-
|
|
636
|
-
resizeMeasurements(len, colCount);
|
|
637
|
-
|
|
638
|
-
let prependCount = 0;
|
|
639
|
-
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
640
|
-
const oldFirstItem = lastItems[ 0 ];
|
|
641
|
-
if (oldFirstItem !== undefined) {
|
|
642
|
-
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
643
|
-
if (newItems[ i ] === oldFirstItem) {
|
|
644
|
-
prependCount = i;
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (prependCount > 0) {
|
|
652
|
-
itemSizesX.shift(prependCount);
|
|
653
|
-
itemSizesY.shift(prependCount);
|
|
654
|
-
|
|
655
|
-
if (pendingScroll.value && pendingScroll.value.rowIndex !== null && pendingScroll.value.rowIndex !== undefined) {
|
|
656
|
-
pendingScroll.value.rowIndex += prependCount;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const newMeasuredX = new Uint8Array(len);
|
|
660
|
-
const newMeasuredY = new Uint8Array(len);
|
|
661
|
-
newMeasuredX.set(measuredItemsX.subarray(0, Math.min(len - prependCount, measuredItemsX.length)), prependCount);
|
|
662
|
-
newMeasuredY.set(measuredItemsY.subarray(0, Math.min(len - prependCount, measuredItemsY.length)), prependCount);
|
|
663
|
-
measuredItemsX = newMeasuredX;
|
|
664
|
-
measuredItemsY = newMeasuredY;
|
|
665
|
-
|
|
666
|
-
// Calculate added size
|
|
667
|
-
const gap = props.value.gap || 0;
|
|
668
|
-
const columnGap = props.value.columnGap || 0;
|
|
669
|
-
let addedX = 0;
|
|
670
|
-
let addedY = 0;
|
|
671
|
-
|
|
672
|
-
for (let i = 0; i < prependCount; i++) {
|
|
673
|
-
const size = typeof props.value.itemSize === 'function' ? props.value.itemSize(newItems[ i ] as T, i) : defaultSize.value;
|
|
674
|
-
if (direction.value === 'horizontal') {
|
|
675
|
-
addedX += size + columnGap;
|
|
676
|
-
} else { addedY += size + gap; }
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (addedX > 0 || addedY > 0) {
|
|
680
|
-
nextTick(() => {
|
|
681
|
-
scrollToOffset(
|
|
682
|
-
addedX > 0 ? relativeScrollX.value + addedX : null,
|
|
683
|
-
addedY > 0 ? relativeScrollY.value + addedY : null,
|
|
684
|
-
{ behavior: 'auto', isCorrection: true } as ScrollToIndexOptions,
|
|
685
|
-
);
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
initializeMeasurements();
|
|
691
|
-
|
|
692
|
-
lastItems = [ ...newItems ];
|
|
693
|
-
sizesInitialized.value = true;
|
|
694
|
-
treeUpdateFlag.value++;
|
|
695
|
-
};
|
|
468
|
+
const initialize = () => initializeSizes(handleScrollCorrection);
|
|
696
469
|
|
|
697
470
|
/**
|
|
698
471
|
* Updates the host element's offset relative to the scroll container.
|
|
@@ -756,7 +529,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
756
529
|
() => props.value.columnGap,
|
|
757
530
|
() => props.value.defaultItemSize,
|
|
758
531
|
() => props.value.defaultColumnWidth,
|
|
759
|
-
],
|
|
532
|
+
], initialize, { immediate: true });
|
|
760
533
|
|
|
761
534
|
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
762
535
|
updateHostOffset();
|
|
@@ -812,28 +585,36 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
812
585
|
});
|
|
813
586
|
|
|
814
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
|
+
*/
|
|
815
594
|
const getRowIndexAt = (offset: number) => {
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
return Math.floor(offset / step);
|
|
824
|
-
}
|
|
825
|
-
return itemSizesX.findLowerBound(offset);
|
|
826
|
-
}
|
|
827
|
-
const step = (fixedSize || 0) + gap;
|
|
828
|
-
if (fixedSize !== null && step > 0) {
|
|
829
|
-
return Math.floor(offset / step);
|
|
830
|
-
}
|
|
831
|
-
return itemSizesY.findLowerBound(offset);
|
|
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
|
+
);
|
|
832
602
|
};
|
|
833
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
|
+
*/
|
|
834
610
|
const getColIndexAt = (offset: number) => {
|
|
835
611
|
if (direction.value === 'both') {
|
|
836
|
-
return
|
|
612
|
+
return calculateIndexAt(
|
|
613
|
+
offset,
|
|
614
|
+
fixedColumnWidth.value,
|
|
615
|
+
props.value.columnGap || 0,
|
|
616
|
+
(off) => columnSizes.findLowerBound(off),
|
|
617
|
+
);
|
|
837
618
|
}
|
|
838
619
|
if (direction.value === 'horizontal') {
|
|
839
620
|
return getRowIndexAt(offset);
|
|
@@ -905,25 +686,17 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
905
686
|
const safeStart = Math.max(0, colStart);
|
|
906
687
|
const safeEnd = Math.min(totalCols, colEnd || totalCols);
|
|
907
688
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
: columnSizes.query(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
:
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
: (columnSizes.query(safeEnd) - (safeEnd > 0 ? columnGap : 0));
|
|
920
|
-
|
|
921
|
-
return {
|
|
922
|
-
start: safeStart,
|
|
923
|
-
end: safeEnd,
|
|
924
|
-
padStart,
|
|
925
|
-
padEnd: Math.max(0, totalColWidth - contentEnd),
|
|
926
|
-
};
|
|
689
|
+
return calculateColumnRange({
|
|
690
|
+
columnCount: totalCols,
|
|
691
|
+
relativeScrollX: calculateOffsetAt(safeStart, fixedColumnWidth.value, props.value.columnGap || 0, (idx) => columnSizes.query(idx)),
|
|
692
|
+
usableWidth: calculateRangeSize(safeStart, safeEnd, fixedColumnWidth.value, props.value.columnGap || 0, (idx) => columnSizes.query(idx)),
|
|
693
|
+
colBuffer: 0,
|
|
694
|
+
fixedWidth: fixedColumnWidth.value,
|
|
695
|
+
columnGap: props.value.columnGap || 0,
|
|
696
|
+
findLowerBound: (offset) => columnSizes.findLowerBound(offset),
|
|
697
|
+
query: (idx) => columnSizes.query(idx),
|
|
698
|
+
totalColsQuery: () => columnSizes.query(totalCols),
|
|
699
|
+
});
|
|
927
700
|
}
|
|
928
701
|
|
|
929
702
|
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
@@ -953,9 +726,6 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
953
726
|
|
|
954
727
|
const { start, end } = range.value;
|
|
955
728
|
const items: RenderedItem<T>[] = [];
|
|
956
|
-
const fixedSize = fixedItemSize.value;
|
|
957
|
-
const gap = props.value.gap || 0;
|
|
958
|
-
const columnGap = props.value.columnGap || 0;
|
|
959
729
|
const stickyIndices = sortedStickyIndices.value;
|
|
960
730
|
const stickySet = stickyIndicesSet.value;
|
|
961
731
|
|
|
@@ -974,24 +744,19 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
974
744
|
sortedIndices.push(i);
|
|
975
745
|
}
|
|
976
746
|
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
ssrOffsetX = fixedSize !== null ? ssrStartCol * (fixedSize + columnGap) : itemSizesX.query(ssrStartCol);
|
|
991
|
-
} else if (direction.value === 'both') {
|
|
992
|
-
ssrOffsetX = columnSizes.query(ssrStartCol);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
747
|
+
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
|
+
)
|
|
759
|
+
: { x: 0, y: 0 };
|
|
995
760
|
|
|
996
761
|
const lastItemsMap = new Map(lastRenderedItems.map((it) => [ it.index, it ]));
|
|
997
762
|
|
|
@@ -1201,144 +966,120 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1201
966
|
}
|
|
1202
967
|
|
|
1203
968
|
const scrollValueX = isRtl.value ? Math.abs(scrollX.value) : scrollX.value;
|
|
1204
|
-
|
|
1205
|
-
|
|
969
|
+
const virtualX = displayToVirtual(scrollValueX, componentOffset.x, scaleX.value);
|
|
970
|
+
const virtualY = displayToVirtual(scrollY.value, componentOffset.y, scaleY.value);
|
|
971
|
+
|
|
972
|
+
if (Math.abs(virtualX - lastInternalX) > 0.5) {
|
|
973
|
+
scrollDirectionX = virtualX > lastInternalX ? 'end' : 'start';
|
|
974
|
+
lastInternalX = virtualX;
|
|
975
|
+
}
|
|
976
|
+
if (Math.abs(virtualY - lastInternalY) > 0.5) {
|
|
977
|
+
scrollDirectionY = virtualY > lastInternalY ? 'end' : 'start';
|
|
978
|
+
lastInternalY = virtualY;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
internalScrollX.value = virtualX;
|
|
982
|
+
internalScrollY.value = virtualY;
|
|
983
|
+
|
|
984
|
+
if (!isProgrammaticScroll.value) {
|
|
985
|
+
pendingScroll.value = null;
|
|
986
|
+
}
|
|
1206
987
|
|
|
1207
988
|
if (!isScrolling.value) {
|
|
1208
|
-
if (!isProgrammaticScroll.value) {
|
|
1209
|
-
pendingScroll.value = null;
|
|
1210
|
-
}
|
|
1211
989
|
isScrolling.value = true;
|
|
1212
990
|
}
|
|
1213
991
|
clearTimeout(scrollTimeout);
|
|
1214
992
|
scrollTimeout = setTimeout(() => {
|
|
993
|
+
const wasProgrammatic = isProgrammaticScroll.value;
|
|
1215
994
|
isScrolling.value = false;
|
|
1216
995
|
isProgrammaticScroll.value = false;
|
|
1217
|
-
}, 250);
|
|
1218
|
-
};
|
|
1219
996
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
const d = targetW - oldW;
|
|
1252
|
-
if (Math.abs(d) > 0.1) {
|
|
1253
|
-
columnSizes.update(colIdx, d);
|
|
1254
|
-
needUpdate = true;
|
|
1255
|
-
if (colIdx < firstColIndex) {
|
|
1256
|
-
deltaX += d;
|
|
1257
|
-
}
|
|
997
|
+
// Only perform snapping if enabled and the last scroll was user-initiated
|
|
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;
|
|
1258
1028
|
}
|
|
1259
|
-
measuredColumns[ colIdx ] = 1;
|
|
1260
1029
|
}
|
|
1261
|
-
}
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1265
|
-
// Ignore 0-size measurements as they usually indicate hidden/detached elements
|
|
1266
|
-
if (inlineSize <= 0 && blockSize <= 0) {
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
1030
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.1) {
|
|
1291
|
-
const d = targetHeight - oldHeight;
|
|
1292
|
-
itemSizesY.update(index, d);
|
|
1293
|
-
measuredItemsY[ index ] = 1;
|
|
1294
|
-
needUpdate = true;
|
|
1295
|
-
if (index < firstRowIndex) {
|
|
1296
|
-
deltaY += d;
|
|
1297
|
-
}
|
|
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;
|
|
1298
1051
|
}
|
|
1299
1052
|
}
|
|
1300
|
-
}
|
|
1301
1053
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
&& props.value.columnCount
|
|
1308
|
-
&& isColMeasurable
|
|
1309
|
-
&& (inlineSize > 0 || element.dataset.colIndex === undefined)
|
|
1310
|
-
) {
|
|
1311
|
-
const colIndexAttr = element.dataset.colIndex;
|
|
1312
|
-
if (colIndexAttr != null) {
|
|
1313
|
-
tryUpdateColumn(Number.parseInt(colIndexAttr, 10), inlineSize);
|
|
1314
|
-
} else {
|
|
1315
|
-
// If the element is a row, try to find cells with data-col-index
|
|
1316
|
-
const cells = Array.from(element.querySelectorAll('[data-col-index]')) as HTMLElement[];
|
|
1317
|
-
|
|
1318
|
-
for (const child of cells) {
|
|
1319
|
-
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1320
|
-
tryUpdateColumn(colIndex, child.getBoundingClientRect().width);
|
|
1321
|
-
}
|
|
1054
|
+
if (shouldSnap) {
|
|
1055
|
+
scrollToIndex(targetRow, targetCol, {
|
|
1056
|
+
align: { x: alignX, y: alignY },
|
|
1057
|
+
behavior: 'smooth',
|
|
1058
|
+
});
|
|
1322
1059
|
}
|
|
1323
1060
|
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
if (needUpdate) {
|
|
1327
|
-
treeUpdateFlag.value++;
|
|
1328
|
-
// Only compensate if not in a programmatic scroll,
|
|
1329
|
-
// as it would interrupt the browser animation or explicit alignment.
|
|
1330
|
-
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1061
|
+
}, 250);
|
|
1062
|
+
};
|
|
1331
1063
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* Updates the size of multiple items in the Fenwick tree.
|
|
1066
|
+
*
|
|
1067
|
+
* @param updates - Array of updates
|
|
1068
|
+
*/
|
|
1069
|
+
const updateItemSizes = (updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement | undefined; }>) => {
|
|
1070
|
+
coreUpdateItemSizes(
|
|
1071
|
+
updates,
|
|
1072
|
+
getRowIndexAt,
|
|
1073
|
+
getColIndexAt,
|
|
1074
|
+
relativeScrollX.value,
|
|
1075
|
+
relativeScrollY.value,
|
|
1076
|
+
(dx, dy) => {
|
|
1077
|
+
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1078
|
+
if (!hasPendingScroll) {
|
|
1079
|
+
handleScrollCorrection(dx, dy);
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
);
|
|
1342
1083
|
};
|
|
1343
1084
|
|
|
1344
1085
|
/**
|
|
@@ -1418,8 +1159,8 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1418
1159
|
const reachedX = (colIndex === null || colIndex === undefined) || Math.abs(currentRelX - targetX) < toleranceX;
|
|
1419
1160
|
const reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(currentRelY - targetY) < toleranceY;
|
|
1420
1161
|
|
|
1421
|
-
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns[ colIndex ] === 1;
|
|
1422
|
-
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY[ rowIndex ] === 1;
|
|
1162
|
+
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns.value[ colIndex ] === 1;
|
|
1163
|
+
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY.value[ rowIndex ] === 1;
|
|
1423
1164
|
|
|
1424
1165
|
if (reachedX && reachedY) {
|
|
1425
1166
|
if (isMeasuredX && isMeasuredY && !isScrolling.value && !isProgrammaticScroll.value) {
|
|
@@ -1562,13 +1303,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1562
1303
|
* Useful if item source data has changed in a way that affects sizes without changing the items array reference.
|
|
1563
1304
|
*/
|
|
1564
1305
|
const refresh = () => {
|
|
1565
|
-
|
|
1566
|
-
itemSizesY.resize(0);
|
|
1567
|
-
columnSizes.resize(0);
|
|
1568
|
-
measuredColumns.fill(0);
|
|
1569
|
-
measuredItemsX.fill(0);
|
|
1570
|
-
measuredItemsY.fill(0);
|
|
1571
|
-
initializeSizes();
|
|
1306
|
+
coreRefresh(handleScrollCorrection);
|
|
1572
1307
|
};
|
|
1573
1308
|
|
|
1574
1309
|
return {
|
|
@@ -1628,7 +1363,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1628
1363
|
* @param index - The row index.
|
|
1629
1364
|
* @returns The virtual offset in VU.
|
|
1630
1365
|
*/
|
|
1631
|
-
getRowOffset: (index: number) => (flowStartY.value + stickyStartY.value + paddingStartY.value) + itemSizesY.query(
|
|
1366
|
+
getRowOffset: (index: number) => (flowStartY.value + stickyStartY.value + paddingStartY.value) + calculateOffsetAt(index, fixedItemSize.value, props.value.gap || 0, (idx) => itemSizesY.query(idx)),
|
|
1632
1367
|
|
|
1633
1368
|
/**
|
|
1634
1369
|
* Helper to get the virtual offset of a specific column.
|
|
@@ -1636,7 +1371,13 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1636
1371
|
* @param index - The column index.
|
|
1637
1372
|
* @returns The virtual offset in VU.
|
|
1638
1373
|
*/
|
|
1639
|
-
getColumnOffset: (index: number) =>
|
|
1374
|
+
getColumnOffset: (index: number) => {
|
|
1375
|
+
const itemsStartVU_X = flowStartX.value + stickyStartX.value + paddingStartX.value;
|
|
1376
|
+
if (direction.value === 'both') {
|
|
1377
|
+
return itemsStartVU_X + calculateOffsetAt(index, fixedColumnWidth.value, props.value.columnGap || 0, (idx) => columnSizes.query(idx));
|
|
1378
|
+
}
|
|
1379
|
+
return itemsStartVU_X + calculateOffsetAt(index, fixedItemSize.value, props.value.columnGap || 0, (idx) => itemSizesX.query(idx));
|
|
1380
|
+
},
|
|
1640
1381
|
|
|
1641
1382
|
/**
|
|
1642
1383
|
* Helper to get the virtual offset of a specific item along the scroll axis.
|
|
@@ -1644,7 +1385,7 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1644
1385
|
* @param index - The item index.
|
|
1645
1386
|
* @returns The virtual offset in VU.
|
|
1646
1387
|
*/
|
|
1647
|
-
getItemOffset: (index: number) => (direction.value === 'horizontal' ? (flowStartX.value + stickyStartX.value + paddingStartX.value) + itemSizesX.query(
|
|
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))),
|
|
1648
1389
|
|
|
1649
1390
|
/**
|
|
1650
1391
|
* Helper to get the size of a specific item along the scroll axis.
|
|
@@ -1652,20 +1393,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1652
1393
|
* @param index - The item index.
|
|
1653
1394
|
* @returns The size in VU (excluding gap).
|
|
1654
1395
|
*/
|
|
1655
|
-
getItemSize: (index: number) =>
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
const itemSize = props.value.itemSize;
|
|
1660
|
-
if (typeof itemSize === 'number' && itemSize > 0) {
|
|
1661
|
-
return itemSize;
|
|
1662
|
-
}
|
|
1663
|
-
if (typeof itemSize === 'function') {
|
|
1664
|
-
const item = props.value.items[ index ];
|
|
1665
|
-
return item !== undefined ? itemSize(item, index) : (props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
|
|
1666
|
-
}
|
|
1667
|
-
return Math.max(0, itemSizesY.get(index) - (props.value.gap || 0));
|
|
1668
|
-
},
|
|
1396
|
+
getItemSize: (index: number) => (direction.value === 'horizontal'
|
|
1397
|
+
? getColumnWidth(index)
|
|
1398
|
+
: getRowHeight(index)),
|
|
1669
1399
|
|
|
1670
1400
|
/**
|
|
1671
1401
|
* Programmatically scroll to a specific row and/or column.
|
|
@@ -1771,5 +1501,15 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
1771
1501
|
* Physical height of the items wrapper in the DOM (clamped to browser limits, in DU).
|
|
1772
1502
|
*/
|
|
1773
1503
|
renderedVirtualHeight,
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Helper to get the row index at a specific virtual offset.
|
|
1507
|
+
*/
|
|
1508
|
+
getRowIndexAt,
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Helper to get the column index at a specific virtual offset.
|
|
1512
|
+
*/
|
|
1513
|
+
getColIndexAt,
|
|
1774
1514
|
};
|
|
1775
1515
|
}
|