@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
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ScrollAlignmentOptions,
|
|
6
6
|
ScrollDetails,
|
|
7
7
|
VirtualScrollProps,
|
|
8
|
-
} from '../
|
|
8
|
+
} from '../types';
|
|
9
9
|
import type { VNodeChild } from 'vue';
|
|
10
10
|
|
|
11
11
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
@@ -14,67 +14,198 @@ import {
|
|
|
14
14
|
DEFAULT_ITEM_SIZE,
|
|
15
15
|
useVirtualScroll,
|
|
16
16
|
} from '../composables/useVirtualScroll';
|
|
17
|
-
import {
|
|
17
|
+
import { isWindowLike } from '../utils/scroll';
|
|
18
|
+
import { calculateItemStyle } from '../utils/virtual-scroll-logic';
|
|
18
19
|
|
|
19
20
|
export interface Props<T = unknown> {
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Array of items to be virtualized.
|
|
23
|
+
* Required.
|
|
24
|
+
*/
|
|
21
25
|
items: T[];
|
|
22
|
-
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fixed size of each item (in pixels) or a function that returns the size of an item.
|
|
29
|
+
* Pass 0, null or undefined for dynamic size detection via ResizeObserver.
|
|
30
|
+
* @default 40
|
|
31
|
+
*/
|
|
23
32
|
itemSize?: number | ((item: T, index: number) => number) | null;
|
|
24
|
-
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Direction of the scroll.
|
|
36
|
+
* - 'vertical': Standard vertical list.
|
|
37
|
+
* - 'horizontal': Standard horizontal list.
|
|
38
|
+
* - 'both': Grid mode virtualizing both rows and columns.
|
|
39
|
+
* @default 'vertical'
|
|
40
|
+
*/
|
|
25
41
|
direction?: 'vertical' | 'horizontal' | 'both';
|
|
26
|
-
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Number of items to render before the visible viewport.
|
|
45
|
+
* Useful for smoother scrolling and keyboard navigation.
|
|
46
|
+
* @default 5
|
|
47
|
+
*/
|
|
27
48
|
bufferBefore?: number;
|
|
28
|
-
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Number of items to render after the visible viewport.
|
|
52
|
+
* @default 5
|
|
53
|
+
*/
|
|
29
54
|
bufferAfter?: number;
|
|
30
|
-
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The scrollable container element or window.
|
|
58
|
+
* If not provided, the host element (root of VirtualScroll) is used.
|
|
59
|
+
* @default hostRef
|
|
60
|
+
*/
|
|
31
61
|
container?: HTMLElement | Window | null;
|
|
32
|
-
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Range of items to render during Server-Side Rendering.
|
|
65
|
+
* When provided, these items will be rendered in-flow before hydration.
|
|
66
|
+
* @see SSRRange
|
|
67
|
+
*/
|
|
33
68
|
ssrRange?: {
|
|
69
|
+
/** First row index to render. */
|
|
34
70
|
start: number;
|
|
71
|
+
/** Last row index to render (exclusive). */
|
|
35
72
|
end: number;
|
|
73
|
+
/** First column index to render (for grid mode). */
|
|
36
74
|
colStart?: number;
|
|
75
|
+
/** Last column index to render (exclusive, for grid mode). */
|
|
37
76
|
colEnd?: number;
|
|
38
77
|
};
|
|
39
|
-
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Number of columns for bidirectional (grid) scroll.
|
|
81
|
+
* Only applicable when direction="both".
|
|
82
|
+
* @default 0
|
|
83
|
+
*/
|
|
40
84
|
columnCount?: number;
|
|
41
|
-
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fixed width of columns (in pixels), an array of widths, or a function for column widths.
|
|
88
|
+
* Pass 0, null or undefined for dynamic width detection via ResizeObserver.
|
|
89
|
+
* Only applicable when direction="both".
|
|
90
|
+
* @default 100
|
|
91
|
+
*/
|
|
42
92
|
columnWidth?: number | number[] | ((index: number) => number) | null;
|
|
43
|
-
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The HTML tag to use for the root container.
|
|
96
|
+
* @default 'div'
|
|
97
|
+
*/
|
|
44
98
|
containerTag?: string;
|
|
45
|
-
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The HTML tag to use for the items wrapper.
|
|
102
|
+
* Useful for <table> integration (e.g. 'tbody').
|
|
103
|
+
* @default 'div'
|
|
104
|
+
*/
|
|
46
105
|
wrapperTag?: string;
|
|
47
|
-
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The HTML tag to use for each item.
|
|
109
|
+
* Useful for <table> integration (e.g. 'tr').
|
|
110
|
+
* @default 'div'
|
|
111
|
+
*/
|
|
48
112
|
itemTag?: string;
|
|
49
|
-
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Additional padding at the start of the scroll container (top or left).
|
|
116
|
+
* Can be a number (applied to current direction) or an object with x/y.
|
|
117
|
+
* @default 0
|
|
118
|
+
*/
|
|
50
119
|
scrollPaddingStart?: number | { x?: number; y?: number; };
|
|
51
|
-
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Additional padding at the end of the scroll container (bottom or right).
|
|
123
|
+
* @default 0
|
|
124
|
+
*/
|
|
52
125
|
scrollPaddingEnd?: number | { x?: number; y?: number; };
|
|
53
|
-
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether the content in the 'header' slot is sticky.
|
|
129
|
+
* If true, the header size is measured and accounted for in scroll padding.
|
|
130
|
+
* @default false
|
|
131
|
+
*/
|
|
54
132
|
stickyHeader?: boolean;
|
|
55
|
-
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether the content in the 'footer' slot is sticky.
|
|
136
|
+
* @default false
|
|
137
|
+
*/
|
|
56
138
|
stickyFooter?: boolean;
|
|
57
|
-
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gap between items in pixels (vertical gap in vertical/grid mode, horizontal gap in horizontal mode).
|
|
142
|
+
* @default 0
|
|
143
|
+
*/
|
|
58
144
|
gap?: number;
|
|
59
|
-
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Gap between columns in pixels. Only applicable when direction="both" or "horizontal".
|
|
148
|
+
* @default 0
|
|
149
|
+
*/
|
|
60
150
|
columnGap?: number;
|
|
61
|
-
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Indices of items that should stick to the top/start of the viewport.
|
|
154
|
+
* Supports iOS-style pushing effect where the next sticky item pushes the previous one.
|
|
155
|
+
* @default []
|
|
156
|
+
*/
|
|
62
157
|
stickyIndices?: number[];
|
|
63
|
-
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Distance from the end of the scrollable area (in pixels) to trigger the 'load' event.
|
|
161
|
+
* @default 200
|
|
162
|
+
*/
|
|
64
163
|
loadDistance?: number;
|
|
65
|
-
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether items are currently being loaded.
|
|
167
|
+
* Prevents multiple 'load' events from triggering and shows the 'loading' slot.
|
|
168
|
+
* @default false
|
|
169
|
+
*/
|
|
66
170
|
loading?: boolean;
|
|
67
|
-
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether to automatically restore and maintain scroll position when items are prepended to the list.
|
|
174
|
+
* Perfect for chat applications.
|
|
175
|
+
* @default false
|
|
176
|
+
*/
|
|
68
177
|
restoreScrollOnPrepend?: boolean;
|
|
69
|
-
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Initial scroll index to jump to immediately after mount.
|
|
181
|
+
*/
|
|
70
182
|
initialScrollIndex?: number;
|
|
71
|
-
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Alignment for the initial scroll index.
|
|
186
|
+
* @default 'start'
|
|
187
|
+
* @see ScrollAlignment
|
|
188
|
+
*/
|
|
72
189
|
initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions;
|
|
73
|
-
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Default size for items before they are measured by ResizeObserver.
|
|
193
|
+
* Only used when itemSize is dynamic.
|
|
194
|
+
* @default 40
|
|
195
|
+
*/
|
|
74
196
|
defaultItemSize?: number;
|
|
75
|
-
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Default width for columns before they are measured by ResizeObserver.
|
|
200
|
+
* Only used when columnWidth is dynamic.
|
|
201
|
+
* @default 100
|
|
202
|
+
*/
|
|
76
203
|
defaultColumnWidth?: number;
|
|
77
|
-
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether to show debug information (visible offsets and indices) over items.
|
|
207
|
+
* @default false
|
|
208
|
+
*/
|
|
78
209
|
debug?: boolean;
|
|
79
210
|
}
|
|
80
211
|
|
|
@@ -106,26 +237,55 @@ const emit = defineEmits<{
|
|
|
106
237
|
}>();
|
|
107
238
|
|
|
108
239
|
const slots = defineSlots<{
|
|
109
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* Content rendered at the top of the scrollable area.
|
|
242
|
+
* Can be made sticky using the `stickyHeader` prop.
|
|
243
|
+
*/
|
|
110
244
|
header?: (props: Record<string, never>) => VNodeChild;
|
|
111
|
-
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Scoped slot for rendering each individual item.
|
|
248
|
+
*/
|
|
112
249
|
item?: (props: {
|
|
113
|
-
/** The data item
|
|
250
|
+
/** The original data item from the `items` array. */
|
|
114
251
|
item: T;
|
|
115
|
-
/** The index of the item in the items array. */
|
|
252
|
+
/** The original index of the item in the `items` array. */
|
|
116
253
|
index: number;
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Information about the current visible range of columns (for grid mode).
|
|
256
|
+
* @see ColumnRange
|
|
257
|
+
*/
|
|
258
|
+
columnRange: {
|
|
259
|
+
/** Index of the first rendered column. */
|
|
260
|
+
start: number;
|
|
261
|
+
/** Index of the last rendered column (exclusive). */
|
|
262
|
+
end: number;
|
|
263
|
+
/** Pixel offset from the start of the row to the first rendered cell. */
|
|
264
|
+
padStart: number;
|
|
265
|
+
/** Pixel offset from the last rendered cell to the end of the row. */
|
|
266
|
+
padEnd: number;
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Helper function to get the width of a specific column.
|
|
270
|
+
* Useful for setting consistent widths in grid mode.
|
|
271
|
+
*/
|
|
120
272
|
getColumnWidth: (index: number) => number;
|
|
121
|
-
/** Whether this item is configured to be sticky
|
|
273
|
+
/** Whether this item is configured to be sticky via `stickyIndices`. */
|
|
122
274
|
isSticky?: boolean | undefined;
|
|
123
|
-
/** Whether this item is currently in a sticky state. */
|
|
275
|
+
/** Whether this item is currently in a sticky state (stuck at the top/start). */
|
|
124
276
|
isStickyActive?: boolean | undefined;
|
|
125
277
|
}) => VNodeChild;
|
|
126
|
-
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Content shown at the end of the list when the `loading` prop is true.
|
|
281
|
+
* Also prevents additional 'load' events from triggering while visible.
|
|
282
|
+
*/
|
|
127
283
|
loading?: (props: Record<string, never>) => VNodeChild;
|
|
128
|
-
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Content rendered at the bottom of the scrollable area.
|
|
287
|
+
* Can be made sticky using the `stickyFooter` prop.
|
|
288
|
+
*/
|
|
129
289
|
footer?: (props: Record<string, never>) => VNodeChild;
|
|
130
290
|
}>();
|
|
131
291
|
|
|
@@ -151,21 +311,23 @@ const virtualScrollProps = computed(() => {
|
|
|
151
311
|
const pStart = props.scrollPaddingStart;
|
|
152
312
|
const pEnd = props.scrollPaddingEnd;
|
|
153
313
|
|
|
154
|
-
/*
|
|
314
|
+
/* Trigger re-evaluation on items array mutations */
|
|
315
|
+
// eslint-disable-next-line ts/no-unused-expressions
|
|
316
|
+
props.items.length;
|
|
317
|
+
|
|
155
318
|
const startX = typeof pStart === 'object'
|
|
156
319
|
? (pStart.x || 0)
|
|
157
|
-
: (props.direction === 'horizontal' ? (pStart || 0) : 0);
|
|
320
|
+
: ((props.direction === 'horizontal' || props.direction === 'both') ? (pStart || 0) : 0);
|
|
158
321
|
const startY = typeof pStart === 'object'
|
|
159
322
|
? (pStart.y || 0)
|
|
160
|
-
: (props.direction
|
|
323
|
+
: ((props.direction === 'vertical' || props.direction === 'both') ? (pStart || 0) : 0);
|
|
161
324
|
|
|
162
325
|
const endX = typeof pEnd === 'object'
|
|
163
326
|
? (pEnd.x || 0)
|
|
164
|
-
: (props.direction === 'horizontal' ? (pEnd || 0) : 0);
|
|
327
|
+
: ((props.direction === 'horizontal' || props.direction === 'both') ? (pEnd || 0) : 0);
|
|
165
328
|
const endY = typeof pEnd === 'object'
|
|
166
329
|
? (pEnd.y || 0)
|
|
167
|
-
: (props.direction
|
|
168
|
-
/* v8 ignore stop -- @preserve */
|
|
330
|
+
: ((props.direction === 'vertical' || props.direction === 'both') ? (pEnd || 0) : 0);
|
|
169
331
|
|
|
170
332
|
return {
|
|
171
333
|
items: props.items,
|
|
@@ -228,7 +390,6 @@ function refresh() {
|
|
|
228
390
|
const updates: { index: number; inlineSize: number; blockSize: number; element?: HTMLElement; }[] = [];
|
|
229
391
|
|
|
230
392
|
for (const [ index, el ] of itemRefs.entries()) {
|
|
231
|
-
/* v8 ignore else -- @preserve */
|
|
232
393
|
if (el) {
|
|
233
394
|
updates.push({
|
|
234
395
|
index,
|
|
@@ -288,7 +449,6 @@ watch(scrollDetails, (details, oldDetails) => {
|
|
|
288
449
|
});
|
|
289
450
|
|
|
290
451
|
watch(isHydrated, (hydrated) => {
|
|
291
|
-
/* v8 ignore else -- @preserve */
|
|
292
452
|
if (hydrated) {
|
|
293
453
|
emit('visibleRangeChange', {
|
|
294
454
|
start: scrollDetails.value.range.start,
|
|
@@ -299,12 +459,10 @@ watch(isHydrated, (hydrated) => {
|
|
|
299
459
|
}
|
|
300
460
|
}, { once: true });
|
|
301
461
|
|
|
302
|
-
/* v8 ignore next 2 -- @preserve */
|
|
303
462
|
const hostResizeObserver = typeof window === 'undefined'
|
|
304
463
|
? null
|
|
305
464
|
: new ResizeObserver(updateHostOffset);
|
|
306
465
|
|
|
307
|
-
/* v8 ignore next 2 -- @preserve */
|
|
308
466
|
const itemResizeObserver = typeof window === 'undefined'
|
|
309
467
|
? null
|
|
310
468
|
: new ResizeObserver((entries) => {
|
|
@@ -315,34 +473,32 @@ const itemResizeObserver = typeof window === 'undefined'
|
|
|
315
473
|
const index = Number(target.dataset.index);
|
|
316
474
|
const colIndex = target.dataset.colIndex;
|
|
317
475
|
|
|
476
|
+
let inlineSize = entry.contentRect.width;
|
|
477
|
+
let blockSize = entry.contentRect.height;
|
|
478
|
+
|
|
479
|
+
if (entry.borderBoxSize && entry.borderBoxSize.length > 0) {
|
|
480
|
+
inlineSize = entry.borderBoxSize[ 0 ]!.inlineSize;
|
|
481
|
+
blockSize = entry.borderBoxSize[ 0 ]!.blockSize;
|
|
482
|
+
} else {
|
|
483
|
+
// Fallback for older browsers or if borderBoxSize is missing
|
|
484
|
+
inlineSize = target.offsetWidth;
|
|
485
|
+
blockSize = target.offsetHeight;
|
|
486
|
+
}
|
|
487
|
+
|
|
318
488
|
if (colIndex !== undefined) {
|
|
319
489
|
// It's a cell measurement. row index is not strictly needed for column width.
|
|
320
490
|
// We use -1 as a placeholder for row index if it's a cell measurement.
|
|
321
|
-
updates.push({ index: -1, inlineSize
|
|
491
|
+
updates.push({ index: -1, inlineSize, blockSize, element: target });
|
|
322
492
|
} else if (!Number.isNaN(index)) {
|
|
323
|
-
let inlineSize = entry.contentRect.width;
|
|
324
|
-
let blockSize = entry.contentRect.height;
|
|
325
|
-
|
|
326
|
-
if (entry.borderBoxSize && entry.borderBoxSize.length > 0) {
|
|
327
|
-
inlineSize = entry.borderBoxSize[ 0 ]!.inlineSize;
|
|
328
|
-
blockSize = entry.borderBoxSize[ 0 ]!.blockSize;
|
|
329
|
-
} else {
|
|
330
|
-
// Fallback for older browsers or if borderBoxSize is missing
|
|
331
|
-
inlineSize = target.offsetWidth;
|
|
332
|
-
blockSize = target.offsetHeight;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
493
|
updates.push({ index, inlineSize, blockSize, element: target });
|
|
336
494
|
}
|
|
337
495
|
}
|
|
338
496
|
|
|
339
|
-
/* v8 ignore else -- @preserve */
|
|
340
497
|
if (updates.length > 0) {
|
|
341
498
|
updateItemSizes(updates);
|
|
342
499
|
}
|
|
343
500
|
});
|
|
344
501
|
|
|
345
|
-
/* v8 ignore next 2 -- @preserve */
|
|
346
502
|
const extraResizeObserver = typeof window === 'undefined'
|
|
347
503
|
? null
|
|
348
504
|
: new ResizeObserver(() => {
|
|
@@ -352,7 +508,6 @@ const extraResizeObserver = typeof window === 'undefined'
|
|
|
352
508
|
});
|
|
353
509
|
|
|
354
510
|
watch(headerRef, (newEl, oldEl) => {
|
|
355
|
-
/* v8 ignore if -- @preserve */
|
|
356
511
|
if (oldEl) {
|
|
357
512
|
extraResizeObserver?.unobserve(oldEl);
|
|
358
513
|
}
|
|
@@ -362,7 +517,6 @@ watch(headerRef, (newEl, oldEl) => {
|
|
|
362
517
|
}, { immediate: true });
|
|
363
518
|
|
|
364
519
|
watch(footerRef, (newEl, oldEl) => {
|
|
365
|
-
/* v8 ignore if -- @preserve */
|
|
366
520
|
if (oldEl) {
|
|
367
521
|
extraResizeObserver?.unobserve(oldEl);
|
|
368
522
|
}
|
|
@@ -371,28 +525,7 @@ watch(footerRef, (newEl, oldEl) => {
|
|
|
371
525
|
}
|
|
372
526
|
}, { immediate: true });
|
|
373
527
|
|
|
374
|
-
const firstRenderedIndex = computed(() => renderedItems.value[ 0 ]?.index);
|
|
375
|
-
watch(firstRenderedIndex, (newIdx, oldIdx) => {
|
|
376
|
-
if (props.direction === 'both') {
|
|
377
|
-
/* v8 ignore else -- @preserve */
|
|
378
|
-
if (oldIdx !== undefined) {
|
|
379
|
-
const oldEl = itemRefs.get(oldIdx);
|
|
380
|
-
if (oldEl) {
|
|
381
|
-
oldEl.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.unobserve(c));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (newIdx !== undefined) {
|
|
385
|
-
const newEl = itemRefs.get(newIdx);
|
|
386
|
-
/* v8 ignore else -- @preserve */
|
|
387
|
-
if (newEl) {
|
|
388
|
-
newEl.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.observe(c));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}, { flush: 'post' });
|
|
393
|
-
|
|
394
528
|
onMounted(() => {
|
|
395
|
-
/* v8 ignore else -- @preserve */
|
|
396
529
|
if (hostRef.value) {
|
|
397
530
|
hostResizeObserver?.observe(hostRef.value);
|
|
398
531
|
}
|
|
@@ -400,14 +533,7 @@ onMounted(() => {
|
|
|
400
533
|
// Re-observe items that were set before observer was ready
|
|
401
534
|
for (const el of itemRefs.values()) {
|
|
402
535
|
itemResizeObserver?.observe(el);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Observe cells of the first rendered item
|
|
406
|
-
/* v8 ignore else -- @preserve */
|
|
407
|
-
if (firstRenderedIndex.value !== undefined) {
|
|
408
|
-
const el = itemRefs.get(firstRenderedIndex.value);
|
|
409
|
-
/* v8 ignore else -- @preserve */
|
|
410
|
-
if (el) {
|
|
536
|
+
if (props.direction === 'both') {
|
|
411
537
|
el.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.observe(c));
|
|
412
538
|
}
|
|
413
539
|
}
|
|
@@ -426,77 +552,86 @@ function setItemRef(el: unknown, index: number) {
|
|
|
426
552
|
if (el) {
|
|
427
553
|
itemRefs.set(index, el as HTMLElement);
|
|
428
554
|
itemResizeObserver?.observe(el as HTMLElement);
|
|
555
|
+
|
|
556
|
+
if (props.direction === 'both') {
|
|
557
|
+
(el as HTMLElement).querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.observe(c));
|
|
558
|
+
}
|
|
429
559
|
} else {
|
|
430
560
|
const oldEl = itemRefs.get(index);
|
|
431
|
-
/* v8 ignore else -- @preserve */
|
|
432
561
|
if (oldEl) {
|
|
433
562
|
itemResizeObserver?.unobserve(oldEl);
|
|
563
|
+
if (props.direction === 'both') {
|
|
564
|
+
oldEl.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.unobserve(c));
|
|
565
|
+
}
|
|
434
566
|
itemRefs.delete(index);
|
|
435
567
|
}
|
|
436
568
|
}
|
|
437
569
|
}
|
|
438
570
|
|
|
439
571
|
function handleKeyDown(event: KeyboardEvent) {
|
|
440
|
-
stopProgrammaticScroll();
|
|
441
572
|
const { viewportSize, scrollOffset } = scrollDetails.value;
|
|
442
573
|
const isHorizontal = props.direction !== 'vertical';
|
|
443
574
|
const isVertical = props.direction !== 'horizontal';
|
|
444
575
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
576
|
+
switch (event.key) {
|
|
577
|
+
case 'Home':
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
stopProgrammaticScroll();
|
|
580
|
+
scrollToIndex(0, 0, 'start');
|
|
581
|
+
break;
|
|
582
|
+
case 'End': {
|
|
583
|
+
event.preventDefault();
|
|
584
|
+
stopProgrammaticScroll();
|
|
585
|
+
const lastItemIndex = props.items.length - 1;
|
|
586
|
+
const lastColIndex = (props.columnCount || 0) > 0 ? props.columnCount - 1 : 0;
|
|
587
|
+
|
|
588
|
+
if (isHorizontal) {
|
|
589
|
+
if (isVertical) {
|
|
590
|
+
scrollToIndex(lastItemIndex, lastColIndex, 'end');
|
|
591
|
+
} else {
|
|
592
|
+
scrollToIndex(0, lastItemIndex, 'end');
|
|
593
|
+
}
|
|
458
594
|
} else {
|
|
459
|
-
scrollToIndex(
|
|
595
|
+
scrollToIndex(lastItemIndex, 0, 'end');
|
|
460
596
|
}
|
|
461
|
-
|
|
462
|
-
scrollToIndex(lastItemIndex, 0, 'end');
|
|
597
|
+
break;
|
|
463
598
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
599
|
+
case 'ArrowUp':
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
stopProgrammaticScroll();
|
|
602
|
+
scrollToOffset(null, scrollOffset.y - DEFAULT_ITEM_SIZE);
|
|
603
|
+
break;
|
|
604
|
+
case 'ArrowDown':
|
|
605
|
+
event.preventDefault();
|
|
606
|
+
stopProgrammaticScroll();
|
|
607
|
+
scrollToOffset(null, scrollOffset.y + DEFAULT_ITEM_SIZE);
|
|
608
|
+
break;
|
|
609
|
+
case 'ArrowLeft':
|
|
610
|
+
event.preventDefault();
|
|
611
|
+
stopProgrammaticScroll();
|
|
612
|
+
scrollToOffset(scrollOffset.x - DEFAULT_ITEM_SIZE, null);
|
|
613
|
+
break;
|
|
614
|
+
case 'ArrowRight':
|
|
615
|
+
event.preventDefault();
|
|
616
|
+
stopProgrammaticScroll();
|
|
617
|
+
scrollToOffset(scrollOffset.x + DEFAULT_ITEM_SIZE, null);
|
|
618
|
+
break;
|
|
619
|
+
case 'PageUp':
|
|
620
|
+
event.preventDefault();
|
|
621
|
+
stopProgrammaticScroll();
|
|
622
|
+
scrollToOffset(
|
|
623
|
+
!isVertical && isHorizontal ? scrollOffset.x - viewportSize.width : null,
|
|
624
|
+
isVertical ? scrollOffset.y - viewportSize.height : null,
|
|
625
|
+
);
|
|
626
|
+
break;
|
|
627
|
+
case 'PageDown':
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
stopProgrammaticScroll();
|
|
630
|
+
scrollToOffset(
|
|
631
|
+
!isVertical && isHorizontal ? scrollOffset.x + viewportSize.width : null,
|
|
632
|
+
isVertical ? scrollOffset.y + viewportSize.height : null,
|
|
633
|
+
);
|
|
634
|
+
break;
|
|
500
635
|
}
|
|
501
636
|
}
|
|
502
637
|
|
|
@@ -506,23 +641,7 @@ onUnmounted(() => {
|
|
|
506
641
|
extraResizeObserver?.disconnect();
|
|
507
642
|
});
|
|
508
643
|
|
|
509
|
-
const isWindowContainer = computed(() =>
|
|
510
|
-
const c = props.container;
|
|
511
|
-
if (
|
|
512
|
-
c === null
|
|
513
|
-
// window
|
|
514
|
-
|| (typeof window !== 'undefined' && c === window)
|
|
515
|
-
) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// body
|
|
520
|
-
if (c && typeof c === 'object' && 'tagName' in c) {
|
|
521
|
-
return (c as HTMLElement).tagName === 'BODY';
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return false;
|
|
525
|
-
});
|
|
644
|
+
const isWindowContainer = computed(() => isWindowLike(props.container));
|
|
526
645
|
|
|
527
646
|
const containerStyle = computed(() => {
|
|
528
647
|
if (isWindowContainer.value) {
|
|
@@ -562,47 +681,15 @@ const spacerStyle = computed(() => ({
|
|
|
562
681
|
}));
|
|
563
682
|
|
|
564
683
|
function getItemStyle(item: RenderedItem<T>) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (isVertical && props.containerTag === 'table') {
|
|
575
|
-
style.minInlineSize = '100%';
|
|
576
|
-
} else {
|
|
577
|
-
style.inlineSize = isVertical ? '100%' : (!isDynamic ? `${ item.size.width }px` : 'auto');
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (isDynamic) {
|
|
581
|
-
if (!isVertical) {
|
|
582
|
-
style.minInlineSize = '1px';
|
|
583
|
-
}
|
|
584
|
-
if (!isHorizontal) {
|
|
585
|
-
style.minBlockSize = '1px';
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (isHydrated.value) {
|
|
590
|
-
if (item.isStickyActive) {
|
|
591
|
-
if (isVertical || isBoth) {
|
|
592
|
-
style.insetBlockStart = `${ getPaddingY(props.scrollPaddingStart, props.direction) }px`;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (isHorizontal || isBoth) {
|
|
596
|
-
style.insetInlineStart = `${ getPaddingX(props.scrollPaddingStart, props.direction) }px`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
style.transform = `translate(${ item.stickyOffset.x }px, ${ item.stickyOffset.y }px)`;
|
|
600
|
-
} else {
|
|
601
|
-
style.transform = `translate(${ item.offset.x }px, ${ item.offset.y }px)`;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return style;
|
|
684
|
+
return calculateItemStyle({
|
|
685
|
+
containerTag: props.containerTag,
|
|
686
|
+
direction: props.direction,
|
|
687
|
+
isHydrated: isHydrated.value,
|
|
688
|
+
item,
|
|
689
|
+
itemSize: props.itemSize,
|
|
690
|
+
paddingStartX: (virtualScrollProps.value.scrollPaddingStart as { x: number; y: number; }).x,
|
|
691
|
+
paddingStartY: (virtualScrollProps.value.scrollPaddingStart as { x: number; y: number; }).y,
|
|
692
|
+
});
|
|
606
693
|
}
|
|
607
694
|
|
|
608
695
|
const isDebug = computed(() => props.debug);
|
|
@@ -611,12 +698,59 @@ const headerTag = computed(() => isTable.value ? 'thead' : 'div');
|
|
|
611
698
|
const footerTag = computed(() => isTable.value ? 'tfoot' : 'div');
|
|
612
699
|
|
|
613
700
|
defineExpose({
|
|
701
|
+
/**
|
|
702
|
+
* Detailed information about the current scroll state.
|
|
703
|
+
* @see ScrollDetails
|
|
704
|
+
* @see useVirtualScroll
|
|
705
|
+
*/
|
|
614
706
|
scrollDetails,
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Information about the current visible range of columns.
|
|
710
|
+
* @see ColumnRange
|
|
711
|
+
* @see useVirtualScroll
|
|
712
|
+
*/
|
|
615
713
|
columnRange,
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Helper to get the width of a specific column.
|
|
717
|
+
* @param index - The column index.
|
|
718
|
+
* @see useVirtualScroll
|
|
719
|
+
*/
|
|
616
720
|
getColumnWidth,
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Programmatically scroll to a specific row and/or column.
|
|
724
|
+
*
|
|
725
|
+
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
726
|
+
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
727
|
+
* @param options - Alignment and behavior options. Defaults to { align: 'auto', behavior: 'auto' }.
|
|
728
|
+
* @see ScrollAlignment
|
|
729
|
+
* @see ScrollToIndexOptions
|
|
730
|
+
* @see useVirtualScroll
|
|
731
|
+
*/
|
|
617
732
|
scrollToIndex,
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Programmatically scroll to a specific pixel offset.
|
|
736
|
+
*
|
|
737
|
+
* @param x - The pixel offset to scroll to on the X axis. Pass null to keep current position.
|
|
738
|
+
* @param y - The pixel offset to scroll to on the Y axis. Pass null to keep current position.
|
|
739
|
+
* @param options - Scroll options (behavior). Defaults to { behavior: 'auto' }.
|
|
740
|
+
* @see useVirtualScroll
|
|
741
|
+
*/
|
|
618
742
|
scrollToOffset,
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Resets all dynamic measurements and re-initializes from props.
|
|
746
|
+
* @see useVirtualScroll
|
|
747
|
+
*/
|
|
619
748
|
refresh,
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Immediately stops any currently active smooth scroll animation and clears pending corrections.
|
|
752
|
+
* @see useVirtualScroll
|
|
753
|
+
*/
|
|
620
754
|
stopProgrammaticScroll,
|
|
621
755
|
});
|
|
622
756
|
</script>
|
|
@@ -641,7 +775,6 @@ defineExpose({
|
|
|
641
775
|
@pointerdown.passive="stopProgrammaticScroll"
|
|
642
776
|
@touchstart.passive="stopProgrammaticScroll"
|
|
643
777
|
>
|
|
644
|
-
<!-- v8 ignore start -->
|
|
645
778
|
<component
|
|
646
779
|
:is="headerTag"
|
|
647
780
|
v-if="slots.header"
|
|
@@ -651,7 +784,6 @@ defineExpose({
|
|
|
651
784
|
>
|
|
652
785
|
<slot name="header" />
|
|
653
786
|
</component>
|
|
654
|
-
<!-- v8 ignore stop -->
|
|
655
787
|
|
|
656
788
|
<component
|
|
657
789
|
:is="wrapperTag"
|
|
@@ -660,7 +792,6 @@ defineExpose({
|
|
|
660
792
|
:style="wrapperStyle"
|
|
661
793
|
>
|
|
662
794
|
<!-- Phantom element to push scroll height -->
|
|
663
|
-
<!-- v8 ignore start -->
|
|
664
795
|
<component
|
|
665
796
|
:is="itemTag"
|
|
666
797
|
v-if="isTable"
|
|
@@ -669,7 +800,6 @@ defineExpose({
|
|
|
669
800
|
>
|
|
670
801
|
<td style="padding: 0; border: none; block-size: inherit;" />
|
|
671
802
|
</component>
|
|
672
|
-
<!-- v8 ignore stop -->
|
|
673
803
|
|
|
674
804
|
<component
|
|
675
805
|
:is="itemTag"
|
|
@@ -699,7 +829,6 @@ defineExpose({
|
|
|
699
829
|
</component>
|
|
700
830
|
</component>
|
|
701
831
|
|
|
702
|
-
<!-- v8 ignore start -->
|
|
703
832
|
<div
|
|
704
833
|
v-if="loading && slots.loading"
|
|
705
834
|
class="virtual-scroll-loading"
|
|
@@ -717,7 +846,6 @@ defineExpose({
|
|
|
717
846
|
>
|
|
718
847
|
<slot name="footer" />
|
|
719
848
|
</component>
|
|
720
|
-
<!-- v8 ignore stop -->
|
|
721
849
|
</component>
|
|
722
850
|
</template>
|
|
723
851
|
|
|
@@ -754,6 +882,7 @@ defineExpose({
|
|
|
754
882
|
}
|
|
755
883
|
|
|
756
884
|
.virtual-scroll-item {
|
|
885
|
+
display: grid;
|
|
757
886
|
box-sizing: border-box;
|
|
758
887
|
will-change: transform;
|
|
759
888
|
|