@pdanpdan/virtual-scroll 0.2.1 → 0.4.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 +278 -140
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +866 -112
- package/dist/index.js +1 -844
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1125 -0
- package/dist/index.mjs.map +1 -0
- package/dist/virtual-scroll.css +2 -0
- package/package.json +8 -5
- package/src/components/VirtualScroll.test.ts +527 -688
- package/src/components/VirtualScroll.vue +402 -209
- package/src/composables/useVirtualScroll.test.ts +241 -1447
- package/src/composables/useVirtualScroll.ts +544 -531
- package/src/index.ts +2 -0
- package/src/types.ts +535 -0
- package/src/utils/fenwick-tree.ts +38 -18
- package/src/utils/scroll.test.ts +148 -0
- package/src/utils/scroll.ts +40 -10
- package/src/utils/virtual-scroll-logic.test.ts +2517 -0
- package/src/utils/virtual-scroll-logic.ts +605 -0
- package/dist/index.css +0 -2
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RenderedItem,
|
|
3
|
+
ScrollAlignment,
|
|
4
|
+
ScrollAlignmentOptions,
|
|
5
|
+
ScrollDetails,
|
|
6
|
+
ScrollDirection,
|
|
7
|
+
ScrollToIndexOptions,
|
|
8
|
+
VirtualScrollProps,
|
|
9
|
+
} from '../types';
|
|
1
10
|
import type { Ref } from 'vue';
|
|
2
11
|
|
|
3
12
|
/* global ScrollToOptions */
|
|
@@ -5,111 +14,35 @@ import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactiv
|
|
|
5
14
|
|
|
6
15
|
import { FenwickTree } from '../utils/fenwick-tree';
|
|
7
16
|
import { getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions } from '../utils/scroll';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
import {
|
|
18
|
+
calculateColumnRange,
|
|
19
|
+
calculateItemPosition,
|
|
20
|
+
calculateRange,
|
|
21
|
+
calculateScrollTarget,
|
|
22
|
+
calculateStickyItem,
|
|
23
|
+
calculateTotalSize,
|
|
24
|
+
} from '../utils/virtual-scroll-logic';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
type RenderedItem,
|
|
28
|
+
type ScrollAlignment,
|
|
29
|
+
type ScrollAlignmentOptions,
|
|
30
|
+
type ScrollDetails,
|
|
31
|
+
type ScrollDirection,
|
|
32
|
+
type ScrollToIndexOptions,
|
|
33
|
+
type VirtualScrollProps,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_ITEM_SIZE = 40;
|
|
37
|
+
export const DEFAULT_COLUMN_WIDTH = 100;
|
|
11
38
|
export const DEFAULT_BUFFER = 5;
|
|
12
39
|
|
|
13
|
-
export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
|
|
14
|
-
export type ScrollAlignment = 'start' | 'center' | 'end' | 'auto';
|
|
15
|
-
|
|
16
|
-
export interface ScrollAlignmentOptions {
|
|
17
|
-
x?: ScrollAlignment;
|
|
18
|
-
y?: ScrollAlignment;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ScrollToIndexOptions {
|
|
22
|
-
align?: ScrollAlignment | ScrollAlignmentOptions;
|
|
23
|
-
behavior?: 'auto' | 'smooth';
|
|
24
|
-
isCorrection?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface VirtualScrollProps<T = unknown> {
|
|
28
|
-
/** Array of items to be virtualized */
|
|
29
|
-
items: T[];
|
|
30
|
-
/** Fixed size of each item or a function that returns the size of an item */
|
|
31
|
-
itemSize?: number | ((item: T, index: number) => number) | undefined;
|
|
32
|
-
/** Direction of the scroll: 'vertical', 'horizontal', or 'both' */
|
|
33
|
-
direction?: ScrollDirection | undefined;
|
|
34
|
-
/** Number of items to render before the visible viewport */
|
|
35
|
-
bufferBefore?: number | undefined;
|
|
36
|
-
/** Number of items to render after the visible viewport */
|
|
37
|
-
bufferAfter?: number | undefined;
|
|
38
|
-
/** The scrollable container element or window */
|
|
39
|
-
container?: HTMLElement | Window | null | undefined;
|
|
40
|
-
/** The host element that contains the items */
|
|
41
|
-
hostElement?: HTMLElement | null | undefined;
|
|
42
|
-
/** Range of items to render for SSR */
|
|
43
|
-
ssrRange?: {
|
|
44
|
-
start: number;
|
|
45
|
-
end: number;
|
|
46
|
-
colStart?: number;
|
|
47
|
-
colEnd?: number;
|
|
48
|
-
} | undefined;
|
|
49
|
-
/** Number of columns for bidirectional scroll */
|
|
50
|
-
columnCount?: number | undefined;
|
|
51
|
-
/** Fixed width of columns or an array of widths for alternating columns */
|
|
52
|
-
columnWidth?: number | number[] | ((index: number) => number) | undefined;
|
|
53
|
-
/** Padding at the start of the scroll container (e.g. for sticky headers) */
|
|
54
|
-
scrollPaddingStart?: number | { x?: number; y?: number; } | undefined;
|
|
55
|
-
/** Padding at the end of the scroll container */
|
|
56
|
-
scrollPaddingEnd?: number | { x?: number; y?: number; } | undefined;
|
|
57
|
-
/** Gap between items in pixels (vertical) */
|
|
58
|
-
gap?: number | undefined;
|
|
59
|
-
/** Gap between columns in pixels (horizontal/grid) */
|
|
60
|
-
columnGap?: number | undefined;
|
|
61
|
-
/** Indices of items that should stick to the top/start */
|
|
62
|
-
stickyIndices?: number[] | undefined;
|
|
63
|
-
/** Distance from the end of the scrollable area to trigger 'load' event */
|
|
64
|
-
loadDistance?: number | undefined;
|
|
65
|
-
/** Whether items are currently being loaded */
|
|
66
|
-
loading?: boolean | undefined;
|
|
67
|
-
/** Whether to restore scroll position when items are prepended */
|
|
68
|
-
restoreScrollOnPrepend?: boolean | undefined;
|
|
69
|
-
/** Initial scroll index to jump to on mount */
|
|
70
|
-
initialScrollIndex?: number | undefined;
|
|
71
|
-
/** Alignment for the initial scroll index */
|
|
72
|
-
initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions | undefined;
|
|
73
|
-
/** Default size for items before they are measured */
|
|
74
|
-
defaultItemSize?: number | undefined;
|
|
75
|
-
/** Default width for columns before they are measured */
|
|
76
|
-
defaultColumnWidth?: number | undefined;
|
|
77
|
-
/** Whether to enable debug mode (e.g. showing offsets) */
|
|
78
|
-
debug?: boolean | undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface RenderedItem<T = unknown> {
|
|
82
|
-
item: T;
|
|
83
|
-
index: number;
|
|
84
|
-
offset: { x: number; y: number; };
|
|
85
|
-
size: { width: number; height: number; };
|
|
86
|
-
originalX: number;
|
|
87
|
-
originalY: number;
|
|
88
|
-
isSticky?: boolean;
|
|
89
|
-
isStickyActive?: boolean;
|
|
90
|
-
stickyOffset: { x: number; y: number; };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ScrollDetails<T = unknown> {
|
|
94
|
-
items: RenderedItem<T>[];
|
|
95
|
-
currentIndex: number;
|
|
96
|
-
currentColIndex: number;
|
|
97
|
-
scrollOffset: { x: number; y: number; };
|
|
98
|
-
viewportSize: { width: number; height: number; };
|
|
99
|
-
totalSize: { width: number; height: number; };
|
|
100
|
-
isScrolling: boolean;
|
|
101
|
-
isProgrammaticScroll: boolean;
|
|
102
|
-
/** Range of items currently being rendered */
|
|
103
|
-
range: { start: number; end: number; };
|
|
104
|
-
/** Range of columns currently being rendered (for grid mode) */
|
|
105
|
-
columnRange: { start: number; end: number; padStart: number; padEnd: number; };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
40
|
/**
|
|
109
41
|
* Composable for virtual scrolling logic.
|
|
110
|
-
* Handles calculation of visible items, scroll events,
|
|
42
|
+
* Handles calculation of visible items, scroll events, dynamic item sizes, and programmatic scrolling.
|
|
111
43
|
*
|
|
112
|
-
* @param props -
|
|
44
|
+
* @param props - A Ref to the configuration properties.
|
|
45
|
+
* @see VirtualScrollProps
|
|
113
46
|
*/
|
|
114
47
|
export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>) {
|
|
115
48
|
// --- State ---
|
|
@@ -127,8 +60,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
127
60
|
const isProgrammaticScroll = ref(false);
|
|
128
61
|
|
|
129
62
|
// --- Fenwick Trees for efficient size and offset management ---
|
|
130
|
-
const itemSizesX = new FenwickTree(props.value.items
|
|
131
|
-
const itemSizesY = new FenwickTree(props.value.items
|
|
63
|
+
const itemSizesX = new FenwickTree(props.value.items?.length || 0);
|
|
64
|
+
const itemSizesY = new FenwickTree(props.value.items?.length || 0);
|
|
132
65
|
const columnSizes = new FenwickTree(props.value.columnCount || 0);
|
|
133
66
|
|
|
134
67
|
const treeUpdateFlag = ref(0);
|
|
@@ -161,12 +94,33 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
161
94
|
(typeof props.value.itemSize === 'number' && props.value.itemSize > 0) ? props.value.itemSize : null,
|
|
162
95
|
);
|
|
163
96
|
|
|
164
|
-
const
|
|
97
|
+
const fixedColumnWidth = computed(() =>
|
|
98
|
+
(typeof props.value.columnWidth === 'number' && props.value.columnWidth > 0) ? props.value.columnWidth : null,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
|
|
165
102
|
|
|
166
103
|
const sortedStickyIndices = computed(() =>
|
|
167
104
|
[ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
|
|
168
105
|
);
|
|
169
106
|
|
|
107
|
+
const stickyIndicesSet = computed(() => new Set(sortedStickyIndices.value));
|
|
108
|
+
|
|
109
|
+
const paddingStartX = computed(() => getPaddingX(props.value.scrollPaddingStart, props.value.direction));
|
|
110
|
+
const paddingEndX = computed(() => getPaddingX(props.value.scrollPaddingEnd, props.value.direction));
|
|
111
|
+
const paddingStartY = computed(() => getPaddingY(props.value.scrollPaddingStart, props.value.direction));
|
|
112
|
+
const paddingEndY = computed(() => getPaddingY(props.value.scrollPaddingEnd, props.value.direction));
|
|
113
|
+
|
|
114
|
+
const usableWidth = computed(() => {
|
|
115
|
+
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
116
|
+
return viewportWidth.value - (isHorizontal ? (paddingStartX.value + paddingEndX.value) : 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const usableHeight = computed(() => {
|
|
120
|
+
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
121
|
+
return viewportHeight.value - (isVertical ? (paddingStartY.value + paddingEndY.value) : 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
170
124
|
// --- Size Calculations ---
|
|
171
125
|
/**
|
|
172
126
|
* Total width of all items in the scrollable area.
|
|
@@ -178,28 +132,38 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
178
132
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
179
133
|
const { start = 0, end = 0, colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
180
134
|
const colCount = props.value.columnCount || 0;
|
|
181
|
-
if (props.value.direction === 'both'
|
|
182
|
-
|
|
135
|
+
if (props.value.direction === 'both') {
|
|
136
|
+
if (colCount <= 0) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
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));
|
|
183
142
|
}
|
|
184
|
-
/* v8 ignore else -- @preserve */
|
|
185
143
|
if (props.value.direction === 'horizontal') {
|
|
186
144
|
if (fixedItemSize.value !== null) {
|
|
187
|
-
|
|
145
|
+
const len = end - start;
|
|
146
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.columnGap || 0)) - (len > 0 ? (props.value.columnGap || 0) : 0));
|
|
188
147
|
}
|
|
189
|
-
|
|
148
|
+
const total = itemSizesX.query(end) - itemSizesX.query(start);
|
|
149
|
+
return Math.max(0, total - (end > start ? (props.value.columnGap || 0) : 0));
|
|
190
150
|
}
|
|
191
151
|
}
|
|
192
152
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
153
|
+
return calculateTotalSize({
|
|
154
|
+
direction: props.value.direction || 'vertical',
|
|
155
|
+
itemsLength: props.value.items.length,
|
|
156
|
+
columnCount: props.value.columnCount || 0,
|
|
157
|
+
fixedSize: fixedItemSize.value,
|
|
158
|
+
fixedWidth: fixedColumnWidth.value,
|
|
159
|
+
gap: props.value.gap || 0,
|
|
160
|
+
columnGap: props.value.columnGap || 0,
|
|
161
|
+
usableWidth: usableWidth.value,
|
|
162
|
+
usableHeight: usableHeight.value,
|
|
163
|
+
queryY: (idx) => itemSizesY.query(idx),
|
|
164
|
+
queryX: (idx) => itemSizesX.query(idx),
|
|
165
|
+
queryColumn: (idx) => columnSizes.query(idx),
|
|
166
|
+
}).width;
|
|
203
167
|
});
|
|
204
168
|
|
|
205
169
|
/**
|
|
@@ -211,22 +175,30 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
211
175
|
|
|
212
176
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
213
177
|
const { start, end } = props.value.ssrRange;
|
|
214
|
-
/* v8 ignore else -- @preserve */
|
|
215
178
|
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
216
179
|
if (fixedItemSize.value !== null) {
|
|
217
|
-
|
|
180
|
+
const len = end - start;
|
|
181
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.gap || 0)) - (len > 0 ? (props.value.gap || 0) : 0));
|
|
218
182
|
}
|
|
219
|
-
|
|
183
|
+
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
184
|
+
return Math.max(0, total - (end > start ? (props.value.gap || 0) : 0));
|
|
220
185
|
}
|
|
221
186
|
}
|
|
222
187
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
188
|
+
return calculateTotalSize({
|
|
189
|
+
direction: props.value.direction || 'vertical',
|
|
190
|
+
itemsLength: props.value.items.length,
|
|
191
|
+
columnCount: props.value.columnCount || 0,
|
|
192
|
+
fixedSize: fixedItemSize.value,
|
|
193
|
+
fixedWidth: fixedColumnWidth.value,
|
|
194
|
+
gap: props.value.gap || 0,
|
|
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;
|
|
230
202
|
});
|
|
231
203
|
|
|
232
204
|
const relativeScrollX = computed(() => {
|
|
@@ -250,7 +222,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
250
222
|
return cw;
|
|
251
223
|
}
|
|
252
224
|
if (Array.isArray(cw) && cw.length > 0) {
|
|
253
|
-
|
|
225
|
+
const val = cw[ index % cw.length ];
|
|
226
|
+
return (val != null && val > 0) ? val : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
|
|
254
227
|
}
|
|
255
228
|
if (typeof cw === 'function') {
|
|
256
229
|
return cw(index);
|
|
@@ -265,6 +238,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
265
238
|
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
266
239
|
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
267
240
|
* @param options - Scroll options including alignment ('start', 'center', 'end', 'auto') and behavior ('auto', 'smooth').
|
|
241
|
+
* Defaults to { align: 'auto', behavior: 'auto' }.
|
|
268
242
|
*/
|
|
269
243
|
const scrollToIndex = (
|
|
270
244
|
rowIndex: number | null | undefined,
|
|
@@ -275,162 +249,55 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
275
249
|
? options.isCorrection
|
|
276
250
|
: false;
|
|
277
251
|
|
|
278
|
-
if (!isCorrection) {
|
|
279
|
-
pendingScroll.value = { rowIndex, colIndex, options };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
252
|
const container = props.value.container || window;
|
|
283
|
-
const fixedSize = fixedItemSize.value;
|
|
284
|
-
const gap = props.value.gap || 0;
|
|
285
|
-
const columnGap = props.value.columnGap || 0;
|
|
286
|
-
|
|
287
|
-
let align: ScrollAlignment | ScrollAlignmentOptions | undefined;
|
|
288
|
-
let behavior: 'auto' | 'smooth' | undefined;
|
|
289
|
-
|
|
290
|
-
if (isScrollToIndexOptions(options)) {
|
|
291
|
-
align = options.align;
|
|
292
|
-
behavior = options.behavior;
|
|
293
|
-
} else {
|
|
294
|
-
align = options as ScrollAlignment | ScrollAlignmentOptions;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const alignX = (typeof align === 'object' ? align.x : align) || 'auto';
|
|
298
|
-
const alignY = (typeof align === 'object' ? align.y : align) || 'auto';
|
|
299
|
-
|
|
300
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
301
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
|
|
302
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
|
|
303
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
|
|
304
253
|
|
|
305
254
|
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
306
255
|
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
307
256
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
} else {
|
|
334
|
-
const isVisibleY = targetY >= relativeScrollY.value && (targetY + itemHeight) <= (relativeScrollY.value + usableHeight);
|
|
335
|
-
if (!isVisibleY) {
|
|
336
|
-
if (targetY < relativeScrollY.value) {
|
|
337
|
-
// keep targetY at start
|
|
338
|
-
} else {
|
|
339
|
-
targetY -= (usableHeight - itemHeight);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// X calculation
|
|
346
|
-
if (colIndex !== null && colIndex !== undefined) {
|
|
347
|
-
const totalCols = props.value.columnCount || 0;
|
|
348
|
-
if (colIndex >= totalCols && totalCols > 0) {
|
|
349
|
-
targetX = totalWidth.value;
|
|
350
|
-
itemWidth = 0;
|
|
351
|
-
} else if (props.value.direction === 'horizontal') {
|
|
352
|
-
targetX = fixedSize !== null ? colIndex * (fixedSize + columnGap) : itemSizesX.query(colIndex);
|
|
353
|
-
itemWidth = fixedSize !== null ? fixedSize : itemSizesX.get(colIndex) - columnGap;
|
|
354
|
-
} else {
|
|
355
|
-
targetX = columnSizes.query(colIndex);
|
|
356
|
-
itemWidth = columnSizes.get(colIndex);
|
|
357
|
-
}
|
|
257
|
+
const { targetX, targetY, effectiveAlignX, effectiveAlignY } = calculateScrollTarget({
|
|
258
|
+
rowIndex,
|
|
259
|
+
colIndex,
|
|
260
|
+
options,
|
|
261
|
+
itemsLength: props.value.items.length,
|
|
262
|
+
columnCount: props.value.columnCount || 0,
|
|
263
|
+
direction: props.value.direction || 'vertical',
|
|
264
|
+
usableWidth: usableWidth.value,
|
|
265
|
+
usableHeight: usableHeight.value,
|
|
266
|
+
totalWidth: totalWidth.value,
|
|
267
|
+
totalHeight: totalHeight.value,
|
|
268
|
+
gap: props.value.gap || 0,
|
|
269
|
+
columnGap: props.value.columnGap || 0,
|
|
270
|
+
fixedSize: fixedItemSize.value,
|
|
271
|
+
fixedWidth: fixedColumnWidth.value,
|
|
272
|
+
relativeScrollX: relativeScrollX.value,
|
|
273
|
+
relativeScrollY: relativeScrollY.value,
|
|
274
|
+
getItemSizeY: (idx) => itemSizesY.get(idx),
|
|
275
|
+
getItemSizeX: (idx) => itemSizesX.get(idx),
|
|
276
|
+
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
277
|
+
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
278
|
+
getColumnSize: (idx) => columnSizes.get(idx),
|
|
279
|
+
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
280
|
+
stickyIndices: sortedStickyIndices.value,
|
|
281
|
+
});
|
|
358
282
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
/* v8 ignore if -- @preserve */
|
|
370
|
-
if (targetX < relativeScrollX.value) {
|
|
371
|
-
// keep targetX at start
|
|
372
|
-
} else {
|
|
373
|
-
targetX -= (usableWidth - itemWidth);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
283
|
+
if (!isCorrection) {
|
|
284
|
+
const behavior = isScrollToIndexOptions(options) ? options.behavior : undefined;
|
|
285
|
+
pendingScroll.value = {
|
|
286
|
+
rowIndex,
|
|
287
|
+
colIndex,
|
|
288
|
+
options: {
|
|
289
|
+
align: { x: effectiveAlignX, y: effectiveAlignY },
|
|
290
|
+
...(behavior != null ? { behavior } : {}),
|
|
291
|
+
},
|
|
292
|
+
};
|
|
377
293
|
}
|
|
378
294
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight.value - usableHeight)));
|
|
382
|
-
|
|
383
|
-
const finalX = targetX + hostOffset.x - (isHorizontal ? paddingStartX : 0);
|
|
384
|
-
const finalY = targetY + hostOffset.y - (isVertical ? paddingStartY : 0);
|
|
385
|
-
|
|
386
|
-
// Check if we reached the target
|
|
387
|
-
const tolerance = 1;
|
|
388
|
-
let reachedX = (colIndex === null || colIndex === undefined) || Math.abs(relativeScrollX.value - targetX) < tolerance;
|
|
389
|
-
let reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(relativeScrollY.value - targetY) < tolerance;
|
|
390
|
-
|
|
391
|
-
if (!reachedX || !reachedY) {
|
|
392
|
-
let curX = 0;
|
|
393
|
-
let curY = 0;
|
|
394
|
-
let maxW = 0;
|
|
395
|
-
let maxH = 0;
|
|
396
|
-
let viewW = 0;
|
|
397
|
-
let viewH = 0;
|
|
398
|
-
|
|
399
|
-
/* v8 ignore else -- @preserve */
|
|
400
|
-
if (typeof window !== 'undefined') {
|
|
401
|
-
if (container === window) {
|
|
402
|
-
curX = window.scrollX;
|
|
403
|
-
curY = window.scrollY;
|
|
404
|
-
maxW = document.documentElement.scrollWidth;
|
|
405
|
-
maxH = document.documentElement.scrollHeight;
|
|
406
|
-
viewW = window.innerWidth;
|
|
407
|
-
viewH = window.innerHeight;
|
|
408
|
-
} else if (isElement(container)) {
|
|
409
|
-
curX = container.scrollLeft;
|
|
410
|
-
curY = container.scrollTop;
|
|
411
|
-
maxW = container.scrollWidth;
|
|
412
|
-
maxH = container.scrollHeight;
|
|
413
|
-
viewW = container.clientWidth;
|
|
414
|
-
viewH = container.clientHeight;
|
|
415
|
-
}
|
|
295
|
+
const finalX = targetX + hostOffset.x - (isHorizontal ? paddingStartX.value : 0);
|
|
296
|
+
const finalY = targetY + hostOffset.y - (isVertical ? paddingStartY.value : 0);
|
|
416
297
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
/* v8 ignore else -- @preserve */
|
|
421
|
-
if (atLeft || atRight) {
|
|
422
|
-
reachedX = true;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (!reachedY && rowIndex !== null && rowIndex !== undefined) {
|
|
427
|
-
const atTop = curY <= tolerance && finalY <= tolerance;
|
|
428
|
-
const atBottom = curY >= maxH - viewH - tolerance && finalY >= maxH - viewH - tolerance;
|
|
429
|
-
if (atTop || atBottom) {
|
|
430
|
-
reachedY = true;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
298
|
+
let behavior: 'auto' | 'smooth' | undefined;
|
|
299
|
+
if (isScrollToIndexOptions(options)) {
|
|
300
|
+
behavior = options.behavior;
|
|
434
301
|
}
|
|
435
302
|
|
|
436
303
|
const scrollBehavior = isCorrection ? 'auto' : (behavior || 'smooth');
|
|
@@ -475,9 +342,9 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
475
342
|
}
|
|
476
343
|
}
|
|
477
344
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
345
|
+
// We do NOT clear pendingScroll here anymore.
|
|
346
|
+
// It will be cleared in checkPendingScroll when fully stable,
|
|
347
|
+
// or in handleScroll if the user manually scrolls.
|
|
481
348
|
};
|
|
482
349
|
|
|
483
350
|
/**
|
|
@@ -486,7 +353,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
486
353
|
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
487
354
|
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
488
355
|
* @param options - Scroll options (behavior)
|
|
489
|
-
* @param options.behavior - The scroll behavior ('auto' | 'smooth')
|
|
356
|
+
* @param options.behavior - The scroll behavior ('auto' | 'smooth'). Defaults to 'auto'.
|
|
490
357
|
*/
|
|
491
358
|
const scrollToOffset = (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => {
|
|
492
359
|
const container = props.value.container || window;
|
|
@@ -495,26 +362,18 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
495
362
|
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
496
363
|
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
497
364
|
|
|
498
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
499
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
|
|
500
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
|
|
501
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
|
|
502
|
-
|
|
503
|
-
const usableWidth = viewportWidth.value - (isHorizontal ? (paddingStartX + paddingEndX) : 0);
|
|
504
|
-
const usableHeight = viewportHeight.value - (isVertical ? (paddingStartY + paddingEndY) : 0);
|
|
505
|
-
|
|
506
365
|
const clampedX = (x !== null && x !== undefined)
|
|
507
|
-
? (isHorizontal ? Math.max(0, Math.min(x, Math.max(0, totalWidth.value - usableWidth))) : Math.max(0, x))
|
|
366
|
+
? (isHorizontal ? Math.max(0, Math.min(x, Math.max(0, totalWidth.value - usableWidth.value))) : Math.max(0, x))
|
|
508
367
|
: null;
|
|
509
368
|
const clampedY = (y !== null && y !== undefined)
|
|
510
|
-
? (isVertical ? Math.max(0, Math.min(y, Math.max(0, totalHeight.value - usableHeight))) : Math.max(0, y))
|
|
369
|
+
? (isVertical ? Math.max(0, Math.min(y, Math.max(0, totalHeight.value - usableHeight.value))) : Math.max(0, y))
|
|
511
370
|
: null;
|
|
512
371
|
|
|
513
372
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
514
373
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
515
374
|
|
|
516
|
-
const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
|
|
517
|
-
const targetY = (clampedY !== null) ? clampedY + hostOffset.y - (isVertical ? paddingStartY : 0) : currentY;
|
|
375
|
+
const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX.value : 0) : currentX;
|
|
376
|
+
const targetY = (clampedY !== null) ? clampedY + hostOffset.y - (isVertical ? paddingStartY.value : 0) : currentY;
|
|
518
377
|
|
|
519
378
|
if (typeof window !== 'undefined' && container === window) {
|
|
520
379
|
window.scrollTo({
|
|
@@ -585,7 +444,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
585
444
|
let prependCount = 0;
|
|
586
445
|
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
587
446
|
const oldFirstItem = lastItems[ 0 ];
|
|
588
|
-
/* v8 ignore else -- @preserve */
|
|
589
447
|
if (oldFirstItem !== undefined) {
|
|
590
448
|
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
591
449
|
if (newItems[ i ] === oldFirstItem) {
|
|
@@ -629,7 +487,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
629
487
|
}
|
|
630
488
|
}
|
|
631
489
|
|
|
632
|
-
/* v8 ignore else -- @preserve */
|
|
633
490
|
if (addedX > 0 || addedY > 0) {
|
|
634
491
|
nextTick(() => {
|
|
635
492
|
scrollToOffset(
|
|
@@ -641,22 +498,35 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
641
498
|
}
|
|
642
499
|
}
|
|
643
500
|
|
|
644
|
-
// Initialize columns
|
|
501
|
+
// Initialize columns
|
|
645
502
|
if (colCount > 0) {
|
|
646
503
|
const columnGap = props.value.columnGap || 0;
|
|
647
504
|
let colNeedsRebuild = false;
|
|
505
|
+
const cw = props.value.columnWidth;
|
|
506
|
+
|
|
648
507
|
for (let i = 0; i < colCount; i++) {
|
|
649
|
-
const width = getColumnWidth(i);
|
|
650
508
|
const currentW = columnSizes.get(i);
|
|
509
|
+
const isMeasured = measuredColumns[ i ] === 1;
|
|
510
|
+
|
|
511
|
+
if (!isDynamicColumnWidth.value || (!isMeasured && currentW === 0)) {
|
|
512
|
+
let baseWidth = 0;
|
|
513
|
+
if (typeof cw === 'number' && cw > 0) {
|
|
514
|
+
baseWidth = cw;
|
|
515
|
+
} else if (Array.isArray(cw) && cw.length > 0) {
|
|
516
|
+
baseWidth = cw[ i % cw.length ] || props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH;
|
|
517
|
+
} else if (typeof cw === 'function') {
|
|
518
|
+
baseWidth = cw(i);
|
|
519
|
+
} else {
|
|
520
|
+
baseWidth = props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH;
|
|
521
|
+
}
|
|
651
522
|
|
|
652
|
-
|
|
653
|
-
// OR if it's dynamic but we don't have a measurement yet.
|
|
654
|
-
if (!isDynamicColumnWidth.value || currentW === 0) {
|
|
655
|
-
const targetW = width + columnGap;
|
|
656
|
-
/* v8 ignore else -- @preserve */
|
|
523
|
+
const targetW = baseWidth + columnGap;
|
|
657
524
|
if (Math.abs(currentW - targetW) > 0.5) {
|
|
658
525
|
columnSizes.set(i, targetW);
|
|
526
|
+
measuredColumns[ i ] = isDynamicColumnWidth.value ? 0 : 1;
|
|
659
527
|
colNeedsRebuild = true;
|
|
528
|
+
} else if (!isDynamicColumnWidth.value) {
|
|
529
|
+
measuredColumns[ i ] = 1;
|
|
660
530
|
}
|
|
661
531
|
}
|
|
662
532
|
}
|
|
@@ -674,28 +544,54 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
674
544
|
const currentX = itemSizesX.get(i);
|
|
675
545
|
const currentY = itemSizesY.get(i);
|
|
676
546
|
|
|
677
|
-
// If it's dynamic and already has a measurement, keep it.
|
|
678
|
-
if (isDynamicItemSize.value && (currentX > 0 || currentY > 0)) {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const size = typeof props.value.itemSize === 'function'
|
|
683
|
-
? props.value.itemSize(item as T, i)
|
|
684
|
-
: defaultSize.value;
|
|
685
|
-
|
|
686
547
|
const isVertical = props.value.direction === 'vertical';
|
|
687
548
|
const isHorizontal = props.value.direction === 'horizontal';
|
|
688
549
|
const isBoth = props.value.direction === 'both';
|
|
689
550
|
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
551
|
+
const isMeasuredX = measuredItemsX[ i ] === 1;
|
|
552
|
+
const isMeasuredY = measuredItemsY[ i ] === 1;
|
|
553
|
+
|
|
554
|
+
// Logic for X
|
|
555
|
+
if (isHorizontal) {
|
|
556
|
+
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;
|
|
560
|
+
const targetX = baseSize + columnGap;
|
|
561
|
+
|
|
562
|
+
if (Math.abs(currentX - targetX) > 0.5) {
|
|
563
|
+
itemSizesX.set(i, targetX);
|
|
564
|
+
measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
565
|
+
itemsNeedRebuild = true;
|
|
566
|
+
} else if (!isDynamicItemSize.value) {
|
|
567
|
+
measuredItemsX[ i ] = 1;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} else if (currentX !== 0) {
|
|
571
|
+
itemSizesX.set(i, 0);
|
|
572
|
+
measuredItemsX[ i ] = 0;
|
|
695
573
|
itemsNeedRebuild = true;
|
|
696
574
|
}
|
|
697
|
-
|
|
698
|
-
|
|
575
|
+
|
|
576
|
+
// Logic for Y
|
|
577
|
+
if (isVertical || isBoth) {
|
|
578
|
+
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;
|
|
582
|
+
const targetY = baseSize + gap;
|
|
583
|
+
|
|
584
|
+
if (Math.abs(currentY - targetY) > 0.5) {
|
|
585
|
+
itemSizesY.set(i, targetY);
|
|
586
|
+
measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
587
|
+
itemsNeedRebuild = true;
|
|
588
|
+
} else if (!isDynamicItemSize.value) {
|
|
589
|
+
measuredItemsY[ i ] = 1;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} else if (currentY !== 0) {
|
|
593
|
+
itemSizesY.set(i, 0);
|
|
594
|
+
measuredItemsY[ i ] = 0;
|
|
699
595
|
itemsNeedRebuild = true;
|
|
700
596
|
}
|
|
701
597
|
}
|
|
@@ -741,12 +637,16 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
741
637
|
};
|
|
742
638
|
|
|
743
639
|
watch([
|
|
640
|
+
() => props.value.items,
|
|
744
641
|
() => props.value.items.length,
|
|
642
|
+
() => props.value.direction,
|
|
745
643
|
() => props.value.columnCount,
|
|
746
644
|
() => props.value.columnWidth,
|
|
747
645
|
() => props.value.itemSize,
|
|
748
646
|
() => props.value.gap,
|
|
749
647
|
() => props.value.columnGap,
|
|
648
|
+
() => props.value.defaultItemSize,
|
|
649
|
+
() => props.value.defaultColumnWidth,
|
|
750
650
|
], initializeSizes, { immediate: true });
|
|
751
651
|
|
|
752
652
|
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
@@ -768,58 +668,26 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
768
668
|
};
|
|
769
669
|
}
|
|
770
670
|
|
|
771
|
-
const direction = props.value.direction || 'vertical';
|
|
772
671
|
const bufferBefore = (props.value.ssrRange && !isScrolling.value) ? 0 : (props.value.bufferBefore ?? DEFAULT_BUFFER);
|
|
773
672
|
const bufferAfter = props.value.bufferAfter ?? DEFAULT_BUFFER;
|
|
774
|
-
const gap = props.value.gap || 0;
|
|
775
|
-
const columnGap = props.value.columnGap || 0;
|
|
776
|
-
const fixedSize = fixedItemSize.value;
|
|
777
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, direction);
|
|
778
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, direction);
|
|
779
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, direction);
|
|
780
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, direction);
|
|
781
|
-
|
|
782
|
-
const isVertical = direction === 'vertical' || direction === 'both';
|
|
783
|
-
const isHorizontal = direction === 'horizontal' || direction === 'both';
|
|
784
|
-
|
|
785
|
-
const usableWidth = viewportWidth.value - (isHorizontal ? (paddingStartX + paddingEndX) : 0);
|
|
786
|
-
const usableHeight = viewportHeight.value - (isVertical ? (paddingStartY + paddingEndY) : 0);
|
|
787
|
-
|
|
788
|
-
let start = 0;
|
|
789
|
-
let end = props.value.items.length;
|
|
790
|
-
|
|
791
|
-
if (isVertical) {
|
|
792
|
-
if (fixedSize !== null) {
|
|
793
|
-
start = Math.floor(relativeScrollY.value / (fixedSize + gap));
|
|
794
|
-
end = Math.ceil((relativeScrollY.value + usableHeight) / (fixedSize + gap));
|
|
795
|
-
} else {
|
|
796
|
-
start = itemSizesY.findLowerBound(relativeScrollY.value);
|
|
797
|
-
let currentY = itemSizesY.query(start);
|
|
798
|
-
let i = start;
|
|
799
|
-
while (i < props.value.items.length && currentY < relativeScrollY.value + usableHeight) {
|
|
800
|
-
currentY = itemSizesY.query(++i);
|
|
801
|
-
}
|
|
802
|
-
end = i;
|
|
803
|
-
}
|
|
804
|
-
} else {
|
|
805
|
-
if (fixedSize !== null) {
|
|
806
|
-
start = Math.floor(relativeScrollX.value / (fixedSize + columnGap));
|
|
807
|
-
end = Math.ceil((relativeScrollX.value + usableWidth) / (fixedSize + columnGap));
|
|
808
|
-
} else {
|
|
809
|
-
start = itemSizesX.findLowerBound(relativeScrollX.value);
|
|
810
|
-
let currentX = itemSizesX.query(start);
|
|
811
|
-
let i = start;
|
|
812
|
-
while (i < props.value.items.length && currentX < relativeScrollX.value + usableWidth) {
|
|
813
|
-
currentX = itemSizesX.query(++i);
|
|
814
|
-
}
|
|
815
|
-
end = i;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
673
|
|
|
819
|
-
return {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
674
|
+
return calculateRange({
|
|
675
|
+
direction: props.value.direction || 'vertical',
|
|
676
|
+
relativeScrollX: relativeScrollX.value,
|
|
677
|
+
relativeScrollY: relativeScrollY.value,
|
|
678
|
+
usableWidth: usableWidth.value,
|
|
679
|
+
usableHeight: usableHeight.value,
|
|
680
|
+
itemsLength: props.value.items.length,
|
|
681
|
+
bufferBefore,
|
|
682
|
+
bufferAfter,
|
|
683
|
+
gap: props.value.gap || 0,
|
|
684
|
+
columnGap: props.value.columnGap || 0,
|
|
685
|
+
fixedSize: fixedItemSize.value,
|
|
686
|
+
findLowerBoundY: (offset) => itemSizesY.findLowerBound(offset),
|
|
687
|
+
findLowerBoundX: (offset) => itemSizesX.findLowerBound(offset),
|
|
688
|
+
queryY: (idx) => itemSizesY.query(idx),
|
|
689
|
+
queryX: (idx) => itemSizesX.query(idx),
|
|
690
|
+
});
|
|
823
691
|
});
|
|
824
692
|
|
|
825
693
|
/**
|
|
@@ -848,6 +716,9 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
848
716
|
/**
|
|
849
717
|
* List of items to be rendered with their calculated offsets and sizes.
|
|
850
718
|
*/
|
|
719
|
+
|
|
720
|
+
let lastRenderedItems: RenderedItem<T>[] = [];
|
|
721
|
+
|
|
851
722
|
const renderedItems = computed<RenderedItem<T>[]>(() => {
|
|
852
723
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
853
724
|
treeUpdateFlag.value;
|
|
@@ -858,6 +729,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
858
729
|
const gap = props.value.gap || 0;
|
|
859
730
|
const columnGap = props.value.columnGap || 0;
|
|
860
731
|
const stickyIndices = sortedStickyIndices.value;
|
|
732
|
+
const stickySet = stickyIndicesSet.value;
|
|
861
733
|
|
|
862
734
|
// Always include relevant sticky items
|
|
863
735
|
const indicesToRender = new Set<number>();
|
|
@@ -885,8 +757,27 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
885
757
|
indicesToRender.add(prevStickyIdx);
|
|
886
758
|
}
|
|
887
759
|
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
}
|
|
890
781
|
indicesToRender.add(idx);
|
|
891
782
|
}
|
|
892
783
|
}
|
|
@@ -912,102 +803,86 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
912
803
|
}
|
|
913
804
|
}
|
|
914
805
|
|
|
806
|
+
const lastItemsMap = new Map(lastRenderedItems.map((it) => [ it.index, it ]));
|
|
807
|
+
|
|
915
808
|
for (const i of sortedIndices) {
|
|
916
809
|
const item = props.value.items[ i ];
|
|
917
810
|
if (item === undefined) {
|
|
918
811
|
continue;
|
|
919
812
|
}
|
|
920
813
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
814
|
+
const { x, y, width, height } = calculateItemPosition({
|
|
815
|
+
index: i,
|
|
816
|
+
direction: props.value.direction || 'vertical',
|
|
817
|
+
fixedSize: fixedItemSize.value,
|
|
818
|
+
gap: props.value.gap || 0,
|
|
819
|
+
columnGap: props.value.columnGap || 0,
|
|
820
|
+
usableWidth: usableWidth.value,
|
|
821
|
+
usableHeight: usableHeight.value,
|
|
822
|
+
totalWidth: totalWidth.value,
|
|
823
|
+
queryY: (idx) => itemSizesY.query(idx),
|
|
824
|
+
queryX: (idx) => itemSizesX.query(idx),
|
|
825
|
+
getSizeY: (idx) => itemSizesY.get(idx),
|
|
826
|
+
getSizeX: (idx) => itemSizesX.get(idx),
|
|
827
|
+
});
|
|
936
828
|
|
|
937
|
-
const isSticky =
|
|
829
|
+
const isSticky = stickySet.has(i);
|
|
938
830
|
const originalX = x;
|
|
939
831
|
const originalY = y;
|
|
940
|
-
let isStickyActive = false;
|
|
941
|
-
const stickyOffset = { x: 0, y: 0 };
|
|
942
|
-
|
|
943
|
-
if (isSticky) {
|
|
944
|
-
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
945
|
-
if (relativeScrollY.value > originalY) {
|
|
946
|
-
isStickyActive = true;
|
|
947
|
-
// Check if next sticky item pushes this one
|
|
948
|
-
let nextStickyIdx: number | undefined;
|
|
949
|
-
let low = 0;
|
|
950
|
-
let high = stickyIndices.length - 1;
|
|
951
|
-
while (low <= high) {
|
|
952
|
-
const mid = (low + high) >>> 1;
|
|
953
|
-
if (stickyIndices[ mid ]! > i) {
|
|
954
|
-
nextStickyIdx = stickyIndices[ mid ];
|
|
955
|
-
high = mid - 1;
|
|
956
|
-
} else {
|
|
957
|
-
low = mid + 1;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
832
|
|
|
961
|
-
|
|
962
|
-
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : itemSizesY.query(nextStickyIdx);
|
|
963
|
-
const distance = nextStickyY - relativeScrollY.value;
|
|
964
|
-
/* v8 ignore else -- @preserve */
|
|
965
|
-
if (distance < height) {
|
|
966
|
-
stickyOffset.y = -(height - distance);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
} else if (props.value.direction === 'horizontal') {
|
|
971
|
-
if (relativeScrollX.value > originalX) {
|
|
972
|
-
isStickyActive = true;
|
|
973
|
-
// Check if next sticky item pushes this one
|
|
974
|
-
let nextStickyIdx: number | undefined;
|
|
975
|
-
let low = 0;
|
|
976
|
-
let high = stickyIndices.length - 1;
|
|
977
|
-
while (low <= high) {
|
|
978
|
-
const mid = (low + high) >>> 1;
|
|
979
|
-
if (stickyIndices[ mid ]! > i) {
|
|
980
|
-
nextStickyIdx = stickyIndices[ mid ];
|
|
981
|
-
high = mid - 1;
|
|
982
|
-
} else {
|
|
983
|
-
low = mid + 1;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (nextStickyIdx !== undefined) {
|
|
988
|
-
const nextStickyX = fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : itemSizesX.query(nextStickyIdx);
|
|
989
|
-
const distance = nextStickyX - relativeScrollX.value;
|
|
990
|
-
/* v8 ignore else -- @preserve */
|
|
991
|
-
if (distance < width) {
|
|
992
|
-
stickyOffset.x = -(width - distance);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
items.push({
|
|
1000
|
-
item,
|
|
833
|
+
const { isStickyActive, stickyOffset } = calculateStickyItem({
|
|
1001
834
|
index: i,
|
|
1002
|
-
|
|
1003
|
-
|
|
835
|
+
isSticky,
|
|
836
|
+
direction: props.value.direction || 'vertical',
|
|
837
|
+
relativeScrollX: relativeScrollX.value,
|
|
838
|
+
relativeScrollY: relativeScrollY.value,
|
|
1004
839
|
originalX,
|
|
1005
840
|
originalY,
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
841
|
+
width,
|
|
842
|
+
height,
|
|
843
|
+
stickyIndices,
|
|
844
|
+
fixedSize: fixedItemSize.value,
|
|
845
|
+
fixedWidth: fixedColumnWidth.value,
|
|
846
|
+
gap: props.value.gap || 0,
|
|
847
|
+
columnGap: props.value.columnGap || 0,
|
|
848
|
+
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
849
|
+
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
1009
850
|
});
|
|
851
|
+
|
|
852
|
+
const offsetX = originalX - ssrOffsetX;
|
|
853
|
+
const offsetY = originalY - ssrOffsetY;
|
|
854
|
+
const last = lastItemsMap.get(i);
|
|
855
|
+
|
|
856
|
+
if (
|
|
857
|
+
last
|
|
858
|
+
&& last.item === item
|
|
859
|
+
&& last.offset.x === offsetX
|
|
860
|
+
&& last.offset.y === offsetY
|
|
861
|
+
&& last.size.width === width
|
|
862
|
+
&& last.size.height === height
|
|
863
|
+
&& last.isSticky === isSticky
|
|
864
|
+
&& last.isStickyActive === isStickyActive
|
|
865
|
+
&& last.stickyOffset.x === stickyOffset.x
|
|
866
|
+
&& last.stickyOffset.y === stickyOffset.y
|
|
867
|
+
) {
|
|
868
|
+
items.push(last);
|
|
869
|
+
} else {
|
|
870
|
+
items.push({
|
|
871
|
+
item,
|
|
872
|
+
index: i,
|
|
873
|
+
offset: { x: offsetX, y: offsetY },
|
|
874
|
+
size: { width, height },
|
|
875
|
+
originalX,
|
|
876
|
+
originalY,
|
|
877
|
+
isSticky,
|
|
878
|
+
isStickyActive,
|
|
879
|
+
stickyOffset,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
1010
882
|
}
|
|
883
|
+
|
|
884
|
+
lastRenderedItems = items;
|
|
885
|
+
|
|
1011
886
|
return items;
|
|
1012
887
|
});
|
|
1013
888
|
|
|
@@ -1033,26 +908,19 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1033
908
|
};
|
|
1034
909
|
}
|
|
1035
910
|
|
|
1036
|
-
const start = columnSizes.findLowerBound(relativeScrollX.value);
|
|
1037
|
-
let currentX = columnSizes.query(start);
|
|
1038
|
-
let end = start;
|
|
1039
|
-
|
|
1040
|
-
while (end < totalCols && currentX < relativeScrollX.value + viewportWidth.value) {
|
|
1041
|
-
currentX = columnSizes.query(++end);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
911
|
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
1045
912
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
+
});
|
|
1056
924
|
});
|
|
1057
925
|
|
|
1058
926
|
/**
|
|
@@ -1066,12 +934,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1066
934
|
const columnGap = props.value.columnGap || 0;
|
|
1067
935
|
|
|
1068
936
|
let currentColIndex = 0;
|
|
1069
|
-
if (props.value.direction === 'horizontal'
|
|
937
|
+
if (props.value.direction === 'horizontal') {
|
|
1070
938
|
if (fixedSize !== null) {
|
|
1071
939
|
currentColIndex = Math.floor(relativeScrollX.value / (fixedSize + columnGap));
|
|
1072
940
|
} else {
|
|
1073
941
|
currentColIndex = itemSizesX.findLowerBound(relativeScrollX.value);
|
|
1074
942
|
}
|
|
943
|
+
} else if (props.value.direction === 'both') {
|
|
944
|
+
currentColIndex = columnSizes.findLowerBound(relativeScrollX.value);
|
|
1075
945
|
}
|
|
1076
946
|
|
|
1077
947
|
return {
|
|
@@ -1109,9 +979,13 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1109
979
|
if (target === window || target === document) {
|
|
1110
980
|
scrollX.value = window.scrollX;
|
|
1111
981
|
scrollY.value = window.scrollY;
|
|
982
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
983
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1112
984
|
} else if (isScrollableElement(target)) {
|
|
1113
985
|
scrollX.value = target.scrollLeft;
|
|
1114
986
|
scrollY.value = target.scrollTop;
|
|
987
|
+
viewportWidth.value = target.clientWidth;
|
|
988
|
+
viewportHeight.value = target.clientHeight;
|
|
1115
989
|
}
|
|
1116
990
|
|
|
1117
991
|
if (!isScrolling.value) {
|
|
@@ -1134,62 +1008,120 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1134
1008
|
*/
|
|
1135
1009
|
const updateItemSizes = (updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement | undefined; }>) => {
|
|
1136
1010
|
let needUpdate = false;
|
|
1011
|
+
let deltaX = 0;
|
|
1012
|
+
let deltaY = 0;
|
|
1137
1013
|
const gap = props.value.gap || 0;
|
|
1138
1014
|
const columnGap = props.value.columnGap || 0;
|
|
1139
1015
|
|
|
1016
|
+
const currentRelX = relativeScrollX.value;
|
|
1017
|
+
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
|
+
|
|
1025
|
+
const isHorizontalMode = props.value.direction === 'horizontal';
|
|
1026
|
+
const isVerticalMode = props.value.direction === 'vertical';
|
|
1027
|
+
const isBothMode = props.value.direction === 'both';
|
|
1028
|
+
|
|
1029
|
+
const processedRows = new Set<number>();
|
|
1030
|
+
const processedCols = new Set<number>();
|
|
1031
|
+
|
|
1140
1032
|
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1141
|
-
|
|
1142
|
-
|
|
1033
|
+
// Ignore 0-size measurements as they usually indicate hidden/detached elements
|
|
1034
|
+
if (inlineSize <= 0 && blockSize <= 0) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const isMeasurable = isDynamicItemSize.value || typeof props.value.itemSize === 'function';
|
|
1039
|
+
if (index >= 0 && !processedRows.has(index) && isMeasurable && blockSize > 0) {
|
|
1040
|
+
processedRows.add(index);
|
|
1041
|
+
if (isHorizontalMode && inlineSize > 0) {
|
|
1143
1042
|
const oldWidth = itemSizesX.get(index);
|
|
1144
1043
|
const targetWidth = inlineSize + columnGap;
|
|
1145
|
-
if (Math.abs(
|
|
1146
|
-
|
|
1044
|
+
if (!measuredItemsX[ index ] || Math.abs(targetWidth - oldWidth) > 0.1) {
|
|
1045
|
+
const d = targetWidth - oldWidth;
|
|
1046
|
+
itemSizesX.update(index, d);
|
|
1147
1047
|
measuredItemsX[ index ] = 1;
|
|
1148
1048
|
needUpdate = true;
|
|
1049
|
+
if (index < firstRowIndex) {
|
|
1050
|
+
deltaX += d;
|
|
1051
|
+
}
|
|
1149
1052
|
}
|
|
1150
1053
|
}
|
|
1151
|
-
if (
|
|
1054
|
+
if (isVerticalMode || isBothMode) {
|
|
1152
1055
|
const oldHeight = itemSizesY.get(index);
|
|
1153
1056
|
const targetHeight = blockSize + gap;
|
|
1154
|
-
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
measuredItemsY[ index ] = 1;
|
|
1159
|
-
needUpdate = true;
|
|
1160
|
-
}
|
|
1161
|
-
} else if (Math.abs(oldHeight - targetHeight) > 0.5 && (targetHeight > oldHeight || !measuredItemsY[ index ])) {
|
|
1162
|
-
itemSizesY.update(index, targetHeight - oldHeight);
|
|
1057
|
+
|
|
1058
|
+
if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.1) {
|
|
1059
|
+
const d = targetHeight - oldHeight;
|
|
1060
|
+
itemSizesY.update(index, d);
|
|
1163
1061
|
measuredItemsY[ index ] = 1;
|
|
1164
1062
|
needUpdate = true;
|
|
1063
|
+
if (index < firstRowIndex) {
|
|
1064
|
+
deltaY += d;
|
|
1065
|
+
}
|
|
1165
1066
|
}
|
|
1166
1067
|
}
|
|
1167
1068
|
}
|
|
1168
1069
|
|
|
1169
1070
|
// Dynamic column width measurement
|
|
1071
|
+
const isColMeasurable = isDynamicColumnWidth.value || typeof props.value.columnWidth === 'function';
|
|
1170
1072
|
if (
|
|
1171
|
-
|
|
1073
|
+
isBothMode
|
|
1172
1074
|
&& element
|
|
1173
1075
|
&& props.value.columnCount
|
|
1174
|
-
&&
|
|
1076
|
+
&& isColMeasurable
|
|
1077
|
+
&& (inlineSize > 0 || element.dataset.colIndex === undefined)
|
|
1175
1078
|
) {
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1182
|
-
|
|
1183
|
-
/* v8 ignore else -- @preserve */
|
|
1184
|
-
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0)) {
|
|
1185
|
-
const w = child.offsetWidth;
|
|
1079
|
+
const colIndexAttr = element.dataset.colIndex;
|
|
1080
|
+
if (colIndexAttr != null) {
|
|
1081
|
+
const colIndex = Number.parseInt(colIndexAttr, 10);
|
|
1082
|
+
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0) && !processedCols.has(colIndex)) {
|
|
1083
|
+
processedCols.add(colIndex);
|
|
1186
1084
|
const oldW = columnSizes.get(colIndex);
|
|
1187
|
-
const targetW =
|
|
1188
|
-
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1085
|
+
const targetW = inlineSize + columnGap;
|
|
1086
|
+
|
|
1087
|
+
if (!measuredColumns[ colIndex ] || Math.abs(oldW - targetW) > 0.1) {
|
|
1088
|
+
const d = targetW - oldW;
|
|
1089
|
+
if (Math.abs(d) > 0.1) {
|
|
1090
|
+
columnSizes.update(colIndex, d);
|
|
1091
|
+
needUpdate = true;
|
|
1092
|
+
if (colIndex < firstColIndex) {
|
|
1093
|
+
deltaX += d;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1191
1096
|
measuredColumns[ colIndex ] = 1;
|
|
1192
|
-
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
// If the element is a row, try to find cells with data-col-index
|
|
1101
|
+
const cells = element.dataset.colIndex !== undefined
|
|
1102
|
+
? [ element ]
|
|
1103
|
+
: Array.from(element.querySelectorAll('[data-col-index]')) as HTMLElement[];
|
|
1104
|
+
|
|
1105
|
+
for (const child of cells) {
|
|
1106
|
+
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1107
|
+
|
|
1108
|
+
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0) && !processedCols.has(colIndex)) {
|
|
1109
|
+
processedCols.add(colIndex);
|
|
1110
|
+
const rect = child.getBoundingClientRect();
|
|
1111
|
+
const w = rect.width;
|
|
1112
|
+
const oldW = columnSizes.get(colIndex);
|
|
1113
|
+
const targetW = w + columnGap;
|
|
1114
|
+
if (!measuredColumns[ colIndex ] || Math.abs(oldW - targetW) > 0.1) {
|
|
1115
|
+
const d = targetW - oldW;
|
|
1116
|
+
if (Math.abs(d) > 0.1) {
|
|
1117
|
+
columnSizes.update(colIndex, d);
|
|
1118
|
+
needUpdate = true;
|
|
1119
|
+
if (colIndex < firstColIndex) {
|
|
1120
|
+
deltaX += d;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
measuredColumns[ colIndex ] = 1;
|
|
1124
|
+
}
|
|
1193
1125
|
}
|
|
1194
1126
|
}
|
|
1195
1127
|
}
|
|
@@ -1198,6 +1130,17 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1198
1130
|
|
|
1199
1131
|
if (needUpdate) {
|
|
1200
1132
|
treeUpdateFlag.value++;
|
|
1133
|
+
// Only compensate if not in a programmatic scroll,
|
|
1134
|
+
// as it would interrupt the browser animation or explicit alignment.
|
|
1135
|
+
const hasPendingScroll = pendingScroll.value !== null || isProgrammaticScroll.value;
|
|
1136
|
+
|
|
1137
|
+
if (!hasPendingScroll && (deltaX !== 0 || deltaY !== 0)) {
|
|
1138
|
+
scrollToOffset(
|
|
1139
|
+
deltaX !== 0 ? currentRelX + deltaX : null,
|
|
1140
|
+
deltaY !== 0 ? currentRelY + deltaY : null,
|
|
1141
|
+
{ behavior: 'auto' },
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1201
1144
|
}
|
|
1202
1145
|
};
|
|
1203
1146
|
|
|
@@ -1217,14 +1160,61 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1217
1160
|
const checkPendingScroll = () => {
|
|
1218
1161
|
if (pendingScroll.value && !isHydrating.value) {
|
|
1219
1162
|
const { rowIndex, colIndex, options } = pendingScroll.value;
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1163
|
+
|
|
1164
|
+
const isSmooth = isScrollToIndexOptions(options) && options.behavior === 'smooth';
|
|
1165
|
+
|
|
1166
|
+
// If it's a smooth scroll, we wait until it's finished before correcting.
|
|
1167
|
+
if (isSmooth && isScrolling.value) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const { targetX, targetY } = calculateScrollTarget({
|
|
1172
|
+
rowIndex,
|
|
1173
|
+
colIndex,
|
|
1174
|
+
options,
|
|
1175
|
+
itemsLength: props.value.items.length,
|
|
1176
|
+
columnCount: props.value.columnCount || 0,
|
|
1177
|
+
direction: props.value.direction || 'vertical',
|
|
1178
|
+
usableWidth: usableWidth.value,
|
|
1179
|
+
usableHeight: usableHeight.value,
|
|
1180
|
+
totalWidth: totalWidth.value,
|
|
1181
|
+
totalHeight: totalHeight.value,
|
|
1182
|
+
gap: props.value.gap || 0,
|
|
1183
|
+
columnGap: props.value.columnGap || 0,
|
|
1184
|
+
fixedSize: fixedItemSize.value,
|
|
1185
|
+
fixedWidth: fixedColumnWidth.value,
|
|
1186
|
+
relativeScrollX: relativeScrollX.value,
|
|
1187
|
+
relativeScrollY: relativeScrollY.value,
|
|
1188
|
+
getItemSizeY: (idx) => itemSizesY.get(idx),
|
|
1189
|
+
getItemSizeX: (idx) => itemSizesX.get(idx),
|
|
1190
|
+
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
1191
|
+
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
1192
|
+
getColumnSize: (idx) => columnSizes.get(idx),
|
|
1193
|
+
getColumnQuery: (idx) => columnSizes.query(idx),
|
|
1194
|
+
stickyIndices: sortedStickyIndices.value,
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const tolerance = 1;
|
|
1198
|
+
const reachedX = (colIndex === null || colIndex === undefined) || Math.abs(relativeScrollX.value - targetX) < tolerance;
|
|
1199
|
+
const reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(relativeScrollY.value - targetY) < tolerance;
|
|
1200
|
+
|
|
1201
|
+
const isMeasuredX = colIndex == null || colIndex === undefined || measuredColumns[ colIndex ] === 1;
|
|
1202
|
+
const isMeasuredY = rowIndex == null || rowIndex === undefined || measuredItemsY[ rowIndex ] === 1;
|
|
1203
|
+
|
|
1204
|
+
if (reachedX && reachedY) {
|
|
1205
|
+
if (isMeasuredX && isMeasuredY) {
|
|
1206
|
+
pendingScroll.value = null;
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
const correctionOptions: ScrollToIndexOptions = isScrollToIndexOptions(options)
|
|
1210
|
+
? { ...options, isCorrection: true }
|
|
1211
|
+
: { align: options as ScrollAlignment | ScrollAlignmentOptions, isCorrection: true };
|
|
1212
|
+
scrollToIndex(rowIndex, colIndex, correctionOptions);
|
|
1213
|
+
}
|
|
1224
1214
|
}
|
|
1225
1215
|
};
|
|
1226
1216
|
|
|
1227
|
-
watch(treeUpdateFlag, checkPendingScroll);
|
|
1217
|
+
watch([ treeUpdateFlag, viewportWidth, viewportHeight ], checkPendingScroll);
|
|
1228
1218
|
|
|
1229
1219
|
watch(isScrolling, (scrolling) => {
|
|
1230
1220
|
if (!scrolling) {
|
|
@@ -1242,14 +1232,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1242
1232
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
1243
1233
|
|
|
1244
1234
|
if (container === window) {
|
|
1245
|
-
viewportWidth.value =
|
|
1246
|
-
viewportHeight.value =
|
|
1235
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
1236
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1247
1237
|
scrollX.value = window.scrollX;
|
|
1248
1238
|
scrollY.value = window.scrollY;
|
|
1249
1239
|
|
|
1250
1240
|
const onResize = () => {
|
|
1251
|
-
viewportWidth.value =
|
|
1252
|
-
viewportHeight.value =
|
|
1241
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
1242
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1253
1243
|
updateHostOffset();
|
|
1254
1244
|
};
|
|
1255
1245
|
window.addEventListener('resize', onResize);
|
|
@@ -1265,7 +1255,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1265
1255
|
|
|
1266
1256
|
resizeObserver = new ResizeObserver((entries) => {
|
|
1267
1257
|
for (const entry of entries) {
|
|
1268
|
-
/* v8 ignore else -- @preserve */
|
|
1269
1258
|
if (entry.target === container) {
|
|
1270
1259
|
viewportWidth.value = (container as HTMLElement).clientWidth;
|
|
1271
1260
|
viewportHeight.value = (container as HTMLElement).clientHeight;
|
|
@@ -1302,7 +1291,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1302
1291
|
: props.value.ssrRange?.start;
|
|
1303
1292
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1304
1293
|
|
|
1305
|
-
/* v8 ignore else -- @preserve */
|
|
1306
1294
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1307
1295
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1308
1296
|
}
|
|
@@ -1338,70 +1326,95 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1338
1326
|
|
|
1339
1327
|
return {
|
|
1340
1328
|
/**
|
|
1341
|
-
* Array of items
|
|
1329
|
+
* Array of items currently rendered in the DOM with their calculated offsets and sizes.
|
|
1330
|
+
* @see RenderedItem
|
|
1342
1331
|
*/
|
|
1343
1332
|
renderedItems,
|
|
1333
|
+
|
|
1344
1334
|
/**
|
|
1345
|
-
* Total calculated width of all items including gaps.
|
|
1335
|
+
* Total calculated width of all items including gaps (in pixels).
|
|
1346
1336
|
*/
|
|
1347
1337
|
totalWidth,
|
|
1338
|
+
|
|
1348
1339
|
/**
|
|
1349
|
-
* Total calculated height of all items including gaps.
|
|
1340
|
+
* Total calculated height of all items including gaps (in pixels).
|
|
1350
1341
|
*/
|
|
1351
1342
|
totalHeight,
|
|
1343
|
+
|
|
1352
1344
|
/**
|
|
1353
1345
|
* Detailed information about the current scroll state.
|
|
1354
|
-
* Includes currentIndex, scrollOffset, viewportSize, totalSize, and
|
|
1346
|
+
* Includes currentIndex, scrollOffset, viewportSize, totalSize, and scrolling status.
|
|
1347
|
+
* @see ScrollDetails
|
|
1355
1348
|
*/
|
|
1356
1349
|
scrollDetails,
|
|
1350
|
+
|
|
1357
1351
|
/**
|
|
1358
1352
|
* Programmatically scroll to a specific row and/or column.
|
|
1359
|
-
*
|
|
1360
|
-
* @param
|
|
1361
|
-
* @param
|
|
1353
|
+
*
|
|
1354
|
+
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
1355
|
+
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
1356
|
+
* @param options - Alignment and behavior options.
|
|
1357
|
+
* @see ScrollAlignment
|
|
1358
|
+
* @see ScrollToIndexOptions
|
|
1362
1359
|
*/
|
|
1363
1360
|
scrollToIndex,
|
|
1361
|
+
|
|
1364
1362
|
/**
|
|
1365
|
-
* Programmatically scroll to a specific pixel offset.
|
|
1366
|
-
*
|
|
1367
|
-
* @param
|
|
1368
|
-
* @param
|
|
1363
|
+
* Programmatically scroll to a specific pixel offset relative to the content start.
|
|
1364
|
+
*
|
|
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.
|
|
1367
|
+
* @param options - Scroll options (behavior).
|
|
1369
1368
|
*/
|
|
1370
1369
|
scrollToOffset,
|
|
1370
|
+
|
|
1371
1371
|
/**
|
|
1372
|
-
* Stops any currently active
|
|
1372
|
+
* Stops any currently active smooth scroll animation and clears pending corrections.
|
|
1373
1373
|
*/
|
|
1374
1374
|
stopProgrammaticScroll,
|
|
1375
|
+
|
|
1375
1376
|
/**
|
|
1376
1377
|
* Updates the stored size of an item. Should be called when an item is measured (e.g., via ResizeObserver).
|
|
1377
|
-
*
|
|
1378
|
-
* @param
|
|
1379
|
-
* @param
|
|
1380
|
-
* @param
|
|
1378
|
+
*
|
|
1379
|
+
* @param index - The item index.
|
|
1380
|
+
* @param width - The measured inlineSize (width).
|
|
1381
|
+
* @param height - The measured blockSize (height).
|
|
1382
|
+
* @param element - The measured element (optional, used for robust grid column detection).
|
|
1381
1383
|
*/
|
|
1382
1384
|
updateItemSize,
|
|
1385
|
+
|
|
1383
1386
|
/**
|
|
1384
|
-
* Updates the stored size of multiple items
|
|
1385
|
-
*
|
|
1387
|
+
* Updates the stored size of multiple items simultaneously.
|
|
1388
|
+
*
|
|
1389
|
+
* @param updates - Array of measurement updates.
|
|
1386
1390
|
*/
|
|
1387
1391
|
updateItemSizes,
|
|
1392
|
+
|
|
1388
1393
|
/**
|
|
1389
1394
|
* Recalculates the host element's offset relative to the scroll container.
|
|
1395
|
+
* Useful if the container or host moves without a resize event.
|
|
1390
1396
|
*/
|
|
1391
1397
|
updateHostOffset,
|
|
1398
|
+
|
|
1392
1399
|
/**
|
|
1393
|
-
* Information about the current visible range of columns.
|
|
1400
|
+
* Information about the current visible range of columns and their paddings.
|
|
1401
|
+
* @see ColumnRange
|
|
1394
1402
|
*/
|
|
1395
1403
|
columnRange,
|
|
1404
|
+
|
|
1396
1405
|
/**
|
|
1397
|
-
* Helper to get the width of a specific column based on current configuration.
|
|
1398
|
-
*
|
|
1406
|
+
* Helper to get the width of a specific column based on current configuration and measurements.
|
|
1407
|
+
*
|
|
1408
|
+
* @param index - The column index.
|
|
1399
1409
|
*/
|
|
1400
1410
|
getColumnWidth,
|
|
1411
|
+
|
|
1401
1412
|
/**
|
|
1402
1413
|
* Resets all dynamic measurements and re-initializes from props.
|
|
1414
|
+
* Useful if item sizes have changed externally.
|
|
1403
1415
|
*/
|
|
1404
1416
|
refresh,
|
|
1417
|
+
|
|
1405
1418
|
/**
|
|
1406
1419
|
* Whether the component has finished its first client-side mount and hydration.
|
|
1407
1420
|
*/
|