@pdanpdan/virtual-scroll 0.3.0 → 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 +160 -116
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +834 -145
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +639 -416
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +1 -1
- package/src/components/VirtualScroll.test.ts +523 -731
- package/src/components/VirtualScroll.vue +343 -214
- package/src/composables/useVirtualScroll.test.ts +240 -1879
- package/src/composables/useVirtualScroll.ts +482 -554
- 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
|
@@ -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,138 +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';
|
|
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
|
+
};
|
|
8
35
|
|
|
9
36
|
export const DEFAULT_ITEM_SIZE = 40;
|
|
10
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
|
-
/** Options for scroll alignment in a single axis or both axes. */
|
|
17
|
-
export interface ScrollAlignmentOptions {
|
|
18
|
-
/** Alignment on the X axis. */
|
|
19
|
-
x?: ScrollAlignment;
|
|
20
|
-
/** Alignment on the Y axis. */
|
|
21
|
-
y?: ScrollAlignment;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Options for the scrollToIndex method. */
|
|
25
|
-
export interface ScrollToIndexOptions {
|
|
26
|
-
/** Where to align the item in the viewport. */
|
|
27
|
-
align?: ScrollAlignment | ScrollAlignmentOptions;
|
|
28
|
-
/** Scroll behavior. */
|
|
29
|
-
behavior?: 'auto' | 'smooth';
|
|
30
|
-
/** Internal flag for recursive correction calls. */
|
|
31
|
-
isCorrection?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Configuration properties for the useVirtualScroll composable. */
|
|
35
|
-
export interface VirtualScrollProps<T = unknown> {
|
|
36
|
-
/** Array of items to be virtualized. */
|
|
37
|
-
items: T[];
|
|
38
|
-
/** Fixed size of each item or a function that returns the size of an item. */
|
|
39
|
-
itemSize?: number | ((item: T, index: number) => number) | undefined;
|
|
40
|
-
/** Direction of the scroll: 'vertical', 'horizontal', or 'both'. */
|
|
41
|
-
direction?: ScrollDirection | undefined;
|
|
42
|
-
/** Number of items to render before the visible viewport. */
|
|
43
|
-
bufferBefore?: number | undefined;
|
|
44
|
-
/** Number of items to render after the visible viewport. */
|
|
45
|
-
bufferAfter?: number | undefined;
|
|
46
|
-
/** The scrollable container element or window. */
|
|
47
|
-
container?: HTMLElement | Window | null | undefined;
|
|
48
|
-
/** The host element that contains the items. */
|
|
49
|
-
hostElement?: HTMLElement | null | undefined;
|
|
50
|
-
/** Range of items to render for SSR. */
|
|
51
|
-
ssrRange?: {
|
|
52
|
-
start: number;
|
|
53
|
-
end: number;
|
|
54
|
-
colStart?: number;
|
|
55
|
-
colEnd?: number;
|
|
56
|
-
} | undefined;
|
|
57
|
-
/** Number of columns for bidirectional scroll. */
|
|
58
|
-
columnCount?: number | undefined;
|
|
59
|
-
/** Fixed width of columns or an array/function for column widths. */
|
|
60
|
-
columnWidth?: number | number[] | ((index: number) => number) | undefined;
|
|
61
|
-
/** Padding at the start of the scroll container. */
|
|
62
|
-
scrollPaddingStart?: number | { x?: number; y?: number; } | undefined;
|
|
63
|
-
/** Padding at the end of the scroll container. */
|
|
64
|
-
scrollPaddingEnd?: number | { x?: number; y?: number; } | undefined;
|
|
65
|
-
/** Gap between items in pixels (vertical). */
|
|
66
|
-
gap?: number | undefined;
|
|
67
|
-
/** Gap between columns in pixels (horizontal/grid). */
|
|
68
|
-
columnGap?: number | undefined;
|
|
69
|
-
/** Indices of items that should stick to the top/start. */
|
|
70
|
-
stickyIndices?: number[] | undefined;
|
|
71
|
-
/** Distance from the end of the scrollable area to trigger 'load' event. */
|
|
72
|
-
loadDistance?: number | undefined;
|
|
73
|
-
/** Whether items are currently being loaded. */
|
|
74
|
-
loading?: boolean | undefined;
|
|
75
|
-
/** Whether to restore scroll position when items are prepended. */
|
|
76
|
-
restoreScrollOnPrepend?: boolean | undefined;
|
|
77
|
-
/** Initial scroll index to jump to on mount. */
|
|
78
|
-
initialScrollIndex?: number | undefined;
|
|
79
|
-
/** Alignment for the initial scroll index. */
|
|
80
|
-
initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions | undefined;
|
|
81
|
-
/** Default size for items before they are measured. */
|
|
82
|
-
defaultItemSize?: number | undefined;
|
|
83
|
-
/** Default width for columns before they are measured. */
|
|
84
|
-
defaultColumnWidth?: number | undefined;
|
|
85
|
-
/** Whether to enable debug mode. */
|
|
86
|
-
debug?: boolean | undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Represents an item currently rendered in the virtual scroll area. */
|
|
90
|
-
export interface RenderedItem<T = unknown> {
|
|
91
|
-
/** The original data item. */
|
|
92
|
-
item: T;
|
|
93
|
-
/** The index of the item in the original array. */
|
|
94
|
-
index: number;
|
|
95
|
-
/** The calculated offset relative to the host element. */
|
|
96
|
-
offset: { x: number; y: number; };
|
|
97
|
-
/** The current measured or estimated size. */
|
|
98
|
-
size: { width: number; height: number; };
|
|
99
|
-
/** The original X offset before sticky adjustments. */
|
|
100
|
-
originalX: number;
|
|
101
|
-
/** The original Y offset before sticky adjustments. */
|
|
102
|
-
originalY: number;
|
|
103
|
-
/** Whether this item is configured to be sticky. */
|
|
104
|
-
isSticky?: boolean;
|
|
105
|
-
/** Whether this item is currently stuck at the threshold. */
|
|
106
|
-
isStickyActive?: boolean;
|
|
107
|
-
/** The offset applied for the sticky pushing effect. */
|
|
108
|
-
stickyOffset: { x: number; y: number; };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Comprehensive state of the virtual scroll system. */
|
|
112
|
-
export interface ScrollDetails<T = unknown> {
|
|
113
|
-
/** List of items currently rendered. */
|
|
114
|
-
items: RenderedItem<T>[];
|
|
115
|
-
/** Index of the first item partially or fully visible in the viewport. */
|
|
116
|
-
currentIndex: number;
|
|
117
|
-
/** Index of the first column partially or fully visible. */
|
|
118
|
-
currentColIndex: number;
|
|
119
|
-
/** Current scroll position relative to content start. */
|
|
120
|
-
scrollOffset: { x: number; y: number; };
|
|
121
|
-
/** Dimensions of the visible viewport. */
|
|
122
|
-
viewportSize: { width: number; height: number; };
|
|
123
|
-
/** Total calculated size of all items and gaps. */
|
|
124
|
-
totalSize: { width: number; height: number; };
|
|
125
|
-
/** Whether the container is currently being scrolled. */
|
|
126
|
-
isScrolling: boolean;
|
|
127
|
-
/** Whether the current scroll was initiated by a method call. */
|
|
128
|
-
isProgrammaticScroll: boolean;
|
|
129
|
-
/** Range of items currently being rendered. */
|
|
130
|
-
range: { start: number; end: number; };
|
|
131
|
-
/** Range of columns currently being rendered (for grid mode). */
|
|
132
|
-
columnRange: { start: number; end: number; padStart: number; padEnd: number; };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
40
|
/**
|
|
136
41
|
* Composable for virtual scrolling logic.
|
|
137
|
-
* Handles calculation of visible items, scroll events,
|
|
42
|
+
* Handles calculation of visible items, scroll events, dynamic item sizes, and programmatic scrolling.
|
|
138
43
|
*
|
|
139
|
-
* @param props -
|
|
44
|
+
* @param props - A Ref to the configuration properties.
|
|
45
|
+
* @see VirtualScrollProps
|
|
140
46
|
*/
|
|
141
47
|
export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>) {
|
|
142
48
|
// --- State ---
|
|
@@ -154,8 +60,8 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
154
60
|
const isProgrammaticScroll = ref(false);
|
|
155
61
|
|
|
156
62
|
// --- Fenwick Trees for efficient size and offset management ---
|
|
157
|
-
const itemSizesX = new FenwickTree(props.value.items
|
|
158
|
-
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);
|
|
159
65
|
const columnSizes = new FenwickTree(props.value.columnCount || 0);
|
|
160
66
|
|
|
161
67
|
const treeUpdateFlag = ref(0);
|
|
@@ -188,12 +94,33 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
188
94
|
(typeof props.value.itemSize === 'number' && props.value.itemSize > 0) ? props.value.itemSize : null,
|
|
189
95
|
);
|
|
190
96
|
|
|
97
|
+
const fixedColumnWidth = computed(() =>
|
|
98
|
+
(typeof props.value.columnWidth === 'number' && props.value.columnWidth > 0) ? props.value.columnWidth : null,
|
|
99
|
+
);
|
|
100
|
+
|
|
191
101
|
const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
|
|
192
102
|
|
|
193
103
|
const sortedStickyIndices = computed(() =>
|
|
194
104
|
[ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
|
|
195
105
|
);
|
|
196
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
|
+
|
|
197
124
|
// --- Size Calculations ---
|
|
198
125
|
/**
|
|
199
126
|
* Total width of all items in the scrollable area.
|
|
@@ -213,7 +140,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
213
140
|
const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
|
|
214
141
|
return Math.max(0, total - (effectiveColEnd > colStart ? (props.value.columnGap || 0) : 0));
|
|
215
142
|
}
|
|
216
|
-
/* v8 ignore else -- @preserve */
|
|
217
143
|
if (props.value.direction === 'horizontal') {
|
|
218
144
|
if (fixedItemSize.value !== null) {
|
|
219
145
|
const len = end - start;
|
|
@@ -224,23 +150,20 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
224
150
|
}
|
|
225
151
|
}
|
|
226
152
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
const total = itemSizesX.query(props.value.items.length);
|
|
243
|
-
return Math.max(0, total - (props.value.items.length > 0 ? (props.value.columnGap || 0) : 0));
|
|
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;
|
|
244
167
|
});
|
|
245
168
|
|
|
246
169
|
/**
|
|
@@ -252,7 +175,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
252
175
|
|
|
253
176
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
254
177
|
const { start, end } = props.value.ssrRange;
|
|
255
|
-
/* v8 ignore else -- @preserve */
|
|
256
178
|
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
257
179
|
if (fixedItemSize.value !== null) {
|
|
258
180
|
const len = end - start;
|
|
@@ -263,15 +185,20 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
263
185
|
}
|
|
264
186
|
}
|
|
265
187
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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;
|
|
275
202
|
});
|
|
276
203
|
|
|
277
204
|
const relativeScrollX = computed(() => {
|
|
@@ -298,7 +225,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
298
225
|
const val = cw[ index % cw.length ];
|
|
299
226
|
return (val != null && val > 0) ? val : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
|
|
300
227
|
}
|
|
301
|
-
/* v8 ignore else -- @preserve */
|
|
302
228
|
if (typeof cw === 'function') {
|
|
303
229
|
return cw(index);
|
|
304
230
|
}
|
|
@@ -312,6 +238,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
312
238
|
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
313
239
|
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
314
240
|
* @param options - Scroll options including alignment ('start', 'center', 'end', 'auto') and behavior ('auto', 'smooth').
|
|
241
|
+
* Defaults to { align: 'auto', behavior: 'auto' }.
|
|
315
242
|
*/
|
|
316
243
|
const scrollToIndex = (
|
|
317
244
|
rowIndex: number | null | undefined,
|
|
@@ -322,162 +249,55 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
322
249
|
? options.isCorrection
|
|
323
250
|
: false;
|
|
324
251
|
|
|
325
|
-
if (!isCorrection) {
|
|
326
|
-
pendingScroll.value = { rowIndex, colIndex, options };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
252
|
const container = props.value.container || window;
|
|
330
|
-
const fixedSize = fixedItemSize.value;
|
|
331
|
-
const gap = props.value.gap || 0;
|
|
332
|
-
const columnGap = props.value.columnGap || 0;
|
|
333
|
-
|
|
334
|
-
let align: ScrollAlignment | ScrollAlignmentOptions | undefined;
|
|
335
|
-
let behavior: 'auto' | 'smooth' | undefined;
|
|
336
|
-
|
|
337
|
-
if (isScrollToIndexOptions(options)) {
|
|
338
|
-
align = options.align;
|
|
339
|
-
behavior = options.behavior;
|
|
340
|
-
} else {
|
|
341
|
-
align = options as ScrollAlignment | ScrollAlignmentOptions;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const alignX = (typeof align === 'object' ? align.x : align) || 'auto';
|
|
345
|
-
const alignY = (typeof align === 'object' ? align.y : align) || 'auto';
|
|
346
|
-
|
|
347
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
348
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
|
|
349
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
|
|
350
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
|
|
351
253
|
|
|
352
254
|
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
353
255
|
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
354
256
|
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
} else {
|
|
381
|
-
const isVisibleY = targetY >= relativeScrollY.value && (targetY + itemHeight) <= (relativeScrollY.value + usableHeight);
|
|
382
|
-
if (!isVisibleY) {
|
|
383
|
-
if (targetY < relativeScrollY.value) {
|
|
384
|
-
// keep targetY at start
|
|
385
|
-
} else {
|
|
386
|
-
targetY -= (usableHeight - itemHeight);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// X calculation
|
|
393
|
-
if (colIndex !== null && colIndex !== undefined) {
|
|
394
|
-
const totalCols = props.value.columnCount || 0;
|
|
395
|
-
if (colIndex >= totalCols && totalCols > 0) {
|
|
396
|
-
targetX = totalWidth.value;
|
|
397
|
-
itemWidth = 0;
|
|
398
|
-
} else if (props.value.direction === 'horizontal') {
|
|
399
|
-
targetX = fixedSize !== null ? colIndex * (fixedSize + columnGap) : itemSizesX.query(colIndex);
|
|
400
|
-
itemWidth = fixedSize !== null ? fixedSize : itemSizesX.get(colIndex) - columnGap;
|
|
401
|
-
} else {
|
|
402
|
-
targetX = columnSizes.query(colIndex);
|
|
403
|
-
itemWidth = columnSizes.get(colIndex) - columnGap;
|
|
404
|
-
}
|
|
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
|
+
});
|
|
405
282
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
/* v8 ignore if -- @preserve */
|
|
417
|
-
if (targetX < relativeScrollX.value) {
|
|
418
|
-
// keep targetX at start
|
|
419
|
-
} else {
|
|
420
|
-
targetX -= (usableWidth - itemWidth);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
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
|
+
};
|
|
424
293
|
}
|
|
425
294
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight.value - usableHeight)));
|
|
429
|
-
|
|
430
|
-
const finalX = targetX + hostOffset.x - (isHorizontal ? paddingStartX : 0);
|
|
431
|
-
const finalY = targetY + hostOffset.y - (isVertical ? paddingStartY : 0);
|
|
432
|
-
|
|
433
|
-
// Check if we reached the target
|
|
434
|
-
const tolerance = 1;
|
|
435
|
-
let reachedX = (colIndex === null || colIndex === undefined) || Math.abs(relativeScrollX.value - targetX) < tolerance;
|
|
436
|
-
let reachedY = (rowIndex === null || rowIndex === undefined) || Math.abs(relativeScrollY.value - targetY) < tolerance;
|
|
437
|
-
|
|
438
|
-
if (!reachedX || !reachedY) {
|
|
439
|
-
let curX = 0;
|
|
440
|
-
let curY = 0;
|
|
441
|
-
let maxW = 0;
|
|
442
|
-
let maxH = 0;
|
|
443
|
-
let viewW = 0;
|
|
444
|
-
let viewH = 0;
|
|
445
|
-
|
|
446
|
-
/* v8 ignore else -- @preserve */
|
|
447
|
-
if (typeof window !== 'undefined') {
|
|
448
|
-
if (container === window) {
|
|
449
|
-
curX = window.scrollX;
|
|
450
|
-
curY = window.scrollY;
|
|
451
|
-
maxW = document.documentElement.scrollWidth;
|
|
452
|
-
maxH = document.documentElement.scrollHeight;
|
|
453
|
-
viewW = window.innerWidth;
|
|
454
|
-
viewH = window.innerHeight;
|
|
455
|
-
} else if (isElement(container)) {
|
|
456
|
-
curX = container.scrollLeft;
|
|
457
|
-
curY = container.scrollTop;
|
|
458
|
-
maxW = container.scrollWidth;
|
|
459
|
-
maxH = container.scrollHeight;
|
|
460
|
-
viewW = container.clientWidth;
|
|
461
|
-
viewH = container.clientHeight;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!reachedX && colIndex !== null && colIndex !== undefined) {
|
|
465
|
-
const atLeft = curX <= tolerance && finalX <= tolerance;
|
|
466
|
-
const atRight = curX >= maxW - viewW - tolerance && finalX >= maxW - viewW - tolerance;
|
|
467
|
-
/* v8 ignore else -- @preserve */
|
|
468
|
-
if (atLeft || atRight) {
|
|
469
|
-
reachedX = true;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
295
|
+
const finalX = targetX + hostOffset.x - (isHorizontal ? paddingStartX.value : 0);
|
|
296
|
+
const finalY = targetY + hostOffset.y - (isVertical ? paddingStartY.value : 0);
|
|
472
297
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (atTop || atBottom) {
|
|
477
|
-
reachedY = true;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
298
|
+
let behavior: 'auto' | 'smooth' | undefined;
|
|
299
|
+
if (isScrollToIndexOptions(options)) {
|
|
300
|
+
behavior = options.behavior;
|
|
481
301
|
}
|
|
482
302
|
|
|
483
303
|
const scrollBehavior = isCorrection ? 'auto' : (behavior || 'smooth');
|
|
@@ -522,9 +342,9 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
522
342
|
}
|
|
523
343
|
}
|
|
524
344
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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.
|
|
528
348
|
};
|
|
529
349
|
|
|
530
350
|
/**
|
|
@@ -533,7 +353,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
533
353
|
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
534
354
|
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
535
355
|
* @param options - Scroll options (behavior)
|
|
536
|
-
* @param options.behavior - The scroll behavior ('auto' | 'smooth')
|
|
356
|
+
* @param options.behavior - The scroll behavior ('auto' | 'smooth'). Defaults to 'auto'.
|
|
537
357
|
*/
|
|
538
358
|
const scrollToOffset = (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => {
|
|
539
359
|
const container = props.value.container || window;
|
|
@@ -542,26 +362,18 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
542
362
|
const isVertical = props.value.direction === 'vertical' || props.value.direction === 'both';
|
|
543
363
|
const isHorizontal = props.value.direction === 'horizontal' || props.value.direction === 'both';
|
|
544
364
|
|
|
545
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
546
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
|
|
547
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
|
|
548
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
|
|
549
|
-
|
|
550
|
-
const usableWidth = viewportWidth.value - (isHorizontal ? (paddingStartX + paddingEndX) : 0);
|
|
551
|
-
const usableHeight = viewportHeight.value - (isVertical ? (paddingStartY + paddingEndY) : 0);
|
|
552
|
-
|
|
553
365
|
const clampedX = (x !== null && x !== undefined)
|
|
554
|
-
? (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))
|
|
555
367
|
: null;
|
|
556
368
|
const clampedY = (y !== null && y !== undefined)
|
|
557
|
-
? (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))
|
|
558
370
|
: null;
|
|
559
371
|
|
|
560
372
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
561
373
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
562
374
|
|
|
563
|
-
const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
|
|
564
|
-
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;
|
|
565
377
|
|
|
566
378
|
if (typeof window !== 'undefined' && container === window) {
|
|
567
379
|
window.scrollTo({
|
|
@@ -632,7 +444,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
632
444
|
let prependCount = 0;
|
|
633
445
|
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
634
446
|
const oldFirstItem = lastItems[ 0 ];
|
|
635
|
-
/* v8 ignore else -- @preserve */
|
|
636
447
|
if (oldFirstItem !== undefined) {
|
|
637
448
|
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
638
449
|
if (newItems[ i ] === oldFirstItem) {
|
|
@@ -676,7 +487,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
676
487
|
}
|
|
677
488
|
}
|
|
678
489
|
|
|
679
|
-
/* v8 ignore else -- @preserve */
|
|
680
490
|
if (addedX > 0 || addedY > 0) {
|
|
681
491
|
nextTick(() => {
|
|
682
492
|
scrollToOffset(
|
|
@@ -688,18 +498,29 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
688
498
|
}
|
|
689
499
|
}
|
|
690
500
|
|
|
691
|
-
// Initialize columns
|
|
501
|
+
// Initialize columns
|
|
692
502
|
if (colCount > 0) {
|
|
693
503
|
const columnGap = props.value.columnGap || 0;
|
|
694
504
|
let colNeedsRebuild = false;
|
|
505
|
+
const cw = props.value.columnWidth;
|
|
506
|
+
|
|
695
507
|
for (let i = 0; i < colCount; i++) {
|
|
696
|
-
const width = getColumnWidth(i);
|
|
697
508
|
const currentW = columnSizes.get(i);
|
|
698
509
|
const isMeasured = measuredColumns[ i ] === 1;
|
|
699
510
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
}
|
|
522
|
+
|
|
523
|
+
const targetW = baseWidth + columnGap;
|
|
703
524
|
if (Math.abs(currentW - targetW) > 0.5) {
|
|
704
525
|
columnSizes.set(i, targetW);
|
|
705
526
|
measuredColumns[ i ] = isDynamicColumnWidth.value ? 0 : 1;
|
|
@@ -723,24 +544,21 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
723
544
|
const currentX = itemSizesX.get(i);
|
|
724
545
|
const currentY = itemSizesY.get(i);
|
|
725
546
|
|
|
726
|
-
const size = typeof props.value.itemSize === 'function'
|
|
727
|
-
? props.value.itemSize(item as T, i)
|
|
728
|
-
: defaultSize.value;
|
|
729
|
-
|
|
730
547
|
const isVertical = props.value.direction === 'vertical';
|
|
731
548
|
const isHorizontal = props.value.direction === 'horizontal';
|
|
732
549
|
const isBoth = props.value.direction === 'both';
|
|
733
550
|
|
|
734
|
-
const targetX = isHorizontal ? size + columnGap : 0;
|
|
735
|
-
const targetY = (isVertical || isBoth) ? size + gap : 0;
|
|
736
|
-
|
|
737
551
|
const isMeasuredX = measuredItemsX[ i ] === 1;
|
|
738
552
|
const isMeasuredY = measuredItemsY[ i ] === 1;
|
|
739
553
|
|
|
740
554
|
// Logic for X
|
|
741
555
|
if (isHorizontal) {
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
|
|
744
562
|
if (Math.abs(currentX - targetX) > 0.5) {
|
|
745
563
|
itemSizesX.set(i, targetX);
|
|
746
564
|
measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
@@ -757,7 +575,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
757
575
|
|
|
758
576
|
// Logic for Y
|
|
759
577
|
if (isVertical || isBoth) {
|
|
760
|
-
if (!isDynamicItemSize.value || !isMeasuredY
|
|
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
|
+
|
|
761
584
|
if (Math.abs(currentY - targetY) > 0.5) {
|
|
762
585
|
itemSizesY.set(i, targetY);
|
|
763
586
|
measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
@@ -815,6 +638,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
815
638
|
|
|
816
639
|
watch([
|
|
817
640
|
() => props.value.items,
|
|
641
|
+
() => props.value.items.length,
|
|
818
642
|
() => props.value.direction,
|
|
819
643
|
() => props.value.columnCount,
|
|
820
644
|
() => props.value.columnWidth,
|
|
@@ -844,58 +668,26 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
844
668
|
};
|
|
845
669
|
}
|
|
846
670
|
|
|
847
|
-
const direction = props.value.direction || 'vertical';
|
|
848
671
|
const bufferBefore = (props.value.ssrRange && !isScrolling.value) ? 0 : (props.value.bufferBefore ?? DEFAULT_BUFFER);
|
|
849
672
|
const bufferAfter = props.value.bufferAfter ?? DEFAULT_BUFFER;
|
|
850
|
-
const gap = props.value.gap || 0;
|
|
851
|
-
const columnGap = props.value.columnGap || 0;
|
|
852
|
-
const fixedSize = fixedItemSize.value;
|
|
853
|
-
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, direction);
|
|
854
|
-
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, direction);
|
|
855
|
-
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, direction);
|
|
856
|
-
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, direction);
|
|
857
|
-
|
|
858
|
-
const isVertical = direction === 'vertical' || direction === 'both';
|
|
859
|
-
const isHorizontal = direction === 'horizontal' || direction === 'both';
|
|
860
673
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
end = i;
|
|
879
|
-
}
|
|
880
|
-
} else {
|
|
881
|
-
if (fixedSize !== null) {
|
|
882
|
-
start = Math.floor(relativeScrollX.value / (fixedSize + columnGap));
|
|
883
|
-
end = Math.ceil((relativeScrollX.value + usableWidth) / (fixedSize + columnGap));
|
|
884
|
-
} else {
|
|
885
|
-
start = itemSizesX.findLowerBound(relativeScrollX.value);
|
|
886
|
-
let currentX = itemSizesX.query(start);
|
|
887
|
-
let i = start;
|
|
888
|
-
while (i < props.value.items.length && currentX < relativeScrollX.value + usableWidth) {
|
|
889
|
-
currentX = itemSizesX.query(++i);
|
|
890
|
-
}
|
|
891
|
-
end = i;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return {
|
|
896
|
-
start: Math.max(0, start - bufferBefore),
|
|
897
|
-
end: Math.min(props.value.items.length, end + bufferAfter),
|
|
898
|
-
};
|
|
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
|
+
});
|
|
899
691
|
});
|
|
900
692
|
|
|
901
693
|
/**
|
|
@@ -924,6 +716,9 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
924
716
|
/**
|
|
925
717
|
* List of items to be rendered with their calculated offsets and sizes.
|
|
926
718
|
*/
|
|
719
|
+
|
|
720
|
+
let lastRenderedItems: RenderedItem<T>[] = [];
|
|
721
|
+
|
|
927
722
|
const renderedItems = computed<RenderedItem<T>[]>(() => {
|
|
928
723
|
// eslint-disable-next-line ts/no-unused-expressions
|
|
929
724
|
treeUpdateFlag.value;
|
|
@@ -934,6 +729,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
934
729
|
const gap = props.value.gap || 0;
|
|
935
730
|
const columnGap = props.value.columnGap || 0;
|
|
936
731
|
const stickyIndices = sortedStickyIndices.value;
|
|
732
|
+
const stickySet = stickyIndicesSet.value;
|
|
937
733
|
|
|
938
734
|
// Always include relevant sticky items
|
|
939
735
|
const indicesToRender = new Set<number>();
|
|
@@ -961,8 +757,27 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
961
757
|
indicesToRender.add(prevStickyIdx);
|
|
962
758
|
}
|
|
963
759
|
|
|
964
|
-
|
|
965
|
-
|
|
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
|
+
}
|
|
966
781
|
indicesToRender.add(idx);
|
|
967
782
|
}
|
|
968
783
|
}
|
|
@@ -988,102 +803,86 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
988
803
|
}
|
|
989
804
|
}
|
|
990
805
|
|
|
806
|
+
const lastItemsMap = new Map(lastRenderedItems.map((it) => [ it.index, it ]));
|
|
807
|
+
|
|
991
808
|
for (const i of sortedIndices) {
|
|
992
809
|
const item = props.value.items[ i ];
|
|
993
810
|
if (item === undefined) {
|
|
994
811
|
continue;
|
|
995
812
|
}
|
|
996
813
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
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
|
+
});
|
|
1012
828
|
|
|
1013
|
-
const isSticky =
|
|
829
|
+
const isSticky = stickySet.has(i);
|
|
1014
830
|
const originalX = x;
|
|
1015
831
|
const originalY = y;
|
|
1016
|
-
let isStickyActive = false;
|
|
1017
|
-
const stickyOffset = { x: 0, y: 0 };
|
|
1018
|
-
|
|
1019
|
-
if (isSticky) {
|
|
1020
|
-
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
1021
|
-
if (relativeScrollY.value > originalY) {
|
|
1022
|
-
isStickyActive = true;
|
|
1023
|
-
// Check if next sticky item pushes this one
|
|
1024
|
-
let nextStickyIdx: number | undefined;
|
|
1025
|
-
let low = 0;
|
|
1026
|
-
let high = stickyIndices.length - 1;
|
|
1027
|
-
while (low <= high) {
|
|
1028
|
-
const mid = (low + high) >>> 1;
|
|
1029
|
-
if (stickyIndices[ mid ]! > i) {
|
|
1030
|
-
nextStickyIdx = stickyIndices[ mid ];
|
|
1031
|
-
high = mid - 1;
|
|
1032
|
-
} else {
|
|
1033
|
-
low = mid + 1;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
832
|
|
|
1037
|
-
|
|
1038
|
-
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : itemSizesY.query(nextStickyIdx);
|
|
1039
|
-
const distance = nextStickyY - relativeScrollY.value;
|
|
1040
|
-
/* v8 ignore else -- @preserve */
|
|
1041
|
-
if (distance < height) {
|
|
1042
|
-
stickyOffset.y = -(height - distance);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
} else if (props.value.direction === 'horizontal') {
|
|
1047
|
-
if (relativeScrollX.value > originalX) {
|
|
1048
|
-
isStickyActive = true;
|
|
1049
|
-
// Check if next sticky item pushes this one
|
|
1050
|
-
let nextStickyIdx: number | undefined;
|
|
1051
|
-
let low = 0;
|
|
1052
|
-
let high = stickyIndices.length - 1;
|
|
1053
|
-
while (low <= high) {
|
|
1054
|
-
const mid = (low + high) >>> 1;
|
|
1055
|
-
if (stickyIndices[ mid ]! > i) {
|
|
1056
|
-
nextStickyIdx = stickyIndices[ mid ];
|
|
1057
|
-
high = mid - 1;
|
|
1058
|
-
} else {
|
|
1059
|
-
low = mid + 1;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (nextStickyIdx !== undefined) {
|
|
1064
|
-
const nextStickyX = fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : itemSizesX.query(nextStickyIdx);
|
|
1065
|
-
const distance = nextStickyX - relativeScrollX.value;
|
|
1066
|
-
/* v8 ignore else -- @preserve */
|
|
1067
|
-
if (distance < width) {
|
|
1068
|
-
stickyOffset.x = -(width - distance);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
items.push({
|
|
1076
|
-
item,
|
|
833
|
+
const { isStickyActive, stickyOffset } = calculateStickyItem({
|
|
1077
834
|
index: i,
|
|
1078
|
-
|
|
1079
|
-
|
|
835
|
+
isSticky,
|
|
836
|
+
direction: props.value.direction || 'vertical',
|
|
837
|
+
relativeScrollX: relativeScrollX.value,
|
|
838
|
+
relativeScrollY: relativeScrollY.value,
|
|
1080
839
|
originalX,
|
|
1081
840
|
originalY,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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),
|
|
1085
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
|
+
}
|
|
1086
882
|
}
|
|
883
|
+
|
|
884
|
+
lastRenderedItems = items;
|
|
885
|
+
|
|
1087
886
|
return items;
|
|
1088
887
|
});
|
|
1089
888
|
|
|
@@ -1109,28 +908,19 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1109
908
|
};
|
|
1110
909
|
}
|
|
1111
910
|
|
|
1112
|
-
const start = columnSizes.findLowerBound(relativeScrollX.value);
|
|
1113
|
-
let currentX = columnSizes.query(start);
|
|
1114
|
-
let end = start;
|
|
1115
|
-
|
|
1116
|
-
while (end < totalCols && currentX < relativeScrollX.value + viewportWidth.value) {
|
|
1117
|
-
currentX = columnSizes.query(++end);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
911
|
const colBuffer = (props.value.ssrRange && !isScrolling.value) ? 0 : 2;
|
|
1121
912
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
};
|
|
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
|
+
});
|
|
1134
924
|
});
|
|
1135
925
|
|
|
1136
926
|
/**
|
|
@@ -1144,12 +934,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1144
934
|
const columnGap = props.value.columnGap || 0;
|
|
1145
935
|
|
|
1146
936
|
let currentColIndex = 0;
|
|
1147
|
-
if (props.value.direction === 'horizontal'
|
|
937
|
+
if (props.value.direction === 'horizontal') {
|
|
1148
938
|
if (fixedSize !== null) {
|
|
1149
939
|
currentColIndex = Math.floor(relativeScrollX.value / (fixedSize + columnGap));
|
|
1150
940
|
} else {
|
|
1151
941
|
currentColIndex = itemSizesX.findLowerBound(relativeScrollX.value);
|
|
1152
942
|
}
|
|
943
|
+
} else if (props.value.direction === 'both') {
|
|
944
|
+
currentColIndex = columnSizes.findLowerBound(relativeScrollX.value);
|
|
1153
945
|
}
|
|
1154
946
|
|
|
1155
947
|
return {
|
|
@@ -1187,9 +979,13 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1187
979
|
if (target === window || target === document) {
|
|
1188
980
|
scrollX.value = window.scrollX;
|
|
1189
981
|
scrollY.value = window.scrollY;
|
|
982
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
983
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1190
984
|
} else if (isScrollableElement(target)) {
|
|
1191
985
|
scrollX.value = target.scrollLeft;
|
|
1192
986
|
scrollY.value = target.scrollTop;
|
|
987
|
+
viewportWidth.value = target.clientWidth;
|
|
988
|
+
viewportHeight.value = target.clientHeight;
|
|
1193
989
|
}
|
|
1194
990
|
|
|
1195
991
|
if (!isScrolling.value) {
|
|
@@ -1212,41 +1008,61 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1212
1008
|
*/
|
|
1213
1009
|
const updateItemSizes = (updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement | undefined; }>) => {
|
|
1214
1010
|
let needUpdate = false;
|
|
1011
|
+
let deltaX = 0;
|
|
1012
|
+
let deltaY = 0;
|
|
1215
1013
|
const gap = props.value.gap || 0;
|
|
1216
1014
|
const columnGap = props.value.columnGap || 0;
|
|
1217
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
|
+
|
|
1218
1032
|
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1033
|
+
// Ignore 0-size measurements as they usually indicate hidden/detached elements
|
|
1034
|
+
if (inlineSize <= 0 && blockSize <= 0) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1219
1038
|
const isMeasurable = isDynamicItemSize.value || typeof props.value.itemSize === 'function';
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1039
|
+
if (index >= 0 && !processedRows.has(index) && isMeasurable && blockSize > 0) {
|
|
1040
|
+
processedRows.add(index);
|
|
1041
|
+
if (isHorizontalMode && inlineSize > 0) {
|
|
1222
1042
|
const oldWidth = itemSizesX.get(index);
|
|
1223
1043
|
const targetWidth = inlineSize + columnGap;
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
/* v8 ignore else -- @preserve */
|
|
1228
|
-
if (!measuredItemsX[ index ] || Math.abs(targetWidth - oldWidth) > 0.5) {
|
|
1229
|
-
itemSizesX.update(index, targetWidth - oldWidth);
|
|
1044
|
+
if (!measuredItemsX[ index ] || Math.abs(targetWidth - oldWidth) > 0.1) {
|
|
1045
|
+
const d = targetWidth - oldWidth;
|
|
1046
|
+
itemSizesX.update(index, d);
|
|
1230
1047
|
measuredItemsX[ index ] = 1;
|
|
1231
1048
|
needUpdate = true;
|
|
1049
|
+
if (index < firstRowIndex) {
|
|
1050
|
+
deltaX += d;
|
|
1051
|
+
}
|
|
1232
1052
|
}
|
|
1233
1053
|
}
|
|
1234
|
-
if (
|
|
1054
|
+
if (isVerticalMode || isBothMode) {
|
|
1235
1055
|
const oldHeight = itemSizesY.get(index);
|
|
1236
1056
|
const targetHeight = blockSize + gap;
|
|
1237
1057
|
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
|
|
1242
|
-
itemSizesY.update(index, targetHeight - oldHeight);
|
|
1243
|
-
measuredItemsY[ index ] = 1;
|
|
1244
|
-
needUpdate = true;
|
|
1245
|
-
}
|
|
1246
|
-
} else if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
|
|
1247
|
-
itemSizesY.update(index, targetHeight - oldHeight);
|
|
1058
|
+
if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.1) {
|
|
1059
|
+
const d = targetHeight - oldHeight;
|
|
1060
|
+
itemSizesY.update(index, d);
|
|
1248
1061
|
measuredItemsY[ index ] = 1;
|
|
1249
1062
|
needUpdate = true;
|
|
1063
|
+
if (index < firstRowIndex) {
|
|
1064
|
+
deltaY += d;
|
|
1065
|
+
}
|
|
1250
1066
|
}
|
|
1251
1067
|
}
|
|
1252
1068
|
}
|
|
@@ -1254,27 +1070,58 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1254
1070
|
// Dynamic column width measurement
|
|
1255
1071
|
const isColMeasurable = isDynamicColumnWidth.value || typeof props.value.columnWidth === 'function';
|
|
1256
1072
|
if (
|
|
1257
|
-
|
|
1073
|
+
isBothMode
|
|
1258
1074
|
&& element
|
|
1259
1075
|
&& props.value.columnCount
|
|
1260
1076
|
&& isColMeasurable
|
|
1077
|
+
&& (inlineSize > 0 || element.dataset.colIndex === undefined)
|
|
1261
1078
|
) {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1268
|
-
|
|
1269
|
-
/* v8 ignore else -- @preserve */
|
|
1270
|
-
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0)) {
|
|
1271
|
-
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);
|
|
1272
1084
|
const oldW = columnSizes.get(colIndex);
|
|
1273
|
-
const targetW =
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
+
}
|
|
1276
1096
|
measuredColumns[ colIndex ] = 1;
|
|
1277
|
-
|
|
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
|
+
}
|
|
1278
1125
|
}
|
|
1279
1126
|
}
|
|
1280
1127
|
}
|
|
@@ -1283,6 +1130,17 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1283
1130
|
|
|
1284
1131
|
if (needUpdate) {
|
|
1285
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
|
+
}
|
|
1286
1144
|
}
|
|
1287
1145
|
};
|
|
1288
1146
|
|
|
@@ -1302,14 +1160,61 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1302
1160
|
const checkPendingScroll = () => {
|
|
1303
1161
|
if (pendingScroll.value && !isHydrating.value) {
|
|
1304
1162
|
const { rowIndex, colIndex, options } = pendingScroll.value;
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
+
}
|
|
1309
1214
|
}
|
|
1310
1215
|
};
|
|
1311
1216
|
|
|
1312
|
-
watch(treeUpdateFlag, checkPendingScroll);
|
|
1217
|
+
watch([ treeUpdateFlag, viewportWidth, viewportHeight ], checkPendingScroll);
|
|
1313
1218
|
|
|
1314
1219
|
watch(isScrolling, (scrolling) => {
|
|
1315
1220
|
if (!scrolling) {
|
|
@@ -1327,14 +1232,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1327
1232
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
1328
1233
|
|
|
1329
1234
|
if (container === window) {
|
|
1330
|
-
viewportWidth.value =
|
|
1331
|
-
viewportHeight.value =
|
|
1235
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
1236
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1332
1237
|
scrollX.value = window.scrollX;
|
|
1333
1238
|
scrollY.value = window.scrollY;
|
|
1334
1239
|
|
|
1335
1240
|
const onResize = () => {
|
|
1336
|
-
viewportWidth.value =
|
|
1337
|
-
viewportHeight.value =
|
|
1241
|
+
viewportWidth.value = document.documentElement.clientWidth;
|
|
1242
|
+
viewportHeight.value = document.documentElement.clientHeight;
|
|
1338
1243
|
updateHostOffset();
|
|
1339
1244
|
};
|
|
1340
1245
|
window.addEventListener('resize', onResize);
|
|
@@ -1350,7 +1255,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1350
1255
|
|
|
1351
1256
|
resizeObserver = new ResizeObserver((entries) => {
|
|
1352
1257
|
for (const entry of entries) {
|
|
1353
|
-
/* v8 ignore else -- @preserve */
|
|
1354
1258
|
if (entry.target === container) {
|
|
1355
1259
|
viewportWidth.value = (container as HTMLElement).clientWidth;
|
|
1356
1260
|
viewportHeight.value = (container as HTMLElement).clientHeight;
|
|
@@ -1387,7 +1291,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1387
1291
|
: props.value.ssrRange?.start;
|
|
1388
1292
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1389
1293
|
|
|
1390
|
-
/* v8 ignore else -- @preserve */
|
|
1391
1294
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1392
1295
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1393
1296
|
}
|
|
@@ -1423,70 +1326,95 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1423
1326
|
|
|
1424
1327
|
return {
|
|
1425
1328
|
/**
|
|
1426
|
-
* Array of items
|
|
1329
|
+
* Array of items currently rendered in the DOM with their calculated offsets and sizes.
|
|
1330
|
+
* @see RenderedItem
|
|
1427
1331
|
*/
|
|
1428
1332
|
renderedItems,
|
|
1333
|
+
|
|
1429
1334
|
/**
|
|
1430
|
-
* Total calculated width of all items including gaps.
|
|
1335
|
+
* Total calculated width of all items including gaps (in pixels).
|
|
1431
1336
|
*/
|
|
1432
1337
|
totalWidth,
|
|
1338
|
+
|
|
1433
1339
|
/**
|
|
1434
|
-
* Total calculated height of all items including gaps.
|
|
1340
|
+
* Total calculated height of all items including gaps (in pixels).
|
|
1435
1341
|
*/
|
|
1436
1342
|
totalHeight,
|
|
1343
|
+
|
|
1437
1344
|
/**
|
|
1438
1345
|
* Detailed information about the current scroll state.
|
|
1439
|
-
* Includes currentIndex, scrollOffset, viewportSize, totalSize, and
|
|
1346
|
+
* Includes currentIndex, scrollOffset, viewportSize, totalSize, and scrolling status.
|
|
1347
|
+
* @see ScrollDetails
|
|
1440
1348
|
*/
|
|
1441
1349
|
scrollDetails,
|
|
1350
|
+
|
|
1442
1351
|
/**
|
|
1443
1352
|
* Programmatically scroll to a specific row and/or column.
|
|
1444
|
-
*
|
|
1445
|
-
* @param
|
|
1446
|
-
* @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
|
|
1447
1359
|
*/
|
|
1448
1360
|
scrollToIndex,
|
|
1361
|
+
|
|
1449
1362
|
/**
|
|
1450
|
-
* Programmatically scroll to a specific pixel offset.
|
|
1451
|
-
*
|
|
1452
|
-
* @param
|
|
1453
|
-
* @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).
|
|
1454
1368
|
*/
|
|
1455
1369
|
scrollToOffset,
|
|
1370
|
+
|
|
1456
1371
|
/**
|
|
1457
|
-
* Stops any currently active
|
|
1372
|
+
* Stops any currently active smooth scroll animation and clears pending corrections.
|
|
1458
1373
|
*/
|
|
1459
1374
|
stopProgrammaticScroll,
|
|
1375
|
+
|
|
1460
1376
|
/**
|
|
1461
1377
|
* Updates the stored size of an item. Should be called when an item is measured (e.g., via ResizeObserver).
|
|
1462
|
-
*
|
|
1463
|
-
* @param
|
|
1464
|
-
* @param
|
|
1465
|
-
* @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).
|
|
1466
1383
|
*/
|
|
1467
1384
|
updateItemSize,
|
|
1385
|
+
|
|
1468
1386
|
/**
|
|
1469
|
-
* Updates the stored size of multiple items
|
|
1470
|
-
*
|
|
1387
|
+
* Updates the stored size of multiple items simultaneously.
|
|
1388
|
+
*
|
|
1389
|
+
* @param updates - Array of measurement updates.
|
|
1471
1390
|
*/
|
|
1472
1391
|
updateItemSizes,
|
|
1392
|
+
|
|
1473
1393
|
/**
|
|
1474
1394
|
* Recalculates the host element's offset relative to the scroll container.
|
|
1395
|
+
* Useful if the container or host moves without a resize event.
|
|
1475
1396
|
*/
|
|
1476
1397
|
updateHostOffset,
|
|
1398
|
+
|
|
1477
1399
|
/**
|
|
1478
|
-
* Information about the current visible range of columns.
|
|
1400
|
+
* Information about the current visible range of columns and their paddings.
|
|
1401
|
+
* @see ColumnRange
|
|
1479
1402
|
*/
|
|
1480
1403
|
columnRange,
|
|
1404
|
+
|
|
1481
1405
|
/**
|
|
1482
|
-
* Helper to get the width of a specific column based on current configuration.
|
|
1483
|
-
*
|
|
1406
|
+
* Helper to get the width of a specific column based on current configuration and measurements.
|
|
1407
|
+
*
|
|
1408
|
+
* @param index - The column index.
|
|
1484
1409
|
*/
|
|
1485
1410
|
getColumnWidth,
|
|
1411
|
+
|
|
1486
1412
|
/**
|
|
1487
1413
|
* Resets all dynamic measurements and re-initializes from props.
|
|
1414
|
+
* Useful if item sizes have changed externally.
|
|
1488
1415
|
*/
|
|
1489
1416
|
refresh,
|
|
1417
|
+
|
|
1490
1418
|
/**
|
|
1491
1419
|
* Whether the component has finished its first client-side mount and hydration.
|
|
1492
1420
|
*/
|