@pdanpdan/virtual-scroll 0.3.0 → 0.5.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.
@@ -4,15 +4,15 @@ import { FenwickTree } from './fenwick-tree';
4
4
 
5
5
  describe('fenwickTree', () => {
6
6
  describe('initialization', () => {
7
- it('should initialize with correct size', () => {
7
+ it('initializes with correct size', () => {
8
8
  const tree = new FenwickTree(5);
9
9
  expect(tree.query(5)).toBe(0);
10
10
  expect(tree.length).toBe(5);
11
11
  });
12
12
  });
13
13
 
14
- describe('query and update', () => {
15
- it('should update and query values', () => {
14
+ describe('updates & queries', () => {
15
+ it('updates and queries values', () => {
16
16
  const tree = new FenwickTree(5);
17
17
  tree.update(0, 10);
18
18
  tree.update(1, 20);
@@ -24,7 +24,7 @@ describe('fenwickTree', () => {
24
24
  expect(tree.query(3)).toBe(60);
25
25
  });
26
26
 
27
- it('should handle updates to existing indices', () => {
27
+ it('handles updates to existing indices', () => {
28
28
  const tree = new FenwickTree(3);
29
29
  tree.update(1, 10);
30
30
  expect(tree.query(2)).toBe(10);
@@ -32,7 +32,7 @@ describe('fenwickTree', () => {
32
32
  expect(tree.query(2)).toBe(15);
33
33
  });
34
34
 
35
- it('should ignore updates for out of bounds indices', () => {
35
+ it('ignores updates for out of bounds indices', () => {
36
36
  const tree = new FenwickTree(5);
37
37
  tree.update(-1, 10);
38
38
  tree.update(5, 10);
@@ -40,22 +40,29 @@ describe('fenwickTree', () => {
40
40
  });
41
41
  });
42
42
 
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
43
+ describe('value access', () => {
44
+ it('returns the individual value at an index', () => {
45
+ const tree = new FenwickTree(3);
46
+ tree.update(0, 10);
47
+ expect(tree.get(0)).toBe(10);
48
+ expect(tree.get(-1)).toBe(0);
49
+ expect(tree.get(10)).toBe(0);
50
+ });
49
51
 
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
52
+ it('returns the underlying values array', () => {
53
+ const tree = new FenwickTree(3);
54
+ tree.update(0, 10);
55
+ tree.update(1, 20);
56
+ const values = tree.getValues();
57
+ expect(values).toBeInstanceOf(Float64Array);
58
+ expect(values[ 0 ]).toBe(10);
59
+ expect(values[ 1 ]).toBe(20);
60
+ expect(values[ 2 ]).toBe(0);
54
61
  });
55
62
  });
56
63
 
57
- describe('rebuild and resize', () => {
58
- it('should set and rebuild correctly', () => {
64
+ describe('rebuild & resize', () => {
65
+ it('sets and rebuilds correctly', () => {
59
66
  const tree = new FenwickTree(5);
60
67
  tree.set(0, 10);
61
68
  tree.set(1, 20);
@@ -67,7 +74,7 @@ describe('fenwickTree', () => {
67
74
  expect(tree.query(3)).toBe(60);
68
75
  });
69
76
 
70
- it('should resize and preserve existing values', () => {
77
+ it('resizes and preserves existing values', () => {
71
78
  const tree = new FenwickTree(5);
72
79
  tree.update(0, 10);
73
80
  tree.resize(10);
@@ -77,36 +84,29 @@ describe('fenwickTree', () => {
77
84
  });
78
85
  });
79
86
 
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
- });
87
+ describe('search & bounds', () => {
88
+ it('finds lower bound correctly', () => {
89
+ const tree = new FenwickTree(5);
90
+ tree.update(0, 10); // sum up to 1: 10
91
+ tree.update(1, 10); // sum up to 2: 20
92
+ tree.update(2, 10); // sum up to 3: 30
88
93
 
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);
94
+ expect(tree.findLowerBound(5)).toBe(0);
95
+ expect(tree.findLowerBound(15)).toBe(1);
96
+ expect(tree.findLowerBound(25)).toBe(2);
97
+ expect(tree.findLowerBound(35)).toBe(5); // Returns size when not found
98
98
  });
99
99
  });
100
100
 
101
- describe('shift', () => {
102
- it('should do nothing when offset is 0', () => {
101
+ describe('shift operations', () => {
102
+ it('does nothing when offset is 0', () => {
103
103
  const tree = new FenwickTree(3);
104
104
  tree.update(0, 10);
105
105
  tree.shift(0);
106
106
  expect(tree.get(0)).toBe(10);
107
107
  });
108
108
 
109
- it('should shift values forward when offset is positive', () => {
109
+ it('shifts values forward when offset is positive', () => {
110
110
  const tree = new FenwickTree(5);
111
111
  tree.update(0, 10);
112
112
  tree.update(1, 20);
@@ -119,7 +119,7 @@ describe('fenwickTree', () => {
119
119
  expect(tree.query(4)).toBe(30);
120
120
  });
121
121
 
122
- it('should shift values backward when offset is negative', () => {
122
+ it('shifts values backward when offset is negative', () => {
123
123
  const tree = new FenwickTree(5);
124
124
  tree.update(2, 10);
125
125
  tree.update(3, 20);
@@ -1,20 +1,28 @@
1
1
  /**
2
2
  * Fenwick Tree (Binary Indexed Tree) implementation for efficient
3
3
  * prefix sum calculations and updates.
4
+ *
5
+ * Provides O(log n) time complexity for both point updates and prefix sum queries.
4
6
  */
5
7
  export class FenwickTree {
6
8
  private tree: Float64Array;
7
9
  private values: Float64Array;
8
10
 
11
+ /**
12
+ * Creates a new Fenwick Tree with the specified size.
13
+ *
14
+ * @param size - The number of elements in the tree.
15
+ */
9
16
  constructor(size: number) {
10
17
  this.tree = new Float64Array(size + 1);
11
18
  this.values = new Float64Array(size);
12
19
  }
13
20
 
14
21
  /**
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)
22
+ * Update the value at a specific index and propagate changes throughout the tree.
23
+ *
24
+ * @param index - The 0-based index to update.
25
+ * @param delta - The change in value (new value - old value).
18
26
  */
19
27
  update(index: number, delta: number): void {
20
28
  if (index < 0 || index >= this.values.length) {
@@ -31,8 +39,9 @@ export class FenwickTree {
31
39
 
32
40
  /**
33
41
  * 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)
42
+ *
43
+ * @param index - 0-based index. `query(n)` returns sum of values from index 0 to n-1.
44
+ * @returns Sum of values in range [0, index).
36
45
  */
37
46
  query(index: number): number {
38
47
  let sum = 0;
@@ -44,8 +53,11 @@ export class FenwickTree {
44
53
  }
45
54
 
46
55
  /**
47
- * Set the individual value at an index without updating the tree.
48
- * Call rebuild() after multiple sets to update the tree efficiently.
56
+ * Set the individual value at an index without updating the prefix sum tree.
57
+ * Call `rebuild()` after multiple sets to update the tree efficiently in O(n).
58
+ *
59
+ * @param index - The 0-based index.
60
+ * @param value - The new value.
49
61
  */
50
62
  set(index: number, value: number): void {
51
63
  if (index < 0 || index >= this.values.length) {
@@ -62,14 +74,19 @@ export class FenwickTree {
62
74
  }
63
75
 
64
76
  /**
65
- * Get the individual value at an index.
77
+ * Get the individual value at a specific index.
78
+ *
79
+ * @param index - The 0-based index.
80
+ * @returns The value at the specified index.
66
81
  */
67
82
  get(index: number): number {
68
83
  return this.values[ index ] || 0;
69
84
  }
70
85
 
71
86
  /**
72
- * Get the underlying values array.
87
+ * Get the underlying values array as a read-only Float64Array.
88
+ *
89
+ * @returns The read-only values array.
73
90
  */
74
91
  getValues(): Readonly<Float64Array> {
75
92
  return this.values;
@@ -77,9 +94,10 @@ export class FenwickTree {
77
94
 
78
95
  /**
79
96
  * 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
97
+ * Highly efficient search used to find which item is at a specific scroll offset.
98
+ *
99
+ * @param value - The prefix sum value to search for.
100
+ * @returns The 0-based index.
83
101
  */
84
102
  findLowerBound(value: number): number {
85
103
  let index = 0;
@@ -101,8 +119,8 @@ export class FenwickTree {
101
119
  }
102
120
 
103
121
  /**
104
- * Rebuild the entire tree from the current values array in O(N).
105
- * Useful after bulk updates to the values array.
122
+ * Rebuild the entire prefix sum tree from the current values array.
123
+ * Time complexity: O(n).
106
124
  */
107
125
  rebuild(): void {
108
126
  this.tree.fill(0);
@@ -118,8 +136,9 @@ export class FenwickTree {
118
136
  }
119
137
 
120
138
  /**
121
- * Resize the tree while preserving existing values.
122
- * @param size New size of the tree
139
+ * Resize the tree while preserving existing values and rebuilding the prefix sums.
140
+ *
141
+ * @param size - The new size of the tree.
123
142
  */
124
143
  resize(size: number): void {
125
144
  if (size === this.values.length) {
@@ -135,8 +154,9 @@ export class FenwickTree {
135
154
 
136
155
  /**
137
156
  * 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)
157
+ * Useful when items are prepended to the list to maintain existing measurements.
158
+ *
159
+ * @param offset - Number of positions to shift. Positive for prepending (shifts right).
140
160
  */
141
161
  shift(offset: number): void {
142
162
  if (offset === 0) {
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { getPaddingX, getPaddingY, isBody, isElement, isScrollableElement, isScrollToIndexOptions, isWindow, isWindowLike } from './scroll';
4
+
5
+ describe('scroll utils', () => {
6
+ describe('element type guards', () => {
7
+ describe('iswindow', () => {
8
+ it('returns true for null', () => {
9
+ expect(isWindow(null)).toBe(true);
10
+ });
11
+
12
+ it('returns true for window object', () => {
13
+ expect(isWindow(window)).toBe(true);
14
+ });
15
+
16
+ it('returns true for document.documentelement object', () => {
17
+ expect(isWindow(document.documentElement)).toBe(true);
18
+ });
19
+
20
+ it('returns false for an element', () => {
21
+ const el = document.createElement('div');
22
+ expect(isWindow(el)).toBe(false);
23
+ });
24
+
25
+ it('returns false for undefined', () => {
26
+ expect(isWindow(undefined)).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('isbody', () => {
31
+ it('returns true for document.body', () => {
32
+ expect(isBody(document.body)).toBe(true);
33
+ });
34
+
35
+ it('returns false for null', () => {
36
+ expect(isBody(null)).toBe(false);
37
+ });
38
+
39
+ it('returns false for undefined', () => {
40
+ expect(isBody(undefined)).toBe(false);
41
+ });
42
+
43
+ it('returns false for a string', () => {
44
+ // @ts-expect-error testing invalid input
45
+ expect(isBody('not an object')).toBe(false);
46
+ });
47
+
48
+ it('returns false for a plain object', () => {
49
+ // @ts-expect-error testing invalid input
50
+ expect(isBody({})).toBe(false);
51
+ });
52
+
53
+ it('returns false for a div', () => {
54
+ const el = document.createElement('div');
55
+ expect(isBody(el)).toBe(false);
56
+ });
57
+
58
+ it('returns false for window', () => {
59
+ expect(isBody(window)).toBe(false);
60
+ });
61
+
62
+ it('returns false for document.documentelement', () => {
63
+ expect(isBody(document.documentElement)).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('iswindowlike', () => {
68
+ it('returns true for window', () => {
69
+ expect(isWindowLike(window)).toBe(true);
70
+ });
71
+
72
+ it('returns true for document.documentelement', () => {
73
+ expect(isWindowLike(document.documentElement)).toBe(true);
74
+ });
75
+
76
+ it('returns true for body', () => {
77
+ expect(isWindowLike(document.body)).toBe(true);
78
+ });
79
+
80
+ it('returns true for null', () => {
81
+ expect(isWindowLike(null)).toBe(true);
82
+ });
83
+
84
+ it('returns false for a div', () => {
85
+ const el = document.createElement('div');
86
+ expect(isWindowLike(el)).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('iselement', () => {
91
+ it('returns true for a div', () => {
92
+ const el = document.createElement('div');
93
+ expect(isElement(el)).toBe(true);
94
+ });
95
+
96
+ it('returns true for document.documentelement', () => {
97
+ expect(isElement(document.documentElement)).toBe(true);
98
+ });
99
+
100
+ it('returns false for window', () => {
101
+ expect(isElement(window)).toBe(false);
102
+ });
103
+
104
+ it('returns false for null', () => {
105
+ expect(isElement(null)).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe('isscrollableelement', () => {
110
+ it('returns true for a div', () => {
111
+ const el = document.createElement('div');
112
+ expect(isScrollableElement(el)).toBe(true);
113
+ });
114
+
115
+ it('returns false for null', () => {
116
+ expect(isScrollableElement(null)).toBe(false);
117
+ });
118
+ });
119
+ });
120
+
121
+ describe('options type guards', () => {
122
+ describe('isscrolltoindexoptions', () => {
123
+ it('returns true for valid options', () => {
124
+ expect(isScrollToIndexOptions({ align: 'start' })).toBe(true);
125
+ expect(isScrollToIndexOptions({ behavior: 'smooth' })).toBe(true);
126
+ expect(isScrollToIndexOptions({ isCorrection: true })).toBe(true);
127
+ });
128
+
129
+ it('returns false for other values', () => {
130
+ expect(isScrollToIndexOptions(null)).toBe(false);
131
+ expect(isScrollToIndexOptions('start')).toBe(false);
132
+ expect(isScrollToIndexOptions({})).toBe(false);
133
+ });
134
+ });
135
+ });
136
+
137
+ describe('padding utilities', () => {
138
+ describe('getpaddingx', () => {
139
+ it('handles numeric padding', () => {
140
+ expect(getPaddingX(10, 'horizontal')).toBe(10);
141
+ expect(getPaddingX(10, 'both')).toBe(10);
142
+ expect(getPaddingX(10, 'vertical')).toBe(0);
143
+ expect(getPaddingX(0, 'horizontal')).toBe(0);
144
+ });
145
+
146
+ it('handles object padding', () => {
147
+ expect(getPaddingX({ x: 15 }, 'vertical')).toBe(15);
148
+ expect(getPaddingX({ y: 20 }, 'horizontal')).toBe(0);
149
+ });
150
+
151
+ it('returns 0 for undefined', () => {
152
+ expect(getPaddingX(undefined)).toBe(0);
153
+ });
154
+ });
155
+
156
+ describe('getpaddingy', () => {
157
+ it('handles numeric padding', () => {
158
+ expect(getPaddingY(10, 'vertical')).toBe(10);
159
+ expect(getPaddingY(10, 'both')).toBe(10);
160
+ expect(getPaddingY(10, 'horizontal')).toBe(0);
161
+ expect(getPaddingY(0, 'vertical')).toBe(0);
162
+ });
163
+
164
+ it('handles object padding', () => {
165
+ expect(getPaddingY({ y: 15 }, 'horizontal')).toBe(15);
166
+ expect(getPaddingY({ x: 20 }, 'vertical')).toBe(0);
167
+ });
168
+
169
+ it('returns 0 for undefined', () => {
170
+ expect(getPaddingY(undefined)).toBe(0);
171
+ });
172
+ });
173
+ });
174
+ });
@@ -1,37 +1,74 @@
1
- import type { ScrollDirection, ScrollToIndexOptions } from '../composables/useVirtualScroll';
1
+ import type { ScrollDirection, ScrollToIndexOptions } from '../types';
2
2
 
3
3
  /**
4
- * Checks if the container has a bounding client rect method.
4
+ * Maximum size (in pixels) for an element that most browsers can handle reliably.
5
+ * Beyond this size, we use scaling for the scrollable area.
6
+ * @default 10000000
7
+ */
8
+ export const BROWSER_MAX_SIZE = 10000000;
9
+
10
+ /**
11
+ * Checks if the container is the window object.
12
+ *
13
+ * @param container - The container element or window to check.
14
+ * @returns `true` if the container is the global window object.
15
+ */
16
+ export function isWindow(container: HTMLElement | Window | null | undefined): container is Window {
17
+ return container === null || container === document.documentElement || (typeof window !== 'undefined' && container === window);
18
+ }
19
+
20
+ /**
21
+ * Checks if the container is the document body element.
22
+ *
23
+ * @param container - The container element or window to check.
24
+ * @returns `true` if the container is the `<body>` element.
25
+ */
26
+ export function isBody(container: HTMLElement | Window | null | undefined): container is HTMLElement {
27
+ return container != null && typeof container === 'object' && 'tagName' in container && container.tagName === 'BODY';
28
+ }
29
+
30
+ /**
31
+ * Checks if the container is window-like (global window or document body).
5
32
  *
6
33
  * @param container - The container element or window to check.
7
- * @returns True if the container is an HTMLElement with getBoundingClientRect.
34
+ * @returns `true` if the container is window or body.
35
+ */
36
+ export function isWindowLike(container: HTMLElement | Window | null | undefined): boolean {
37
+ return isWindow(container) || isBody(container);
38
+ }
39
+
40
+ /**
41
+ * Checks if the container is a valid HTML Element with bounding rect support.
42
+ *
43
+ * @param container - The container to check.
44
+ * @returns `true` if the container is an `HTMLElement`.
8
45
  */
9
46
  export function isElement(container: HTMLElement | Window | null | undefined): container is HTMLElement {
10
- return !!container && 'getBoundingClientRect' in container;
47
+ return container != null && 'getBoundingClientRect' in container;
11
48
  }
12
49
 
13
50
  /**
14
- * Checks if the target is an element with scroll properties.
51
+ * Checks if the target is an element that supports scrolling.
15
52
  *
16
53
  * @param target - The event target to check.
17
- * @returns True if the target is an HTMLElement with scroll properties.
54
+ * @returns `true` if the target is an `HTMLElement` with scroll properties.
18
55
  */
19
56
  export function isScrollableElement(target: EventTarget | null): target is HTMLElement {
20
- return !!target && 'scrollLeft' in target;
57
+ return target != null && 'scrollLeft' in target;
21
58
  }
22
59
 
23
60
  /**
24
- * Helper to determine if an options argument is the full ScrollToIndexOptions object.
61
+ * Helper to determine if an options argument is a full `ScrollToIndexOptions` object.
25
62
  *
26
63
  * @param options - The options object to check.
27
- * @returns True if the options object contains scroll-to-index specific properties.
64
+ * @returns `true` if the options object contains scroll-to-index specific properties.
28
65
  */
29
66
  export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
30
- return typeof options === 'object' && options !== null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
67
+ return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
31
68
  }
32
69
 
33
70
  /**
34
- * Extracts the horizontal padding from a padding value or object.
71
+ * Extracts the horizontal padding from a padding configuration.
35
72
  *
36
73
  * @param p - The padding value (number or object with x/y).
37
74
  * @param direction - The current scroll direction.
@@ -41,11 +78,11 @@ export function getPaddingX(p: number | { x?: number; y?: number; } | undefined,
41
78
  if (typeof p === 'object' && p !== null) {
42
79
  return p.x || 0;
43
80
  }
44
- return direction === 'horizontal' ? (p || 0) : 0;
81
+ return (direction === 'horizontal' || direction === 'both') ? (p || 0) : 0;
45
82
  }
46
83
 
47
84
  /**
48
- * Extracts the vertical padding from a padding value or object.
85
+ * Extracts the vertical padding from a padding configuration.
49
86
  *
50
87
  * @param p - The padding value (number or object with x/y).
51
88
  * @param direction - The current scroll direction.