@pdanpdan/virtual-scroll 0.1.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/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as VirtualScroll } from './components/VirtualScroll.vue';
2
+ export * from './composables/useVirtualScroll.js';
3
+ export * from './utils/fenwick-tree.js';
4
+ export * from './utils/scroll.js';
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { FenwickTree } from './fenwick-tree';
4
+
5
+ describe('fenwickTree', () => {
6
+ it('should initialize with correct size', () => {
7
+ const tree = new FenwickTree(5);
8
+ expect(tree.query(5)).toBe(0);
9
+ });
10
+
11
+ it('should update and query values', () => {
12
+ const tree = new FenwickTree(5);
13
+ tree.update(0, 10);
14
+ tree.update(1, 20);
15
+ tree.update(2, 30);
16
+
17
+ expect(tree.query(0)).toBe(0);
18
+ expect(tree.query(1)).toBe(10);
19
+ expect(tree.query(2)).toBe(30);
20
+ expect(tree.query(3)).toBe(60);
21
+ });
22
+
23
+ it('should handle updates to existing indices', () => {
24
+ const tree = new FenwickTree(3);
25
+ tree.update(1, 10);
26
+ expect(tree.query(2)).toBe(10);
27
+ tree.update(1, 5); // Add 5 to index 1
28
+ expect(tree.query(2)).toBe(15);
29
+ });
30
+
31
+ it('should find lower bound correctly', () => {
32
+ const tree = new FenwickTree(5);
33
+ tree.update(0, 10); // sum up to 1: 10
34
+ tree.update(1, 10); // sum up to 2: 20
35
+ tree.update(2, 10); // sum up to 3: 30
36
+
37
+ expect(tree.findLowerBound(5)).toBe(0);
38
+ expect(tree.findLowerBound(15)).toBe(1);
39
+ expect(tree.findLowerBound(25)).toBe(2);
40
+ expect(tree.findLowerBound(35)).toBe(5); // Returns size when not found
41
+ });
42
+
43
+ it('should resize and preserve existing values', () => {
44
+ const tree = new FenwickTree(5);
45
+ tree.update(0, 10);
46
+ tree.resize(10);
47
+ expect(tree.query(1)).toBe(10);
48
+ expect(tree.query(10)).toBe(10);
49
+ tree.resize(10); // same size
50
+ });
51
+
52
+ it('should ignore updates for out of bounds indices', () => {
53
+ const tree = new FenwickTree(5);
54
+ tree.update(-1, 10);
55
+ tree.update(5, 10);
56
+ expect(tree.query(5)).toBe(0);
57
+ });
58
+
59
+ it('should set and rebuild correctly', () => {
60
+ const tree = new FenwickTree(5);
61
+ tree.set(0, 10);
62
+ tree.set(1, 20);
63
+ tree.set(2, 30);
64
+ tree.set(-1, 40); // ignore
65
+ tree.set(5, 50); // ignore
66
+ expect(tree.query(3)).toBe(0); // not rebuilt yet
67
+ tree.rebuild();
68
+ expect(tree.query(3)).toBe(60);
69
+ });
70
+
71
+ it('should return the underlying values array', () => {
72
+ const tree = new FenwickTree(3);
73
+ expect(tree.length).toBe(3);
74
+ tree.update(0, 10);
75
+ tree.update(1, 20);
76
+ const values = tree.getValues();
77
+ expect(values).toBeInstanceOf(Float64Array);
78
+ expect(values[ 0 ]).toBe(10);
79
+ expect(values[ 1 ]).toBe(20);
80
+ expect(values[ 2 ]).toBe(0);
81
+ expect(tree.get(0)).toBe(10);
82
+ expect(tree.get(-1)).toBe(0);
83
+ expect(tree.get(10)).toBe(0);
84
+ });
85
+
86
+ describe('shift', () => {
87
+ it('should do nothing when offset is 0', () => {
88
+ const tree = new FenwickTree(3);
89
+ tree.update(0, 10);
90
+ tree.shift(0);
91
+ expect(tree.get(0)).toBe(10);
92
+ });
93
+
94
+ it('should shift values forward when offset is positive', () => {
95
+ const tree = new FenwickTree(5);
96
+ tree.update(0, 10);
97
+ tree.update(1, 20);
98
+ tree.shift(2);
99
+ expect(tree.get(0)).toBe(0);
100
+ expect(tree.get(1)).toBe(0);
101
+ expect(tree.get(2)).toBe(10);
102
+ expect(tree.get(3)).toBe(20);
103
+ expect(tree.query(3)).toBe(10);
104
+ expect(tree.query(4)).toBe(30);
105
+ });
106
+
107
+ it('should shift values backward when offset is negative', () => {
108
+ const tree = new FenwickTree(5);
109
+ tree.update(2, 10);
110
+ tree.update(3, 20);
111
+ tree.shift(-2);
112
+ expect(tree.get(0)).toBe(10);
113
+ expect(tree.get(1)).toBe(20);
114
+ expect(tree.get(2)).toBe(0);
115
+ expect(tree.query(1)).toBe(10);
116
+ expect(tree.query(2)).toBe(30);
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Fenwick Tree (Binary Indexed Tree) implementation for efficient
3
+ * prefix sum calculations and updates.
4
+ */
5
+ export class FenwickTree {
6
+ private tree: Float64Array;
7
+ private values: Float64Array;
8
+
9
+ constructor(size: number) {
10
+ this.tree = new Float64Array(size + 1);
11
+ this.values = new Float64Array(size);
12
+ }
13
+
14
+ /**
15
+ * Update the value at a specific index and propagate changes.
16
+ * @param index 0-based index
17
+ * @param delta The change in value (new value - old value)
18
+ */
19
+ update(index: number, delta: number): void {
20
+ if (index < 0 || index >= this.values.length) {
21
+ return;
22
+ }
23
+ this.values[ index ] = this.values[ index ]! + delta;
24
+
25
+ index++; // 1-based index
26
+ while (index < this.tree.length) {
27
+ this.tree[ index ] = this.tree[ index ]! + delta;
28
+ index += index & -index;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get the prefix sum up to a specific index (exclusive).
34
+ * @param index 0-based index. query(n) returns sum of values from 0 to n-1.
35
+ * @returns Sum of values in range [0, index)
36
+ */
37
+ query(index: number): number {
38
+ let sum = 0;
39
+ while (index > 0) {
40
+ sum += this.tree[ index ] || 0;
41
+ index -= index & -index;
42
+ }
43
+ return sum;
44
+ }
45
+
46
+ /**
47
+ * Set the individual value at an index without updating the tree.
48
+ * Call rebuild() after multiple sets to update the tree efficiently.
49
+ */
50
+ set(index: number, value: number): void {
51
+ if (index < 0 || index >= this.values.length) {
52
+ return;
53
+ }
54
+ this.values[ index ] = value;
55
+ }
56
+
57
+ /**
58
+ * Get the number of items in the tree.
59
+ */
60
+ get length(): number {
61
+ return this.values.length;
62
+ }
63
+
64
+ /**
65
+ * Get the individual value at an index.
66
+ */
67
+ get(index: number): number {
68
+ return this.values[ index ] || 0;
69
+ }
70
+
71
+ /**
72
+ * Get the underlying values array.
73
+ */
74
+ getValues(): Readonly<Float64Array> {
75
+ return this.values;
76
+ }
77
+
78
+ /**
79
+ * Find the largest index such that the prefix sum is less than or equal to the given value.
80
+ * Useful for finding which item is at a specific scroll offset.
81
+ * @param value The prefix sum value to search for
82
+ * @returns The 0-based index
83
+ */
84
+ findLowerBound(value: number): number {
85
+ let index = 0;
86
+ const len = this.tree.length;
87
+ let power = 1 << Math.floor(Math.log2(len - 1));
88
+
89
+ while (power > 0) {
90
+ const nextIndex = index + power;
91
+ if (nextIndex < len) {
92
+ const treeVal = this.tree[ nextIndex ] || 0;
93
+ if (treeVal <= value) {
94
+ index = nextIndex;
95
+ value -= treeVal;
96
+ }
97
+ }
98
+ power >>= 1;
99
+ }
100
+ return index;
101
+ }
102
+
103
+ /**
104
+ * Rebuild the entire tree from the current values array in O(N).
105
+ * Useful after bulk updates to the values array.
106
+ */
107
+ rebuild(): void {
108
+ this.tree.fill(0);
109
+ for (let i = 0; i < this.values.length; i++) {
110
+ this.tree[ i + 1 ] = this.values[ i ] || 0;
111
+ }
112
+ for (let i = 1; i < this.tree.length; i++) {
113
+ const j = i + (i & -i);
114
+ if (j < this.tree.length) {
115
+ this.tree[ j ] = this.tree[ j ]! + this.tree[ i ]!;
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Resize the tree while preserving existing values.
122
+ * @param size New size of the tree
123
+ */
124
+ resize(size: number): void {
125
+ if (size === this.values.length) {
126
+ return;
127
+ }
128
+ const newValues = new Float64Array(size);
129
+ newValues.set(this.values.subarray(0, Math.min(size, this.values.length)));
130
+
131
+ this.values = newValues;
132
+ this.tree = new Float64Array(size + 1);
133
+ this.rebuild();
134
+ }
135
+
136
+ /**
137
+ * Shift values by a given offset and rebuild the tree.
138
+ * Useful when items are prepended to the list.
139
+ * @param offset Number of positions to shift (positive for prepending)
140
+ */
141
+ shift(offset: number): void {
142
+ if (offset === 0) {
143
+ return;
144
+ }
145
+ const size = this.values.length;
146
+ const newValues = new Float64Array(size);
147
+ if (offset > 0) {
148
+ newValues.set(this.values.subarray(0, Math.min(size - offset, this.values.length)), offset);
149
+ } else {
150
+ newValues.set(this.values.subarray(-offset));
151
+ }
152
+ this.values = newValues;
153
+ this.rebuild();
154
+ }
155
+ }
@@ -0,0 +1,59 @@
1
+ import type { ScrollDirection, ScrollToIndexOptions } from '../composables/useVirtualScroll.js';
2
+
3
+ /**
4
+ * Checks if the container has a bounding client rect method.
5
+ *
6
+ * @param container - The container element or window to check.
7
+ * @returns True if the container is an HTMLElement with getBoundingClientRect.
8
+ */
9
+ export function isElement(container: HTMLElement | Window | null | undefined): container is HTMLElement {
10
+ return !!container && 'getBoundingClientRect' in container;
11
+ }
12
+
13
+ /**
14
+ * Checks if the target is an element with scroll properties.
15
+ *
16
+ * @param target - The event target to check.
17
+ * @returns True if the target is an HTMLElement with scroll properties.
18
+ */
19
+ export function isScrollableElement(target: EventTarget | null): target is HTMLElement {
20
+ return !!target && 'scrollLeft' in target;
21
+ }
22
+
23
+ /**
24
+ * Helper to determine if an options argument is the full ScrollToIndexOptions object.
25
+ *
26
+ * @param options - The options object to check.
27
+ * @returns True if the options object contains scroll-to-index specific properties.
28
+ */
29
+ export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
30
+ return typeof options === 'object' && options !== null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
31
+ }
32
+
33
+ /**
34
+ * Extracts the horizontal padding from a padding value or object.
35
+ *
36
+ * @param p - The padding value (number or object with x/y).
37
+ * @param direction - The current scroll direction.
38
+ * @returns The horizontal padding in pixels.
39
+ */
40
+ export function getPaddingX(p: number | { x?: number; y?: number; } | undefined, direction?: ScrollDirection) {
41
+ if (typeof p === 'object' && p !== null) {
42
+ return p.x || 0;
43
+ }
44
+ return direction === 'horizontal' ? (p || 0) : 0;
45
+ }
46
+
47
+ /**
48
+ * Extracts the vertical padding from a padding value or object.
49
+ *
50
+ * @param p - The padding value (number or object with x/y).
51
+ * @param direction - The current scroll direction.
52
+ * @returns The vertical padding in pixels.
53
+ */
54
+ export function getPaddingY(p: number | { x?: number; y?: number; } | undefined, direction?: ScrollDirection) {
55
+ if (typeof p === 'object' && p !== null) {
56
+ return p.y || 0;
57
+ }
58
+ return (direction === 'vertical' || direction === 'both') ? (p || 0) : 0;
59
+ }