@pdanpdan/virtual-scroll 0.4.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.
- package/README.md +246 -297
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +873 -257
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2209 -1109
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -2
- package/package.json +5 -1
- package/src/components/VirtualScroll.test.ts +1886 -326
- package/src/components/VirtualScroll.vue +813 -340
- package/src/components/VirtualScrollbar.test.ts +174 -0
- package/src/components/VirtualScrollbar.vue +102 -0
- package/src/composables/useVirtualScroll.test.ts +1506 -228
- package/src/composables/useVirtualScroll.ts +789 -373
- package/src/composables/useVirtualScrollbar.test.ts +526 -0
- package/src/composables/useVirtualScrollbar.ts +239 -0
- package/src/index.ts +2 -0
- package/src/types.ts +333 -52
- package/src/utils/fenwick-tree.test.ts +39 -39
- package/src/utils/scroll.test.ts +133 -107
- package/src/utils/scroll.ts +12 -5
- package/src/utils/virtual-scroll-logic.test.ts +653 -320
- package/src/utils/virtual-scroll-logic.ts +685 -389
|
@@ -4,15 +4,15 @@ import { FenwickTree } from './fenwick-tree';
|
|
|
4
4
|
|
|
5
5
|
describe('fenwickTree', () => {
|
|
6
6
|
describe('initialization', () => {
|
|
7
|
-
it('
|
|
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('
|
|
15
|
-
it('
|
|
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('
|
|
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('
|
|
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('
|
|
44
|
-
it('
|
|
45
|
-
const tree = new FenwickTree(
|
|
46
|
-
tree.update(0, 10);
|
|
47
|
-
tree.
|
|
48
|
-
tree.
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
-
it('
|
|
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('
|
|
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('
|
|
81
|
-
it('
|
|
82
|
-
const tree = new FenwickTree(
|
|
83
|
-
tree.update(0, 10);
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
tree.
|
|
92
|
-
tree.
|
|
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('
|
|
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('
|
|
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('
|
|
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);
|
package/src/utils/scroll.test.ts
CHANGED
|
@@ -3,146 +3,172 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import { getPaddingX, getPaddingY, isBody, isElement, isScrollableElement, isScrollToIndexOptions, isWindow, isWindowLike } from './scroll';
|
|
4
4
|
|
|
5
5
|
describe('scroll utils', () => {
|
|
6
|
-
describe('
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
describe('element type guards', () => {
|
|
7
|
+
describe('iswindow', () => {
|
|
8
|
+
it('returns true for null', () => {
|
|
9
|
+
expect(isWindow(null)).toBe(true);
|
|
10
|
+
});
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
it('returns true for window object', () => {
|
|
13
|
+
expect(isWindow(window)).toBe(true);
|
|
14
|
+
});
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
});
|
|
16
|
+
it('returns true for document.documentelement object', () => {
|
|
17
|
+
expect(isWindow(document.documentElement)).toBe(true);
|
|
18
|
+
});
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
it('returns false for an element', () => {
|
|
21
|
+
const el = document.createElement('div');
|
|
22
|
+
expect(isWindow(el)).toBe(false);
|
|
23
|
+
});
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
it('returns false for undefined', () => {
|
|
26
|
+
expect(isWindow(undefined)).toBe(false);
|
|
27
|
+
});
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
describe('isbody', () => {
|
|
31
|
+
it('returns true for document.body', () => {
|
|
32
|
+
expect(isBody(document.body)).toBe(true);
|
|
33
|
+
});
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
35
|
+
it('returns false for null', () => {
|
|
36
|
+
expect(isBody(null)).toBe(false);
|
|
37
|
+
});
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
39
|
+
it('returns false for undefined', () => {
|
|
40
|
+
expect(isBody(undefined)).toBe(false);
|
|
41
|
+
});
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
it('returns false for a string', () => {
|
|
44
|
+
// @ts-expect-error testing invalid input
|
|
45
|
+
expect(isBody('not an object')).toBe(false);
|
|
46
|
+
});
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
it('returns false for a plain object', () => {
|
|
49
|
+
// @ts-expect-error testing invalid input
|
|
50
|
+
expect(isBody({})).toBe(false);
|
|
51
|
+
});
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
it('returns false for a div', () => {
|
|
54
|
+
const el = document.createElement('div');
|
|
55
|
+
expect(isBody(el)).toBe(false);
|
|
56
|
+
});
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
it('returns false for window', () => {
|
|
59
|
+
expect(isBody(window)).toBe(false);
|
|
60
|
+
});
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
it('returns false for document.documentelement', () => {
|
|
63
|
+
expect(isBody(document.documentElement)).toBe(false);
|
|
64
|
+
});
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
67
|
+
describe('iswindowlike', () => {
|
|
68
|
+
it('returns true for window', () => {
|
|
69
|
+
expect(isWindowLike(window)).toBe(true);
|
|
70
|
+
});
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(isElement(el)).toBe(true);
|
|
77
|
-
});
|
|
72
|
+
it('returns true for document.documentelement', () => {
|
|
73
|
+
expect(isWindowLike(document.documentElement)).toBe(true);
|
|
74
|
+
});
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
it('returns true for body', () => {
|
|
77
|
+
expect(isWindowLike(document.body)).toBe(true);
|
|
78
|
+
});
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
80
|
+
it('returns true for null', () => {
|
|
81
|
+
expect(isWindowLike(null)).toBe(true);
|
|
82
|
+
});
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
it('returns false for a div', () => {
|
|
85
|
+
const el = document.createElement('div');
|
|
86
|
+
expect(isWindowLike(el)).toBe(false);
|
|
87
|
+
});
|
|
92
88
|
});
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
describe('iselement', () => {
|
|
91
|
+
it('returns true for a div', () => {
|
|
92
|
+
const el = document.createElement('div');
|
|
93
|
+
expect(isElement(el)).toBe(true);
|
|
94
|
+
});
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(isScrollToIndexOptions({ behavior: 'smooth' })).toBe(true);
|
|
103
|
-
expect(isScrollToIndexOptions({ isCorrection: true })).toBe(true);
|
|
104
|
-
});
|
|
96
|
+
it('returns true for document.documentelement', () => {
|
|
97
|
+
expect(isElement(document.documentElement)).toBe(true);
|
|
98
|
+
});
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(isScrollToIndexOptions({})).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
100
|
+
it('returns false for window', () => {
|
|
101
|
+
expect(isElement(window)).toBe(false);
|
|
102
|
+
});
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
expect(getPaddingX(10, 'both')).toBe(10);
|
|
117
|
-
expect(getPaddingX(10, 'vertical')).toBe(0);
|
|
118
|
-
expect(getPaddingX(0, 'horizontal')).toBe(0);
|
|
104
|
+
it('returns false for null', () => {
|
|
105
|
+
expect(isElement(null)).toBe(false);
|
|
106
|
+
});
|
|
119
107
|
});
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
describe('isscrollableelement', () => {
|
|
110
|
+
it('returns true for a div', () => {
|
|
111
|
+
const el = document.createElement('div');
|
|
112
|
+
expect(isScrollableElement(el)).toBe(true);
|
|
113
|
+
});
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
it('returns false for null', () => {
|
|
116
|
+
expect(isScrollableElement(null)).toBe(false);
|
|
117
|
+
});
|
|
128
118
|
});
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
describe('
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
});
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
});
|
|
142
134
|
});
|
|
135
|
+
});
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
});
|
|
146
172
|
});
|
|
147
173
|
});
|
|
148
174
|
});
|
package/src/utils/scroll.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ScrollDirection, ScrollToIndexOptions } from '../types';
|
|
2
2
|
|
|
3
|
+
/**
|
|
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
|
+
|
|
3
10
|
/**
|
|
4
11
|
* Checks if the container is the window object.
|
|
5
12
|
*
|
|
@@ -7,7 +14,7 @@ import type { ScrollDirection, ScrollToIndexOptions } from '../types';
|
|
|
7
14
|
* @returns `true` if the container is the global window object.
|
|
8
15
|
*/
|
|
9
16
|
export function isWindow(container: HTMLElement | Window | null | undefined): container is Window {
|
|
10
|
-
return container === null || (typeof window !== 'undefined' && container === window);
|
|
17
|
+
return container === null || container === document.documentElement || (typeof window !== 'undefined' && container === window);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
/**
|
|
@@ -17,7 +24,7 @@ export function isWindow(container: HTMLElement | Window | null | undefined): co
|
|
|
17
24
|
* @returns `true` if the container is the `<body>` element.
|
|
18
25
|
*/
|
|
19
26
|
export function isBody(container: HTMLElement | Window | null | undefined): container is HTMLElement {
|
|
20
|
-
return
|
|
27
|
+
return container != null && typeof container === 'object' && 'tagName' in container && container.tagName === 'BODY';
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -37,7 +44,7 @@ export function isWindowLike(container: HTMLElement | Window | null | undefined)
|
|
|
37
44
|
* @returns `true` if the container is an `HTMLElement`.
|
|
38
45
|
*/
|
|
39
46
|
export function isElement(container: HTMLElement | Window | null | undefined): container is HTMLElement {
|
|
40
|
-
return
|
|
47
|
+
return container != null && 'getBoundingClientRect' in container;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/**
|
|
@@ -47,7 +54,7 @@ export function isElement(container: HTMLElement | Window | null | undefined): c
|
|
|
47
54
|
* @returns `true` if the target is an `HTMLElement` with scroll properties.
|
|
48
55
|
*/
|
|
49
56
|
export function isScrollableElement(target: EventTarget | null): target is HTMLElement {
|
|
50
|
-
return
|
|
57
|
+
return target != null && 'scrollLeft' in target;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
@@ -57,7 +64,7 @@ export function isScrollableElement(target: EventTarget | null): target is HTMLE
|
|
|
57
64
|
* @returns `true` if the options object contains scroll-to-index specific properties.
|
|
58
65
|
*/
|
|
59
66
|
export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
|
|
60
|
-
return typeof options === 'object' && options
|
|
67
|
+
return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|