@pdanpdan/virtual-scroll 0.2.0 → 0.2.1
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/dist/index.css +1 -1
- package/dist/index.js +152 -131
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/VirtualScroll.test.ts +353 -332
- package/src/components/VirtualScroll.vue +24 -6
- package/src/composables/useVirtualScroll.test.ts +545 -204
- package/src/composables/useVirtualScroll.ts +28 -25
- package/src/utils/fenwick-tree.test.ts +80 -65
|
@@ -144,9 +144,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
144
144
|
options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
145
145
|
} | null>(null);
|
|
146
146
|
|
|
147
|
-
const maxWidth = ref(0);
|
|
148
|
-
const maxHeight = ref(0);
|
|
149
|
-
|
|
150
147
|
// Track if sizes are initialized
|
|
151
148
|
const sizesInitialized = ref(false);
|
|
152
149
|
let lastItems: T[] = [];
|
|
@@ -184,6 +181,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
184
181
|
if (props.value.direction === 'both' && colCount > 0) {
|
|
185
182
|
return columnSizes.query(colEnd || colCount) - columnSizes.query(colStart);
|
|
186
183
|
}
|
|
184
|
+
/* v8 ignore else -- @preserve */
|
|
187
185
|
if (props.value.direction === 'horizontal') {
|
|
188
186
|
if (fixedItemSize.value !== null) {
|
|
189
187
|
return (end - start) * (fixedItemSize.value + (props.value.columnGap || 0));
|
|
@@ -213,6 +211,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
213
211
|
|
|
214
212
|
if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
|
|
215
213
|
const { start, end } = props.value.ssrRange;
|
|
214
|
+
/* v8 ignore else -- @preserve */
|
|
216
215
|
if (props.value.direction === 'vertical' || props.value.direction === 'both') {
|
|
217
216
|
if (fixedItemSize.value !== null) {
|
|
218
217
|
return (end - start) * (fixedItemSize.value + (props.value.gap || 0));
|
|
@@ -367,6 +366,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
367
366
|
} else {
|
|
368
367
|
const isVisibleX = targetX >= relativeScrollX.value && (targetX + itemWidth) <= (relativeScrollX.value + usableWidth);
|
|
369
368
|
if (!isVisibleX) {
|
|
369
|
+
/* v8 ignore if -- @preserve */
|
|
370
370
|
if (targetX < relativeScrollX.value) {
|
|
371
371
|
// keep targetX at start
|
|
372
372
|
} else {
|
|
@@ -396,6 +396,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
396
396
|
let viewW = 0;
|
|
397
397
|
let viewH = 0;
|
|
398
398
|
|
|
399
|
+
/* v8 ignore else -- @preserve */
|
|
399
400
|
if (typeof window !== 'undefined') {
|
|
400
401
|
if (container === window) {
|
|
401
402
|
curX = window.scrollX;
|
|
@@ -416,6 +417,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
416
417
|
if (!reachedX && colIndex !== null && colIndex !== undefined) {
|
|
417
418
|
const atLeft = curX <= tolerance && finalX <= tolerance;
|
|
418
419
|
const atRight = curX >= maxW - viewW - tolerance && finalX >= maxW - viewW - tolerance;
|
|
420
|
+
/* v8 ignore else -- @preserve */
|
|
419
421
|
if (atLeft || atRight) {
|
|
420
422
|
reachedX = true;
|
|
421
423
|
}
|
|
@@ -495,12 +497,24 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
495
497
|
|
|
496
498
|
const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
|
|
497
499
|
const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
|
|
500
|
+
const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
|
|
501
|
+
const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
|
|
502
|
+
|
|
503
|
+
const usableWidth = viewportWidth.value - (isHorizontal ? (paddingStartX + paddingEndX) : 0);
|
|
504
|
+
const usableHeight = viewportHeight.value - (isVertical ? (paddingStartY + paddingEndY) : 0);
|
|
505
|
+
|
|
506
|
+
const clampedX = (x !== null && x !== undefined)
|
|
507
|
+
? (isHorizontal ? Math.max(0, Math.min(x, Math.max(0, totalWidth.value - usableWidth))) : Math.max(0, x))
|
|
508
|
+
: null;
|
|
509
|
+
const clampedY = (y !== null && y !== undefined)
|
|
510
|
+
? (isVertical ? Math.max(0, Math.min(y, Math.max(0, totalHeight.value - usableHeight))) : Math.max(0, y))
|
|
511
|
+
: null;
|
|
498
512
|
|
|
499
513
|
const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
|
|
500
514
|
const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
|
|
501
515
|
|
|
502
|
-
const targetX = (
|
|
503
|
-
const targetY = (
|
|
516
|
+
const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
|
|
517
|
+
const targetY = (clampedY !== null) ? clampedY + hostOffset.y - (isVertical ? paddingStartY : 0) : currentY;
|
|
504
518
|
|
|
505
519
|
if (typeof window !== 'undefined' && container === window) {
|
|
506
520
|
window.scrollTo({
|
|
@@ -571,6 +585,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
571
585
|
let prependCount = 0;
|
|
572
586
|
if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
|
|
573
587
|
const oldFirstItem = lastItems[ 0 ];
|
|
588
|
+
/* v8 ignore else -- @preserve */
|
|
574
589
|
if (oldFirstItem !== undefined) {
|
|
575
590
|
for (let i = 1; i <= len - lastItems.length; i++) {
|
|
576
591
|
if (newItems[ i ] === oldFirstItem) {
|
|
@@ -614,6 +629,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
614
629
|
}
|
|
615
630
|
}
|
|
616
631
|
|
|
632
|
+
/* v8 ignore else -- @preserve */
|
|
617
633
|
if (addedX > 0 || addedY > 0) {
|
|
618
634
|
nextTick(() => {
|
|
619
635
|
scrollToOffset(
|
|
@@ -637,6 +653,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
637
653
|
// OR if it's dynamic but we don't have a measurement yet.
|
|
638
654
|
if (!isDynamicColumnWidth.value || currentW === 0) {
|
|
639
655
|
const targetW = width + columnGap;
|
|
656
|
+
/* v8 ignore else -- @preserve */
|
|
640
657
|
if (Math.abs(currentW - targetW) > 0.5) {
|
|
641
658
|
columnSizes.set(i, targetW);
|
|
642
659
|
colNeedsRebuild = true;
|
|
@@ -681,17 +698,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
681
698
|
itemSizesY.set(i, targetY);
|
|
682
699
|
itemsNeedRebuild = true;
|
|
683
700
|
}
|
|
684
|
-
|
|
685
|
-
// Max dimension tracking: determines scrollable area size
|
|
686
|
-
const w = isHorizontal ? size : (isBoth ? Math.max(size, viewportWidth.value) : 0);
|
|
687
|
-
const h = (isVertical || isBoth) ? size : 0;
|
|
688
|
-
|
|
689
|
-
if (w > maxWidth.value) {
|
|
690
|
-
maxWidth.value = w;
|
|
691
|
-
}
|
|
692
|
-
if (h > maxHeight.value) {
|
|
693
|
-
maxHeight.value = h;
|
|
694
|
-
}
|
|
695
701
|
}
|
|
696
702
|
|
|
697
703
|
if (itemsNeedRebuild) {
|
|
@@ -955,6 +961,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
955
961
|
if (nextStickyIdx !== undefined) {
|
|
956
962
|
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : itemSizesY.query(nextStickyIdx);
|
|
957
963
|
const distance = nextStickyY - relativeScrollY.value;
|
|
964
|
+
/* v8 ignore else -- @preserve */
|
|
958
965
|
if (distance < height) {
|
|
959
966
|
stickyOffset.y = -(height - distance);
|
|
960
967
|
}
|
|
@@ -980,6 +987,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
980
987
|
if (nextStickyIdx !== undefined) {
|
|
981
988
|
const nextStickyX = fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : itemSizesX.query(nextStickyIdx);
|
|
982
989
|
const distance = nextStickyX - relativeScrollX.value;
|
|
990
|
+
/* v8 ignore else -- @preserve */
|
|
983
991
|
if (distance < width) {
|
|
984
992
|
stickyOffset.x = -(width - distance);
|
|
985
993
|
}
|
|
@@ -1131,13 +1139,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1131
1139
|
|
|
1132
1140
|
for (const { index, inlineSize, blockSize, element } of updates) {
|
|
1133
1141
|
if (isDynamicItemSize.value) {
|
|
1134
|
-
if (inlineSize > maxWidth.value) {
|
|
1135
|
-
maxWidth.value = inlineSize;
|
|
1136
|
-
}
|
|
1137
|
-
if (blockSize > maxHeight.value) {
|
|
1138
|
-
maxHeight.value = blockSize;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1142
|
if (props.value.direction === 'horizontal') {
|
|
1142
1143
|
const oldWidth = itemSizesX.get(index);
|
|
1143
1144
|
const targetWidth = inlineSize + columnGap;
|
|
@@ -1179,10 +1180,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1179
1180
|
for (const child of cells) {
|
|
1180
1181
|
const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
|
|
1181
1182
|
|
|
1183
|
+
/* v8 ignore else -- @preserve */
|
|
1182
1184
|
if (colIndex >= 0 && colIndex < (props.value.columnCount || 0)) {
|
|
1183
1185
|
const w = child.offsetWidth;
|
|
1184
1186
|
const oldW = columnSizes.get(colIndex);
|
|
1185
1187
|
const targetW = w + columnGap;
|
|
1188
|
+
/* v8 ignore else -- @preserve */
|
|
1186
1189
|
if (targetW > oldW || !measuredColumns[ colIndex ]) {
|
|
1187
1190
|
columnSizes.update(colIndex, targetW - oldW);
|
|
1188
1191
|
measuredColumns[ colIndex ] = 1;
|
|
@@ -1262,6 +1265,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1262
1265
|
|
|
1263
1266
|
resizeObserver = new ResizeObserver((entries) => {
|
|
1264
1267
|
for (const entry of entries) {
|
|
1268
|
+
/* v8 ignore else -- @preserve */
|
|
1265
1269
|
if (entry.target === container) {
|
|
1266
1270
|
viewportWidth.value = (container as HTMLElement).clientWidth;
|
|
1267
1271
|
viewportHeight.value = (container as HTMLElement).clientHeight;
|
|
@@ -1298,6 +1302,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1298
1302
|
: props.value.ssrRange?.start;
|
|
1299
1303
|
const initialAlign = props.value.initialScrollAlign || 'start';
|
|
1300
1304
|
|
|
1305
|
+
/* v8 ignore else -- @preserve */
|
|
1301
1306
|
if (initialIndex !== undefined && initialIndex !== null) {
|
|
1302
1307
|
scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
|
|
1303
1308
|
}
|
|
@@ -1328,8 +1333,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
|
|
|
1328
1333
|
measuredColumns.fill(0);
|
|
1329
1334
|
measuredItemsX.fill(0);
|
|
1330
1335
|
measuredItemsY.fill(0);
|
|
1331
|
-
maxWidth.value = 0;
|
|
1332
|
-
maxHeight.value = 0;
|
|
1333
1336
|
initializeSizes();
|
|
1334
1337
|
};
|
|
1335
1338
|
|
|
@@ -3,84 +3,99 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import { FenwickTree } from './fenwick-tree';
|
|
4
4
|
|
|
5
5
|
describe('fenwickTree', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
describe('initialization', () => {
|
|
7
|
+
it('should initialize with correct size', () => {
|
|
8
|
+
const tree = new FenwickTree(5);
|
|
9
|
+
expect(tree.query(5)).toBe(0);
|
|
10
|
+
expect(tree.length).toBe(5);
|
|
11
|
+
});
|
|
9
12
|
});
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
describe('query and update', () => {
|
|
15
|
+
it('should update and query values', () => {
|
|
16
|
+
const tree = new FenwickTree(5);
|
|
17
|
+
tree.update(0, 10);
|
|
18
|
+
tree.update(1, 20);
|
|
19
|
+
tree.update(2, 30);
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
expect(tree.query(0)).toBe(0);
|
|
22
|
+
expect(tree.query(1)).toBe(10);
|
|
23
|
+
expect(tree.query(2)).toBe(30);
|
|
24
|
+
expect(tree.query(3)).toBe(60);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle updates to existing indices', () => {
|
|
28
|
+
const tree = new FenwickTree(3);
|
|
29
|
+
tree.update(1, 10);
|
|
30
|
+
expect(tree.query(2)).toBe(10);
|
|
31
|
+
tree.update(1, 5); // Add 5 to index 1
|
|
32
|
+
expect(tree.query(2)).toBe(15);
|
|
33
|
+
});
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
it('should ignore updates for out of bounds indices', () => {
|
|
36
|
+
const tree = new FenwickTree(5);
|
|
37
|
+
tree.update(-1, 10);
|
|
38
|
+
tree.update(5, 10);
|
|
39
|
+
expect(tree.query(5)).toBe(0);
|
|
40
|
+
});
|
|
29
41
|
});
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
describe('search and bounds', () => {
|
|
44
|
+
it('should find lower bound correctly', () => {
|
|
45
|
+
const tree = new FenwickTree(5);
|
|
46
|
+
tree.update(0, 10); // sum up to 1: 10
|
|
47
|
+
tree.update(1, 10); // sum up to 2: 20
|
|
48
|
+
tree.update(2, 10); // sum up to 3: 30
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
expect(tree.findLowerBound(5)).toBe(0);
|
|
51
|
+
expect(tree.findLowerBound(15)).toBe(1);
|
|
52
|
+
expect(tree.findLowerBound(25)).toBe(2);
|
|
53
|
+
expect(tree.findLowerBound(35)).toBe(5); // Returns size when not found
|
|
54
|
+
});
|
|
41
55
|
});
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
describe('rebuild and resize', () => {
|
|
58
|
+
it('should set and rebuild correctly', () => {
|
|
59
|
+
const tree = new FenwickTree(5);
|
|
60
|
+
tree.set(0, 10);
|
|
61
|
+
tree.set(1, 20);
|
|
62
|
+
tree.set(2, 30);
|
|
63
|
+
tree.set(-1, 40); // ignore
|
|
64
|
+
tree.set(5, 50); // ignore
|
|
65
|
+
expect(tree.query(3)).toBe(0); // not rebuilt yet
|
|
66
|
+
tree.rebuild();
|
|
67
|
+
expect(tree.query(3)).toBe(60);
|
|
68
|
+
});
|
|
51
69
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
it('should resize and preserve existing values', () => {
|
|
71
|
+
const tree = new FenwickTree(5);
|
|
72
|
+
tree.update(0, 10);
|
|
73
|
+
tree.resize(10);
|
|
74
|
+
expect(tree.query(1)).toBe(10);
|
|
75
|
+
expect(tree.query(10)).toBe(10);
|
|
76
|
+
tree.resize(10); // same size
|
|
77
|
+
});
|
|
57
78
|
});
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
tree.rebuild();
|
|
68
|
-
expect(tree.query(3)).toBe(60);
|
|
69
|
-
});
|
|
80
|
+
describe('values access', () => {
|
|
81
|
+
it('should return the individual value at an index', () => {
|
|
82
|
+
const tree = new FenwickTree(3);
|
|
83
|
+
tree.update(0, 10);
|
|
84
|
+
expect(tree.get(0)).toBe(10);
|
|
85
|
+
expect(tree.get(-1)).toBe(0);
|
|
86
|
+
expect(tree.get(10)).toBe(0);
|
|
87
|
+
});
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expect(tree.get(0)).toBe(10);
|
|
82
|
-
expect(tree.get(-1)).toBe(0);
|
|
83
|
-
expect(tree.get(10)).toBe(0);
|
|
89
|
+
it('should return the underlying values array', () => {
|
|
90
|
+
const tree = new FenwickTree(3);
|
|
91
|
+
tree.update(0, 10);
|
|
92
|
+
tree.update(1, 20);
|
|
93
|
+
const values = tree.getValues();
|
|
94
|
+
expect(values).toBeInstanceOf(Float64Array);
|
|
95
|
+
expect(values[ 0 ]).toBe(10);
|
|
96
|
+
expect(values[ 1 ]).toBe(20);
|
|
97
|
+
expect(values[ 2 ]).toBe(0);
|
|
98
|
+
});
|
|
84
99
|
});
|
|
85
100
|
|
|
86
101
|
describe('shift', () => {
|