@pdanpdan/virtual-scroll 0.2.0 → 0.3.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 +182 -88
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -35
- package/dist/index.js +1 -823
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +902 -0
- package/dist/index.mjs.map +1 -0
- package/dist/virtual-scroll.css +2 -0
- package/package.json +9 -6
- package/src/components/VirtualScroll.test.ts +397 -329
- package/src/components/VirtualScroll.vue +107 -25
- package/src/composables/useVirtualScroll.test.ts +1029 -255
- package/src/composables/useVirtualScroll.ts +176 -88
- package/src/utils/fenwick-tree.test.ts +80 -65
- package/dist/index.css +0 -2
|
@@ -6,102 +6,129 @@ import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactiv
|
|
|
6
6
|
import { FenwickTree } from '../utils/fenwick-tree';
|
|
7
7
|
import { getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions } from '../utils/scroll';
|
|
8
8
|
|
|
9
|
-
export const DEFAULT_ITEM_SIZE =
|
|
10
|
-
export const DEFAULT_COLUMN_WIDTH =
|
|
9
|
+
export const DEFAULT_ITEM_SIZE = 40;
|
|
10
|
+
export const DEFAULT_COLUMN_WIDTH = 100;
|
|
11
11
|
export const DEFAULT_BUFFER = 5;
|
|
12
12
|
|
|
13
13
|
export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
|
|
14
14
|
export type ScrollAlignment = 'start' | 'center' | 'end' | 'auto';
|
|
15
15
|
|
|
16
|
+
/** Options for scroll alignment in a single axis or both axes. */
|
|
16
17
|
export interface ScrollAlignmentOptions {
|
|
18
|
+
/** Alignment on the X axis. */
|
|
17
19
|
x?: ScrollAlignment;
|
|
20
|
+
/** Alignment on the Y axis. */
|
|
18
21
|
y?: ScrollAlignment;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/** Options for the scrollToIndex method. */
|
|
21
25
|
export interface ScrollToIndexOptions {
|
|
26
|
+
/** Where to align the item in the viewport. */
|
|
22
27
|
align?: ScrollAlignment | ScrollAlignmentOptions;
|
|
28
|
+
/** Scroll behavior. */
|
|
23
29
|
behavior?: 'auto' | 'smooth';
|
|
30
|
+
/** Internal flag for recursive correction calls. */
|
|
24
31
|
isCorrection?: boolean;
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/** Configuration properties for the useVirtualScroll composable. */
|
|
27
35
|
export interface VirtualScrollProps<T = unknown> {
|
|
28
|
-
/** Array of items to be virtualized */
|
|
36
|
+
/** Array of items to be virtualized. */
|
|
29
37
|
items: T[];
|
|
30
|
-
/** Fixed size of each item or a function that returns the size of an item */
|
|
38
|
+
/** Fixed size of each item or a function that returns the size of an item. */
|
|
31
39
|
itemSize?: number | ((item: T, index: number) => number) | undefined;
|
|
32
|
-
/** Direction of the scroll: 'vertical', 'horizontal', or 'both' */
|
|
40
|
+
/** Direction of the scroll: 'vertical', 'horizontal', or 'both'. */
|
|
33
41
|
direction?: ScrollDirection | undefined;
|
|
34
|
-
/** Number of items to render before the visible viewport */
|
|
42
|
+
/** Number of items to render before the visible viewport. */
|
|
35
43
|
bufferBefore?: number | undefined;
|
|
36
|
-
/** Number of items to render after the visible viewport */
|
|
44
|
+
/** Number of items to render after the visible viewport. */
|
|
37
45
|
bufferAfter?: number | undefined;
|
|
38
|
-
/** The scrollable container element or window */
|
|
46
|
+
/** The scrollable container element or window. */
|
|
39
47
|
container?: HTMLElement | Window | null | undefined;
|
|
40
|
-
/** The host element that contains the items */
|
|
48
|
+
/** The host element that contains the items. */
|
|
41
49
|
hostElement?: HTMLElement | null | undefined;
|
|
42
|
-
/** Range of items to render for SSR */
|
|
50
|
+
/** Range of items to render for SSR. */
|
|
43
51
|
ssrRange?: {
|
|
44
52
|
start: number;
|
|
45
53
|
end: number;
|
|
46
54
|
colStart?: number;
|
|
47
55
|
colEnd?: number;
|
|
48
56
|
} | undefined;
|
|
49
|
-
/** Number of columns for bidirectional scroll */
|
|
57
|
+
/** Number of columns for bidirectional scroll. */
|
|
50
58
|
columnCount?: number | undefined;
|
|
51
|
-
/** Fixed width of columns or an array
|
|
59
|
+
/** Fixed width of columns or an array/function for column widths. */
|
|
52
60
|
columnWidth?: number | number[] | ((index: number) => number) | undefined;
|
|
53
|
-
/** Padding at the start of the scroll container
|
|
61
|
+
/** Padding at the start of the scroll container. */
|
|
54
62
|
scrollPaddingStart?: number | { x?: number; y?: number; } | undefined;
|
|
55
|
-
/** Padding at the end of the scroll container */
|
|
63
|
+
/** Padding at the end of the scroll container. */
|
|
56
64
|
scrollPaddingEnd?: number | { x?: number; y?: number; } | undefined;
|
|
57
|
-
/** Gap between items in pixels (vertical) */
|
|
65
|
+
/** Gap between items in pixels (vertical). */
|
|
58
66
|
gap?: number | undefined;
|
|
59
|
-
/** Gap between columns in pixels (horizontal/grid) */
|
|
67
|
+
/** Gap between columns in pixels (horizontal/grid). */
|
|
60
68
|
columnGap?: number | undefined;
|
|
61
|
-
/** Indices of items that should stick to the top/start */
|
|
69
|
+
/** Indices of items that should stick to the top/start. */
|
|
62
70
|
stickyIndices?: number[] | undefined;
|
|
63
|
-
/** Distance from the end of the scrollable area to trigger 'load' event */
|
|
71
|
+
/** Distance from the end of the scrollable area to trigger 'load' event. */
|
|
64
72
|
loadDistance?: number | undefined;
|
|
65
|
-
/** Whether items are currently being loaded */
|
|
73
|
+
/** Whether items are currently being loaded. */
|
|
66
74
|
loading?: boolean | undefined;
|
|
67
|
-
/** Whether to restore scroll position when items are prepended */
|
|
75
|
+
/** Whether to restore scroll position when items are prepended. */
|
|
68
76
|
restoreScrollOnPrepend?: boolean | undefined;
|
|
69
|
-
/** Initial scroll index to jump to on mount */
|
|
77
|
+
/** Initial scroll index to jump to on mount. */
|
|
70
78
|
initialScrollIndex?: number | undefined;
|
|
71
|
-
/** Alignment for the initial scroll index */
|
|
79
|
+
/** Alignment for the initial scroll index. */
|
|
72
80
|
initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions | undefined;
|
|
73
|
-
/** Default size for items before they are measured */
|
|
81
|
+
/** Default size for items before they are measured. */
|
|
74
82
|
defaultItemSize?: number | undefined;
|
|
75
|
-
/** Default width for columns before they are measured */
|
|
83
|
+
/** Default width for columns before they are measured. */
|
|
76
84
|
defaultColumnWidth?: number | undefined;
|
|
77
|
-
/** Whether to enable debug mode
|
|
85
|
+
/** Whether to enable debug mode. */
|
|
78
86
|
debug?: boolean | undefined;
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
/** Represents an item currently rendered in the virtual scroll area. */
|
|
81
90
|
export interface RenderedItem<T = unknown> {
|
|
91
|
+
/** The original data item. */
|
|
82
92
|
item: T;
|
|
93
|
+
/** The index of the item in the original array. */
|
|
83
94
|
index: number;
|
|
95
|
+
/** The calculated offset relative to the host element. */
|
|
84
96
|
offset: { x: number; y: number; };
|
|
97
|
+
/** The current measured or estimated size. */
|
|
85
98
|
size: { width: number; height: number; };
|
|
99
|
+
/** The original X offset before sticky adjustments. */
|
|
86
100
|
originalX: number;
|
|
101
|
+
/** The original Y offset before sticky adjustments. */
|
|
87
102
|
originalY: number;
|
|
103
|
+
/** Whether this item is configured to be sticky. */
|
|
88
104
|
isSticky?: boolean;
|
|
105
|
+
/** Whether this item is currently stuck at the threshold. */
|
|
89
106
|
isStickyActive?: boolean;
|
|
107
|
+
/** The offset applied for the sticky pushing effect. */
|
|
90
108
|
stickyOffset: { x: number; y: number; };
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
/** Comprehensive state of the virtual scroll system. */
|
|
93
112
|
export interface ScrollDetails<T = unknown> {
|
|
113
|
+
/** List of items currently rendered. */
|
|
94
114
|
items: RenderedItem<T>[];
|
|
115
|
+
/** Index of the first item partially or fully visible in the viewport. */
|
|
95
116
|
currentIndex: number;
|
|
117
|
+
/** Index of the first column partially or fully visible. */
|
|
96
118
|
currentColIndex: number;
|
|
119
|
+
/** Current scroll position relative to content start. */
|
|
97
120
|
scrollOffset: { x: number; y: number; };
|
|
121
|
+
/** Dimensions of the visible viewport. */
|
|
98
122
|
viewportSize: { width: number; height: number; };
|
|
123
|
+
/** Total calculated size of all items and gaps. */
|
|
99
124
|
totalSize: { width: number; height: number; };
|
|
125
|
+
/** Whether the container is currently being scrolled. */
|
|
100
126
|
isScrolling: boolean;
|
|
127
|
+
/** Whether the current scroll was initiated by a method call. */
|
|
101
128
|
isProgrammaticScroll: boolean;
|
|
102
|
-
/** Range of items currently being rendered */
|
|
129
|
+
/** Range of items currently being rendered. */
|
|
103
130
|
range: { start: number; end: number; };
|
|
104
|
-
/** Range of columns currently being rendered (for grid mode) */
|
|
131
|
+
/** Range of columns currently being rendered (for grid mode). */
|
|
105
132
|
columnRange: { start: number; end: number; padStart: number; padEnd: number; };
|
|
106
133
|
}
|
|
107
134
|
|
|
@@ -144,9 +171,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
144
171
|
options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
145
172
|
} | null>(null);
|
|
146
173
|
|
|
147
|
-
const maxWidth = ref(0);
|
|
148
|
-
const maxHeight = ref(0);
|
|
149
|
-
|
|
150
174
|
// Track if sizes are initialized
|
|
151
175
|
const sizesInitialized = ref(false);
|
|
152
176
|
let lastItems: T[] = [];
|
|
@@ -164,7 +188,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
164
188
|
(typeof props.value.itemSize === 'number' && props.value.itemSize > 0) ? props.value.itemSize : null,
|
|
165
189
|
);
|
|
166
190
|
|
|
167
|
-
const defaultSize = computed(() =>
|
|
191
|
+
const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
|
|
168
192
|
|
|
169
193
|
const sortedStickyIndices = computed(() =>
|
|
170
194
|
[ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
|
|
@@ -181,27 +205,42 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
181
205
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
182
206
|
const { start = 0, end = 0, colStart = 0, colEnd = 0 } = props.value.ssrRange;
|
|
183
207
|
const colCount = props.value.columnCount || 0;
|
|
184
|
-
if (props.value.direction === 'both'
|
|
185
|
-
|
|
208
|
+
if (props.value.direction === 'both') {
|
|
209
|
+
if (colCount <= 0) {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
const effectiveColEnd = colEnd || colCount;
|
|
213
|
+
const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
|
|
214
|
+
return Math.max(0, total - (effectiveColEnd > colStart ? (props.value.columnGap || 0) : 0));
|
|
186
215
|
}
|
|
216
|
+
/* v8 ignore else -- @preserve */
|
|
187
217
|
if (props.value.direction === 'horizontal') {
|
|
188
218
|
if (fixedItemSize.value !== null) {
|
|
189
|
-
|
|
219
|
+
const len = end - start;
|
|
220
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.columnGap || 0)) - (len > 0 ? (props.value.columnGap || 0) : 0));
|
|
190
221
|
}
|
|
191
|
-
|
|
222
|
+
const total = itemSizesX.query(end) - itemSizesX.query(start);
|
|
223
|
+
return Math.max(0, total - (end > start ? (props.value.columnGap || 0) : 0));
|
|
192
224
|
}
|
|
193
225
|
}
|
|
194
226
|
|
|
195
|
-
if (props.value.direction === 'both'
|
|
196
|
-
|
|
227
|
+
if (props.value.direction === 'both') {
|
|
228
|
+
const colCount = props.value.columnCount || 0;
|
|
229
|
+
if (colCount <= 0) {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
const total = columnSizes.query(colCount);
|
|
233
|
+
return Math.max(0, total - (props.value.columnGap || 0));
|
|
197
234
|
}
|
|
198
235
|
if (props.value.direction === 'vertical') {
|
|
199
236
|
return 0;
|
|
200
237
|
}
|
|
201
238
|
if (fixedItemSize.value !== null) {
|
|
202
|
-
|
|
239
|
+
const len = props.value.items.length;
|
|
240
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.columnGap || 0)) - (len > 0 ? (props.value.columnGap || 0) : 0));
|
|
203
241
|
}
|
|
204
|
-
|
|
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));
|
|
205
244
|
});
|
|
206
245
|
|
|
207
246
|
/**
|
|
@@ -213,11 +252,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
213
252
|
|
|
214
253
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
215
254
|
const { start, end } = props.value.ssrRange;
|
|
255
|
+
/* v8 ignore else -- @preserve */
|
|
216
256
|
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
217
257
|
if (fixedItemSize.value !== null) {
|
|
218
|
-
|
|
258
|
+
const len = end - start;
|
|
259
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.gap || 0)) - (len > 0 ? (props.value.gap || 0) : 0));
|
|
219
260
|
}
|
|
220
|
-
|
|
261
|
+
const total = itemSizesY.query(end) - itemSizesY.query(start);
|
|
262
|
+
return Math.max(0, total - (end > start ? (props.value.gap || 0) : 0));
|
|
221
263
|
}
|
|
222
264
|
}
|
|
223
265
|
|
|
@@ -225,9 +267,11 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
225
267
|
return 0;
|
|
226
268
|
}
|
|
227
269
|
if (fixedItemSize.value !== null) {
|
|
228
|
-
|
|
270
|
+
const len = props.value.items.length;
|
|
271
|
+
return Math.max(0, len * (fixedItemSize.value + (props.value.gap || 0)) - (len > 0 ? (props.value.gap || 0) : 0));
|
|
229
272
|
}
|
|
230
|
-
|
|
273
|
+
const total = itemSizesY.query(props.value.items.length);
|
|
274
|
+
return Math.max(0, total - (props.value.items.length > 0 ? (props.value.gap || 0) : 0));
|
|
231
275
|
});
|
|
232
276
|
|
|
233
277
|
const relativeScrollX = computed(() => {
|
|
@@ -251,8 +295,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
251
295
|
return cw;
|
|
252
296
|
}
|
|
253
297
|
if (Array.isArray(cw) && cw.length > 0) {
|
|
254
|
-
|
|
298
|
+
const val = cw[ index % cw.length ];
|
|
299
|
+
return (val != null && val > 0) ? val : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
|
|
255
300
|
}
|
|
301
|
+
/* v8 ignore else -- @preserve */
|
|
256
302
|
if (typeof cw === 'function') {
|
|
257
303
|
return cw(index);
|
|
258
304
|
}
|
|
@@ -354,7 +400,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
354
400
|
itemWidth = fixedSize !== null ? fixedSize : itemSizesX.get(colIndex) - columnGap;
|
|
355
401
|
} else {
|
|
356
402
|
targetX = columnSizes.query(colIndex);
|
|
357
|
-
itemWidth = columnSizes.get(colIndex);
|
|
403
|
+
itemWidth = columnSizes.get(colIndex) - columnGap;
|
|
358
404
|
}
|
|
359
405
|
|
|
360
406
|
// Apply X Alignment
|
|
@@ -367,6 +413,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
367
413
|
} else {
|
|
368
414
|
const isVisibleX = targetX >= relativeScrollX.value && (targetX + itemWidth) <= (relativeScrollX.value + usableWidth);
|
|
369
415
|
if (!isVisibleX) {
|
|
416
|
+
/* v8 ignore if -- @preserve */
|
|
370
417
|
if (targetX < relativeScrollX.value) {
|
|
371
418
|
// keep targetX at start
|
|
372
419
|
} else {
|
|
@@ -396,6 +443,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
396
443
|
let viewW = 0;
|
|
397
444
|
let viewH = 0;
|
|
398
445
|
|
|
446
|
+
/* v8 ignore else -- @preserve */
|
|
399
447
|
if (typeof window !== 'undefined') {
|
|
400
448
|
if (container === window) {
|
|
401
449
|
curX = window.scrollX;
|
|
@@ -416,6 +464,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
416
464
|
if (!reachedX && colIndex !== null && colIndex !== undefined) {
|
|
417
465
|
const atLeft = curX <= tolerance && finalX <= tolerance;
|
|
418
466
|
const atRight = curX >= maxW - viewW - tolerance && finalX >= maxW - viewW - tolerance;
|
|
467
|
+
/* v8 ignore else -- @preserve */
|
|
419
468
|
if (atLeft || atRight) {
|
|
420
469
|
reachedX = true;
|
|
421
470
|
}
|
|
@@ -495,12 +544,24 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
495
544
|
|
|
496
545
|
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
497
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
|
+
const clampedX = (x !== null && x !== undefined)
|
|
554
|
+
? (isHorizontal ? Math.max(0, Math.min(x, Math.max(0, totalWidth.value - usableWidth))) : Math.max(0, x))
|
|
555
|
+
: null;
|
|
556
|
+
const clampedY = (y !== null && y !== undefined)
|
|
557
|
+
? (isVertical ? Math.max(0, Math.min(y, Math.max(0, totalHeight.value - usableHeight))) : Math.max(0, y))
|
|
558
|
+
: null;
|
|
498
559
|
|
|
499
560
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
500
561
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
501
562
|
|
|
502
|
-
const targetX = (
|
|
503
|
-
const targetY = (
|
|
563
|
+
const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
|
|
564
|
+
const targetY = (clampedY !== null) ? clampedY + hostOffset.y - (isVertical ? paddingStartY : 0) : currentY;
|
|
504
565
|
|
|
505
566
|
if (typeof window !== 'undefined' && container === window) {
|
|
506
567
|
window.scrollTo({
|
|
@@ -571,6 +632,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
571
632
|
let prependCount = 0;
|
|
572
633
|
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
573
634
|
const oldFirstItem = lastItems[ 0 ];
|
|
635
|
+
/* v8 ignore else -- @preserve */
|
|
574
636
|
if (oldFirstItem !== undefined) {
|
|
575
637
|
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
576
638
|
if (newItems[ i ] === oldFirstItem) {
|
|
@@ -614,6 +676,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
614
676
|
}
|
|
615
677
|
}
|
|
616
678
|
|
|
679
|
+
/* v8 ignore else -- @preserve */
|
|
617
680
|
if (addedX > 0 || addedY > 0) {
|
|
618
681
|
nextTick(() => {
|
|
619
682
|
scrollToOffset(
|
|
@@ -632,14 +695,17 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
632
695
|
for (let i = 0; i < colCount; i++) {
|
|
633
696
|
const width = getColumnWidth(i);
|
|
634
697
|
const currentW = columnSizes.get(i);
|
|
698
|
+
const isMeasured = measuredColumns[ i ] === 1;
|
|
635
699
|
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
if (!isDynamicColumnWidth.value || currentW === 0) {
|
|
700
|
+
// If fixed/function, or if dynamic but not measured yet
|
|
701
|
+
if (!isDynamicColumnWidth.value || !isMeasured || currentW === 0) {
|
|
639
702
|
const targetW = width + columnGap;
|
|
640
703
|
if (Math.abs(currentW - targetW) > 0.5) {
|
|
641
704
|
columnSizes.set(i, targetW);
|
|
705
|
+
measuredColumns[ i ] = isDynamicColumnWidth.value ? 0 : 1;
|
|
642
706
|
colNeedsRebuild = true;
|
|
707
|
+
} else if (!isDynamicColumnWidth.value) {
|
|
708
|
+
measuredColumns[ i ] = 1;
|
|
643
709
|
}
|
|
644
710
|
}
|
|
645
711
|
}
|
|
@@ -657,11 +723,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
657
723
|
const currentX = itemSizesX.get(i);
|
|
658
724
|
const currentY = itemSizesY.get(i);
|
|
659
725
|
|
|
660
|
-
// If it's dynamic and already has a measurement, keep it.
|
|
661
|
-
if (isDynamicItemSize.value && (currentX > 0 || currentY > 0)) {
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
726
|
const size = typeof props.value.itemSize === 'function'
|
|
666
727
|
? props.value.itemSize(item as T, i)
|
|
667
728
|
: defaultSize.value;
|
|
@@ -673,24 +734,42 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
673
734
|
const targetX = isHorizontal ? size + columnGap : 0;
|
|
674
735
|
const targetY = (isVertical || isBoth) ? size + gap : 0;
|
|
675
736
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (
|
|
681
|
-
|
|
737
|
+
const isMeasuredX = measuredItemsX[ i ] === 1;
|
|
738
|
+
const isMeasuredY = measuredItemsY[ i ] === 1;
|
|
739
|
+
|
|
740
|
+
// Logic for X
|
|
741
|
+
if (isHorizontal) {
|
|
742
|
+
// If fixed/function, or if dynamic but not measured yet
|
|
743
|
+
if (!isDynamicItemSize.value || !isMeasuredX || currentX === 0) {
|
|
744
|
+
if (Math.abs(currentX - targetX) > 0.5) {
|
|
745
|
+
itemSizesX.set(i, targetX);
|
|
746
|
+
measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
747
|
+
itemsNeedRebuild = true;
|
|
748
|
+
} else if (!isDynamicItemSize.value) {
|
|
749
|
+
measuredItemsX[ i ] = 1;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} else if (currentX !== 0) {
|
|
753
|
+
itemSizesX.set(i, 0);
|
|
754
|
+
measuredItemsX[ i ] = 0;
|
|
682
755
|
itemsNeedRebuild = true;
|
|
683
756
|
}
|
|
684
757
|
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
758
|
+
// Logic for Y
|
|
759
|
+
if (isVertical || isBoth) {
|
|
760
|
+
if (!isDynamicItemSize.value || !isMeasuredY || currentY === 0) {
|
|
761
|
+
if (Math.abs(currentY - targetY) > 0.5) {
|
|
762
|
+
itemSizesY.set(i, targetY);
|
|
763
|
+
measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
|
|
764
|
+
itemsNeedRebuild = true;
|
|
765
|
+
} else if (!isDynamicItemSize.value) {
|
|
766
|
+
measuredItemsY[ i ] = 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} else if (currentY !== 0) {
|
|
770
|
+
itemSizesY.set(i, 0);
|
|
771
|
+
measuredItemsY[ i ] = 0;
|
|
772
|
+
itemsNeedRebuild = true;
|
|
694
773
|
}
|
|
695
774
|
}
|
|
696
775
|
|
|
@@ -735,12 +814,15 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
735
814
|
};
|
|
736
815
|
|
|
737
816
|
watch([
|
|
738
|
-
() => props.value.items
|
|
817
|
+
() => props.value.items,
|
|
818
|
+
() => props.value.direction,
|
|
739
819
|
() => props.value.columnCount,
|
|
740
820
|
() => props.value.columnWidth,
|
|
741
821
|
() => props.value.itemSize,
|
|
742
822
|
() => props.value.gap,
|
|
743
823
|
() => props.value.columnGap,
|
|
824
|
+
() => props.value.defaultItemSize,
|
|
825
|
+
() => props.value.defaultColumnWidth,
|
|
744
826
|
], initializeSizes, { immediate: true });
|
|
745
827
|
|
|
746
828
|
watch(() => [ props.value.container, props.value.hostElement ], () => {
|
|
@@ -955,6 +1037,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
955
1037
|
if (nextStickyIdx !== undefined) {
|
|
956
1038
|
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : itemSizesY.query(nextStickyIdx);
|
|
957
1039
|
const distance = nextStickyY - relativeScrollY.value;
|
|
1040
|
+
/* v8 ignore else -- @preserve */
|
|
958
1041
|
if (distance < height) {
|
|
959
1042
|
stickyOffset.y = -(height - distance);
|
|
960
1043
|
}
|
|
@@ -980,6 +1063,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
980
1063
|
if (nextStickyIdx !== undefined) {
|
|
981
1064
|
const nextStickyX = fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : itemSizesX.query(nextStickyIdx);
|
|
982
1065
|
const distance = nextStickyX - relativeScrollX.value;
|
|
1066
|
+
/* v8 ignore else -- @preserve */
|
|
983
1067
|
if (distance < width) {
|
|
984
1068
|
stickyOffset.x = -(width - distance);
|
|
985
1069
|
}
|
|
@@ -1039,10 +1123,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1039
1123
|
const safeStart = Math.max(0, start - colBuffer);
|
|
1040
1124
|
const safeEnd = Math.min(totalCols, end + colBuffer);
|
|
1041
1125
|
|
|
1126
|
+
const padStart = columnSizes.query(safeStart);
|
|
1127
|
+
|
|
1042
1128
|
return {
|
|
1043
1129
|
start: safeStart,
|
|
1044
1130
|
end: safeEnd,
|
|
1045
|
-
padStart
|
|
1131
|
+
padStart,
|
|
1046
1132
|
padEnd: columnSizes.query(totalCols) - columnSizes.query(safeEnd),
|
|
1047
1133
|
};
|
|
1048
1134
|
});
|
|
@@ -1130,18 +1216,16 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1130
1216
|
const columnGap = props.value.columnGap || 0;
|
|
1131
1217
|
|
|
1132
1218
|
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
maxWidth.value = inlineSize;
|
|
1136
|
-
}
|
|
1137
|
-
if (blockSize > maxHeight.value) {
|
|
1138
|
-
maxHeight.value = blockSize;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1219
|
+
const isMeasurable = isDynamicItemSize.value || typeof props.value.itemSize === 'function';
|
|
1220
|
+
if (isMeasurable && index >= 0) {
|
|
1141
1221
|
if (props.value.direction === 'horizontal') {
|
|
1142
1222
|
const oldWidth = itemSizesX.get(index);
|
|
1143
1223
|
const targetWidth = inlineSize + columnGap;
|
|
1144
|
-
|
|
1224
|
+
// Apply if:
|
|
1225
|
+
// 1. It's the first measurement (measuredItemsX[index] is 0)
|
|
1226
|
+
// 2. It's a significant change (> 0.5px)
|
|
1227
|
+
/* v8 ignore else -- @preserve */
|
|
1228
|
+
if (!measuredItemsX[ index ] || Math.abs(targetWidth - oldWidth) > 0.5) {
|
|
1145
1229
|
itemSizesX.update(index, targetWidth - oldWidth);
|
|
1146
1230
|
measuredItemsX[ index ] = 1;
|
|
1147
1231
|
needUpdate = true;
|
|
@@ -1150,14 +1234,16 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1150
1234
|
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
1151
1235
|
const oldHeight = itemSizesY.get(index);
|
|
1152
1236
|
const targetHeight = blockSize + gap;
|
|
1153
|
-
|
|
1237
|
+
|
|
1154
1238
|
if (props.value.direction === 'both') {
|
|
1155
|
-
|
|
1239
|
+
// For grid, we should be careful with decreases because a row height is the max of all its cells.
|
|
1240
|
+
/* v8 ignore else -- @preserve */
|
|
1241
|
+
if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
|
|
1156
1242
|
itemSizesY.update(index, targetHeight - oldHeight);
|
|
1157
1243
|
measuredItemsY[ index ] = 1;
|
|
1158
1244
|
needUpdate = true;
|
|
1159
1245
|
}
|
|
1160
|
-
} else if (Math.abs(
|
|
1246
|
+
} else if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
|
|
1161
1247
|
itemSizesY.update(index, targetHeight - oldHeight);
|
|
1162
1248
|
measuredItemsY[ index ] = 1;
|
|
1163
1249
|
needUpdate = true;
|
|
@@ -1166,11 +1252,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1166
1252
|
}
|
|
1167
1253
|
|
|
1168
1254
|
// Dynamic column width measurement
|
|
1255
|
+
const isColMeasurable = isDynamicColumnWidth.value || typeof props.value.columnWidth === 'function';
|
|
1169
1256
|
if (
|
|
1170
1257
|
props.value.direction === 'both'
|
|
1171
1258
|
&& element
|
|
1172
1259
|
&& props.value.columnCount
|
|
1173
|
-
&&
|
|
1260
|
+
&& isColMeasurable
|
|
1174
1261
|
) {
|
|
1175
1262
|
const cells = element.dataset.colIndex !== undefined
|
|
1176
1263
|
? [ element ]
|
|
@@ -1179,11 +1266,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1179
1266
|
for (const child of cells) {
|
|
1180
1267
|
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1181
1268
|
|
|
1269
|
+
/* v8 ignore else -- @preserve */
|
|
1182
1270
|
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0)) {
|
|
1183
1271
|
const w = child.offsetWidth;
|
|
1184
1272
|
const oldW = columnSizes.get(colIndex);
|
|
1185
1273
|
const targetW = w + columnGap;
|
|
1186
|
-
if (
|
|
1274
|
+
if (Math.abs(oldW - targetW) > 0.5) {
|
|
1187
1275
|
columnSizes.update(colIndex, targetW - oldW);
|
|
1188
1276
|
measuredColumns[ colIndex ] = 1;
|
|
1189
1277
|
needUpdate = true;
|
|
@@ -1262,6 +1350,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1262
1350
|
|
|
1263
1351
|
resizeObserver = new ResizeObserver((entries) => {
|
|
1264
1352
|
for (const entry of entries) {
|
|
1353
|
+
/* v8 ignore else -- @preserve */
|
|
1265
1354
|
if (entry.target === container) {
|
|
1266
1355
|
viewportWidth.value = (container as HTMLElement).clientWidth;
|
|
1267
1356
|
viewportHeight.value = (container as HTMLElement).clientHeight;
|
|
@@ -1298,6 +1387,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1298
1387
|
: props.value.ssrRange?.start;
|
|
1299
1388
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1300
1389
|
|
|
1390
|
+
/* v8 ignore else -- @preserve */
|
|
1301
1391
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1302
1392
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1303
1393
|
}
|
|
@@ -1328,8 +1418,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1328
1418
|
measuredColumns.fill(0);
|
|
1329
1419
|
measuredItemsX.fill(0);
|
|
1330
1420
|
measuredItemsY.fill(0);
|
|
1331
|
-
maxWidth.value = 0;
|
|
1332
|
-
maxHeight.value = 0;
|
|
1333
1421
|
initializeSizes();
|
|
1334
1422
|
};
|
|
1335
1423
|
|