@reckona/mreact-virtual 0.0.82
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/src/index.ts +465 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tatsuo Kaniwa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @reckona/mreact-virtual
|
|
2
|
+
|
|
3
|
+
`@reckona/mreact-virtual` provides reactive list and grid virtualization primitives for mreact applications. It keeps large timelines and media grids centered around stable keys while exposing bounded render entries, spacer heights, visible ranges, and scroll offsets.
|
|
4
|
+
|
|
5
|
+
## Fixed List
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createVirtualList } from "@reckona/mreact-virtual";
|
|
9
|
+
|
|
10
|
+
const virtual = createVirtualList({
|
|
11
|
+
items: () => messages.get(),
|
|
12
|
+
getKey: (message) => message.id,
|
|
13
|
+
estimateItemSize: () => 48,
|
|
14
|
+
scrollOffset: () => scrollTop.get(),
|
|
15
|
+
viewportSize: () => viewportHeight.get(),
|
|
16
|
+
overscan: 2,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
virtual.entries.get();
|
|
20
|
+
virtual.topSpacerPx.get();
|
|
21
|
+
virtual.bottomSpacerPx.get();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Render `topSpacerPx`, the keyed `entries`, and `bottomSpacerPx` in order. The entries include overscan rows while `visibleRange` reports only the viewport rows.
|
|
25
|
+
|
|
26
|
+
## Responsive Media Grid
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createVirtualGrid } from "@reckona/mreact-virtual";
|
|
30
|
+
|
|
31
|
+
const virtual = createVirtualGrid({
|
|
32
|
+
items: () => media.get(),
|
|
33
|
+
getKey: (item) => item.id,
|
|
34
|
+
estimateItemSize: () => 220,
|
|
35
|
+
getColumnCount: () => columnCount.get(),
|
|
36
|
+
scrollOffset: () => scrollTop.get(),
|
|
37
|
+
viewportSize: () => viewportHeight.get(),
|
|
38
|
+
overscan: 2,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`getColumnCount()` is read when `refresh()` runs, so apps can recompute the virtual range after container or breakpoint changes. `visibleRange` can drive thumbnail prefetching for visible and near-visible media.
|
|
43
|
+
|
|
44
|
+
## Infinite Append And Scroll Restoration
|
|
45
|
+
|
|
46
|
+
Keep fetched pages in application or query state, append new pages there, and call `refresh()` after the item list changes. The virtualizer keeps stable keys for the rendered entries and exposes `scrollToIndex()` and `scrollToKey()` for restoration.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const nextOffset = virtual.scrollToKey(selectedId);
|
|
50
|
+
if (nextOffset !== undefined) {
|
|
51
|
+
scroller.scrollTop = nextOffset;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Measured Items
|
|
56
|
+
|
|
57
|
+
Use `measureItem()` when item height becomes known after images or metadata load. Measuring an item refreshes the range immediately.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
virtual.measureItem(photo.id, element.offsetHeight);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For grids, a measured row uses the largest measured item in that row. Unmeasured rows use `estimateItemSize()`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ReadonlyCell } from "@reckona/mreact-reactive-core";
|
|
2
|
+
export type VirtualKey = string | number;
|
|
3
|
+
export interface VirtualRangeOptions {
|
|
4
|
+
columnCount?: number;
|
|
5
|
+
itemCount: number;
|
|
6
|
+
itemSize: number;
|
|
7
|
+
overscan?: number;
|
|
8
|
+
scrollOffset: number;
|
|
9
|
+
viewportSize: number;
|
|
10
|
+
}
|
|
11
|
+
export interface VirtualRange {
|
|
12
|
+
bottomSpacerPx: number;
|
|
13
|
+
columnCount: number;
|
|
14
|
+
endIndex: number;
|
|
15
|
+
endRow: number;
|
|
16
|
+
itemCount: number;
|
|
17
|
+
rowCount: number;
|
|
18
|
+
startIndex: number;
|
|
19
|
+
startRow: number;
|
|
20
|
+
topSpacerPx: number;
|
|
21
|
+
totalSizePx: number;
|
|
22
|
+
visibleEndIndex: number;
|
|
23
|
+
visibleEndRow: number;
|
|
24
|
+
visibleStartIndex: number;
|
|
25
|
+
visibleStartRow: number;
|
|
26
|
+
}
|
|
27
|
+
export interface VisibleRange {
|
|
28
|
+
endIndex: number;
|
|
29
|
+
endRow: number;
|
|
30
|
+
startIndex: number;
|
|
31
|
+
startRow: number;
|
|
32
|
+
}
|
|
33
|
+
export interface VirtualEntry<TItem> {
|
|
34
|
+
index: number;
|
|
35
|
+
item: TItem;
|
|
36
|
+
key: VirtualKey;
|
|
37
|
+
row: number;
|
|
38
|
+
}
|
|
39
|
+
export interface VirtualListOptions<TItem> {
|
|
40
|
+
estimateItemSize: (index: number, item: TItem | undefined) => number;
|
|
41
|
+
getKey: (item: TItem, index: number) => VirtualKey;
|
|
42
|
+
items: () => readonly TItem[];
|
|
43
|
+
overscan?: number;
|
|
44
|
+
scrollOffset: () => number;
|
|
45
|
+
viewportSize: () => number;
|
|
46
|
+
}
|
|
47
|
+
export interface VirtualGridOptions<TItem> extends VirtualListOptions<TItem> {
|
|
48
|
+
getColumnCount: () => number;
|
|
49
|
+
}
|
|
50
|
+
export interface Virtualizer<TItem> {
|
|
51
|
+
readonly bottomSpacerPx: ReadonlyCell<number>;
|
|
52
|
+
readonly entries: ReadonlyCell<readonly VirtualEntry<TItem>[]>;
|
|
53
|
+
readonly range: ReadonlyCell<VirtualRange>;
|
|
54
|
+
readonly topSpacerPx: ReadonlyCell<number>;
|
|
55
|
+
readonly totalSizePx: ReadonlyCell<number>;
|
|
56
|
+
readonly visibleRange: ReadonlyCell<VisibleRange>;
|
|
57
|
+
measureItem(key: VirtualKey, sizePx: number): void;
|
|
58
|
+
refresh(): void;
|
|
59
|
+
scrollToIndex(index: number): number;
|
|
60
|
+
scrollToKey(key: VirtualKey): number | undefined;
|
|
61
|
+
}
|
|
62
|
+
export declare function calculateVirtualRange(options: VirtualRangeOptions): VirtualRange;
|
|
63
|
+
export declare function createVirtualList<TItem>(options: VirtualListOptions<TItem>): Virtualizer<TItem>;
|
|
64
|
+
export declare function createVirtualGrid<TItem>(options: VirtualGridOptions<TItem>): Virtualizer<TItem>;
|
|
65
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { cell } from "@reckona/mreact-reactive-core";
|
|
2
|
+
export function calculateVirtualRange(options) {
|
|
3
|
+
const itemCount = clampInteger(options.itemCount, 0);
|
|
4
|
+
const columnCount = clampInteger(options.columnCount ?? 1, 1);
|
|
5
|
+
const itemSize = clampPositiveSize(options.itemSize);
|
|
6
|
+
const overscan = clampInteger(options.overscan ?? 0, 0);
|
|
7
|
+
const scrollOffset = clampSize(options.scrollOffset);
|
|
8
|
+
const viewportSize = clampSize(options.viewportSize);
|
|
9
|
+
const rowCount = Math.ceil(itemCount / columnCount);
|
|
10
|
+
const totalSizePx = rowCount * itemSize;
|
|
11
|
+
if (itemCount === 0 || rowCount === 0) {
|
|
12
|
+
return emptyRange(columnCount);
|
|
13
|
+
}
|
|
14
|
+
const visibleStartRow = Math.min(rowCount, Math.floor(scrollOffset / itemSize));
|
|
15
|
+
const visibleEndRow = Math.min(rowCount, Math.ceil((scrollOffset + viewportSize) / itemSize));
|
|
16
|
+
const startRow = Math.max(0, visibleStartRow - overscan);
|
|
17
|
+
const endRow = Math.min(rowCount, visibleEndRow + overscan);
|
|
18
|
+
const startIndex = Math.min(itemCount, startRow * columnCount);
|
|
19
|
+
const endIndex = Math.min(itemCount, endRow * columnCount);
|
|
20
|
+
const visibleStartIndex = Math.min(itemCount, visibleStartRow * columnCount);
|
|
21
|
+
const visibleEndIndex = Math.min(itemCount, visibleEndRow * columnCount);
|
|
22
|
+
return {
|
|
23
|
+
bottomSpacerPx: Math.max(0, (rowCount - endRow) * itemSize),
|
|
24
|
+
columnCount,
|
|
25
|
+
endIndex,
|
|
26
|
+
endRow,
|
|
27
|
+
itemCount,
|
|
28
|
+
rowCount,
|
|
29
|
+
startIndex,
|
|
30
|
+
startRow,
|
|
31
|
+
topSpacerPx: startRow * itemSize,
|
|
32
|
+
totalSizePx,
|
|
33
|
+
visibleEndIndex,
|
|
34
|
+
visibleEndRow,
|
|
35
|
+
visibleStartIndex,
|
|
36
|
+
visibleStartRow,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function createVirtualList(options) {
|
|
40
|
+
return createVirtualizer({
|
|
41
|
+
...options,
|
|
42
|
+
getColumnCount: () => 1,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function createVirtualGrid(options) {
|
|
46
|
+
return createVirtualizer(options);
|
|
47
|
+
}
|
|
48
|
+
function createVirtualizer(options) {
|
|
49
|
+
const measuredSizes = new Map();
|
|
50
|
+
let snapshot = createSnapshot(options, measuredSizes);
|
|
51
|
+
const range = cell(snapshot.range);
|
|
52
|
+
const visibleRange = cell(snapshot.visibleRange);
|
|
53
|
+
const entries = cell(snapshot.entries);
|
|
54
|
+
const topSpacerPx = cell(snapshot.range.topSpacerPx);
|
|
55
|
+
const bottomSpacerPx = cell(snapshot.range.bottomSpacerPx);
|
|
56
|
+
const totalSizePx = cell(snapshot.range.totalSizePx);
|
|
57
|
+
const refresh = () => {
|
|
58
|
+
snapshot = createSnapshot(options, measuredSizes);
|
|
59
|
+
range.set(snapshot.range);
|
|
60
|
+
visibleRange.set(snapshot.visibleRange);
|
|
61
|
+
entries.set(snapshot.entries);
|
|
62
|
+
topSpacerPx.set(snapshot.range.topSpacerPx);
|
|
63
|
+
bottomSpacerPx.set(snapshot.range.bottomSpacerPx);
|
|
64
|
+
totalSizePx.set(snapshot.range.totalSizePx);
|
|
65
|
+
};
|
|
66
|
+
const scrollToIndex = (index) => {
|
|
67
|
+
const itemCount = options.items().length;
|
|
68
|
+
if (itemCount === 0) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
const columnCount = clampInteger(options.getColumnCount(), 1);
|
|
72
|
+
const row = Math.floor(clampInteger(index, 0, itemCount - 1) / columnCount);
|
|
73
|
+
return snapshot.offsetForRow(row);
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
bottomSpacerPx,
|
|
77
|
+
entries,
|
|
78
|
+
range,
|
|
79
|
+
topSpacerPx,
|
|
80
|
+
totalSizePx,
|
|
81
|
+
visibleRange,
|
|
82
|
+
measureItem(key, sizePx) {
|
|
83
|
+
measuredSizes.set(key, clampPositiveSize(sizePx));
|
|
84
|
+
refresh();
|
|
85
|
+
},
|
|
86
|
+
refresh,
|
|
87
|
+
scrollToIndex,
|
|
88
|
+
scrollToKey(key) {
|
|
89
|
+
const index = options
|
|
90
|
+
.items()
|
|
91
|
+
.findIndex((item, itemIndex) => options.getKey(item, itemIndex) === key);
|
|
92
|
+
if (index === -1) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return scrollToIndex(index);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createSnapshot(options, measuredSizes) {
|
|
100
|
+
const items = options.items();
|
|
101
|
+
const itemCount = items.length;
|
|
102
|
+
const columnCount = clampInteger(options.getColumnCount(), 1);
|
|
103
|
+
const overscan = clampInteger(options.overscan ?? 0, 0);
|
|
104
|
+
const rowCount = Math.ceil(itemCount / columnCount);
|
|
105
|
+
if (measuredSizes.size === 0) {
|
|
106
|
+
return createFixedSnapshot(options, items, itemCount, columnCount, rowCount, overscan);
|
|
107
|
+
}
|
|
108
|
+
const rowSizes = computeRowSizes(items, columnCount, options, measuredSizes);
|
|
109
|
+
const rowOffsets = computeRowOffsets(rowSizes);
|
|
110
|
+
const totalSizePx = rowSizes.reduce((total, size) => total + size, 0);
|
|
111
|
+
if (itemCount === 0 || rowCount === 0) {
|
|
112
|
+
const range = emptyRange(columnCount);
|
|
113
|
+
return {
|
|
114
|
+
entries: [],
|
|
115
|
+
offsetForRow: () => 0,
|
|
116
|
+
range,
|
|
117
|
+
rowOffsets,
|
|
118
|
+
rowSizes,
|
|
119
|
+
visibleRange: rangeToVisibleRange(range),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const scrollOffset = clampSize(options.scrollOffset());
|
|
123
|
+
const viewportEnd = scrollOffset + clampSize(options.viewportSize());
|
|
124
|
+
const visibleStartRow = findVisibleStartRow(rowOffsets, rowSizes, scrollOffset);
|
|
125
|
+
const visibleEndRow = findVisibleEndRow(rowOffsets, rowSizes, viewportEnd);
|
|
126
|
+
const startRow = Math.max(0, visibleStartRow - overscan);
|
|
127
|
+
const endRow = Math.min(rowCount, visibleEndRow + overscan);
|
|
128
|
+
const range = createRange({
|
|
129
|
+
columnCount,
|
|
130
|
+
endRow,
|
|
131
|
+
itemCount,
|
|
132
|
+
rowCount,
|
|
133
|
+
rowOffsets,
|
|
134
|
+
rowSizes,
|
|
135
|
+
startRow,
|
|
136
|
+
totalSizePx,
|
|
137
|
+
visibleEndRow,
|
|
138
|
+
visibleStartRow,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),
|
|
142
|
+
offsetForRow: (row) => rowOffsets[row] ?? totalSizePx,
|
|
143
|
+
range,
|
|
144
|
+
rowOffsets,
|
|
145
|
+
rowSizes,
|
|
146
|
+
visibleRange: rangeToVisibleRange(range),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function createFixedSnapshot(options, items, itemCount, columnCount, rowCount, overscan) {
|
|
150
|
+
const itemSize = clampPositiveSize(options.estimateItemSize(0, items[0]));
|
|
151
|
+
const range = calculateVirtualRange({
|
|
152
|
+
columnCount,
|
|
153
|
+
itemCount,
|
|
154
|
+
itemSize,
|
|
155
|
+
overscan,
|
|
156
|
+
scrollOffset: options.scrollOffset(),
|
|
157
|
+
viewportSize: options.viewportSize(),
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),
|
|
161
|
+
offsetForRow: (row) => row * itemSize,
|
|
162
|
+
range,
|
|
163
|
+
rowOffsets: [],
|
|
164
|
+
rowSizes: [],
|
|
165
|
+
visibleRange: rangeToVisibleRange(range),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function computeRowSizes(items, columnCount, options, measuredSizes) {
|
|
169
|
+
const rowCount = Math.ceil(items.length / columnCount);
|
|
170
|
+
return Array.from({ length: rowCount }, (_unused, row) => {
|
|
171
|
+
let measuredMax;
|
|
172
|
+
const startIndex = row * columnCount;
|
|
173
|
+
const endIndex = Math.min(items.length, startIndex + columnCount);
|
|
174
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
175
|
+
const item = items[index];
|
|
176
|
+
if (item === undefined) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const measured = measuredSizes.get(options.getKey(item, index));
|
|
180
|
+
if (measured !== undefined) {
|
|
181
|
+
measuredMax = Math.max(measuredMax ?? 0, measured);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (measuredMax !== undefined) {
|
|
185
|
+
return measuredMax;
|
|
186
|
+
}
|
|
187
|
+
return clampPositiveSize(options.estimateItemSize(startIndex, items[startIndex]));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function computeRowOffsets(rowSizes) {
|
|
191
|
+
const offsets = [];
|
|
192
|
+
let current = 0;
|
|
193
|
+
for (const size of rowSizes) {
|
|
194
|
+
offsets.push(current);
|
|
195
|
+
current += size;
|
|
196
|
+
}
|
|
197
|
+
return offsets;
|
|
198
|
+
}
|
|
199
|
+
function createEntries(items, getKey, startIndex, endIndex, columnCount) {
|
|
200
|
+
const entries = [];
|
|
201
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
202
|
+
const item = items[index];
|
|
203
|
+
if (item === undefined) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
entries.push({
|
|
207
|
+
index,
|
|
208
|
+
item,
|
|
209
|
+
key: getKey(item, index),
|
|
210
|
+
row: Math.floor(index / columnCount),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return entries;
|
|
214
|
+
}
|
|
215
|
+
function createRange(options) {
|
|
216
|
+
const startIndex = Math.min(options.itemCount, options.startRow * options.columnCount);
|
|
217
|
+
const endIndex = Math.min(options.itemCount, options.endRow * options.columnCount);
|
|
218
|
+
const visibleStartIndex = Math.min(options.itemCount, options.visibleStartRow * options.columnCount);
|
|
219
|
+
const visibleEndIndex = Math.min(options.itemCount, options.visibleEndRow * options.columnCount);
|
|
220
|
+
const endOffset = options.endRow >= options.rowCount
|
|
221
|
+
? options.totalSizePx
|
|
222
|
+
: (options.rowOffsets[options.endRow] ?? options.totalSizePx);
|
|
223
|
+
const startOffset = options.startRow >= options.rowCount
|
|
224
|
+
? options.totalSizePx
|
|
225
|
+
: (options.rowOffsets[options.startRow] ?? 0);
|
|
226
|
+
return {
|
|
227
|
+
bottomSpacerPx: Math.max(0, options.totalSizePx - endOffset),
|
|
228
|
+
columnCount: options.columnCount,
|
|
229
|
+
endIndex,
|
|
230
|
+
endRow: options.endRow,
|
|
231
|
+
itemCount: options.itemCount,
|
|
232
|
+
rowCount: options.rowCount,
|
|
233
|
+
startIndex,
|
|
234
|
+
startRow: options.startRow,
|
|
235
|
+
topSpacerPx: startOffset,
|
|
236
|
+
totalSizePx: options.totalSizePx,
|
|
237
|
+
visibleEndIndex,
|
|
238
|
+
visibleEndRow: options.visibleEndRow,
|
|
239
|
+
visibleStartIndex,
|
|
240
|
+
visibleStartRow: options.visibleStartRow,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function emptyRange(columnCount) {
|
|
244
|
+
return {
|
|
245
|
+
bottomSpacerPx: 0,
|
|
246
|
+
columnCount,
|
|
247
|
+
endIndex: 0,
|
|
248
|
+
endRow: 0,
|
|
249
|
+
itemCount: 0,
|
|
250
|
+
rowCount: 0,
|
|
251
|
+
startIndex: 0,
|
|
252
|
+
startRow: 0,
|
|
253
|
+
topSpacerPx: 0,
|
|
254
|
+
totalSizePx: 0,
|
|
255
|
+
visibleEndIndex: 0,
|
|
256
|
+
visibleEndRow: 0,
|
|
257
|
+
visibleStartIndex: 0,
|
|
258
|
+
visibleStartRow: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function rangeToVisibleRange(range) {
|
|
262
|
+
return {
|
|
263
|
+
endIndex: range.visibleEndIndex,
|
|
264
|
+
endRow: range.visibleEndRow,
|
|
265
|
+
startIndex: range.visibleStartIndex,
|
|
266
|
+
startRow: range.visibleStartRow,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function findVisibleStartRow(rowOffsets, rowSizes, scrollOffset) {
|
|
270
|
+
for (let row = 0; row < rowSizes.length; row += 1) {
|
|
271
|
+
if ((rowOffsets[row] ?? 0) + (rowSizes[row] ?? 0) > scrollOffset) {
|
|
272
|
+
return row;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return rowSizes.length;
|
|
276
|
+
}
|
|
277
|
+
function findVisibleEndRow(rowOffsets, rowSizes, viewportEnd) {
|
|
278
|
+
let visibleEndRow = 0;
|
|
279
|
+
for (let row = 0; row < rowSizes.length; row += 1) {
|
|
280
|
+
if ((rowOffsets[row] ?? 0) < viewportEnd) {
|
|
281
|
+
visibleEndRow = row + 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return visibleEndRow;
|
|
285
|
+
}
|
|
286
|
+
function clampInteger(value, min, max = Number.MAX_SAFE_INTEGER) {
|
|
287
|
+
if (!Number.isFinite(value)) {
|
|
288
|
+
return min;
|
|
289
|
+
}
|
|
290
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
291
|
+
}
|
|
292
|
+
function clampSize(value) {
|
|
293
|
+
if (!Number.isFinite(value)) {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
return Math.max(0, value);
|
|
297
|
+
}
|
|
298
|
+
function clampPositiveSize(value) {
|
|
299
|
+
if (!Number.isFinite(value)) {
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
return Math.max(1, value);
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { cell, type ReadonlyCell } from \"@reckona/mreact-reactive-core\";\n\nexport type VirtualKey = string | number;\n\nexport interface VirtualRangeOptions {\n columnCount?: number;\n itemCount: number;\n itemSize: number;\n overscan?: number;\n scrollOffset: number;\n viewportSize: number;\n}\n\nexport interface VirtualRange {\n bottomSpacerPx: number;\n columnCount: number;\n endIndex: number;\n endRow: number;\n itemCount: number;\n rowCount: number;\n startIndex: number;\n startRow: number;\n topSpacerPx: number;\n totalSizePx: number;\n visibleEndIndex: number;\n visibleEndRow: number;\n visibleStartIndex: number;\n visibleStartRow: number;\n}\n\nexport interface VisibleRange {\n endIndex: number;\n endRow: number;\n startIndex: number;\n startRow: number;\n}\n\nexport interface VirtualEntry<TItem> {\n index: number;\n item: TItem;\n key: VirtualKey;\n row: number;\n}\n\nexport interface VirtualListOptions<TItem> {\n estimateItemSize: (index: number, item: TItem | undefined) => number;\n getKey: (item: TItem, index: number) => VirtualKey;\n items: () => readonly TItem[];\n overscan?: number;\n scrollOffset: () => number;\n viewportSize: () => number;\n}\n\nexport interface VirtualGridOptions<TItem> extends VirtualListOptions<TItem> {\n getColumnCount: () => number;\n}\n\nexport interface Virtualizer<TItem> {\n readonly bottomSpacerPx: ReadonlyCell<number>;\n readonly entries: ReadonlyCell<readonly VirtualEntry<TItem>[]>;\n readonly range: ReadonlyCell<VirtualRange>;\n readonly topSpacerPx: ReadonlyCell<number>;\n readonly totalSizePx: ReadonlyCell<number>;\n readonly visibleRange: ReadonlyCell<VisibleRange>;\n measureItem(key: VirtualKey, sizePx: number): void;\n refresh(): void;\n scrollToIndex(index: number): number;\n scrollToKey(key: VirtualKey): number | undefined;\n}\n\ninterface VirtualSnapshot<TItem> {\n entries: readonly VirtualEntry<TItem>[];\n offsetForRow: (row: number) => number;\n range: VirtualRange;\n rowOffsets: readonly number[];\n rowSizes: readonly number[];\n visibleRange: VisibleRange;\n}\n\nexport function calculateVirtualRange(options: VirtualRangeOptions): VirtualRange {\n const itemCount = clampInteger(options.itemCount, 0);\n const columnCount = clampInteger(options.columnCount ?? 1, 1);\n const itemSize = clampPositiveSize(options.itemSize);\n const overscan = clampInteger(options.overscan ?? 0, 0);\n const scrollOffset = clampSize(options.scrollOffset);\n const viewportSize = clampSize(options.viewportSize);\n const rowCount = Math.ceil(itemCount / columnCount);\n const totalSizePx = rowCount * itemSize;\n\n if (itemCount === 0 || rowCount === 0) {\n return emptyRange(columnCount);\n }\n\n const visibleStartRow = Math.min(rowCount, Math.floor(scrollOffset / itemSize));\n const visibleEndRow = Math.min(rowCount, Math.ceil((scrollOffset + viewportSize) / itemSize));\n const startRow = Math.max(0, visibleStartRow - overscan);\n const endRow = Math.min(rowCount, visibleEndRow + overscan);\n const startIndex = Math.min(itemCount, startRow * columnCount);\n const endIndex = Math.min(itemCount, endRow * columnCount);\n const visibleStartIndex = Math.min(itemCount, visibleStartRow * columnCount);\n const visibleEndIndex = Math.min(itemCount, visibleEndRow * columnCount);\n\n return {\n bottomSpacerPx: Math.max(0, (rowCount - endRow) * itemSize),\n columnCount,\n endIndex,\n endRow,\n itemCount,\n rowCount,\n startIndex,\n startRow,\n topSpacerPx: startRow * itemSize,\n totalSizePx,\n visibleEndIndex,\n visibleEndRow,\n visibleStartIndex,\n visibleStartRow,\n };\n}\n\nexport function createVirtualList<TItem>(options: VirtualListOptions<TItem>): Virtualizer<TItem> {\n return createVirtualizer({\n ...options,\n getColumnCount: () => 1,\n });\n}\n\nexport function createVirtualGrid<TItem>(options: VirtualGridOptions<TItem>): Virtualizer<TItem> {\n return createVirtualizer(options);\n}\n\nfunction createVirtualizer<TItem>(options: VirtualGridOptions<TItem>): Virtualizer<TItem> {\n const measuredSizes = new Map<VirtualKey, number>();\n let snapshot = createSnapshot(options, measuredSizes);\n const range = cell(snapshot.range);\n const visibleRange = cell(snapshot.visibleRange);\n const entries = cell(snapshot.entries);\n const topSpacerPx = cell(snapshot.range.topSpacerPx);\n const bottomSpacerPx = cell(snapshot.range.bottomSpacerPx);\n const totalSizePx = cell(snapshot.range.totalSizePx);\n\n const refresh = () => {\n snapshot = createSnapshot(options, measuredSizes);\n range.set(snapshot.range);\n visibleRange.set(snapshot.visibleRange);\n entries.set(snapshot.entries);\n topSpacerPx.set(snapshot.range.topSpacerPx);\n bottomSpacerPx.set(snapshot.range.bottomSpacerPx);\n totalSizePx.set(snapshot.range.totalSizePx);\n };\n const scrollToIndex = (index: number) => {\n const itemCount = options.items().length;\n if (itemCount === 0) {\n return 0;\n }\n const columnCount = clampInteger(options.getColumnCount(), 1);\n const row = Math.floor(clampInteger(index, 0, itemCount - 1) / columnCount);\n return snapshot.offsetForRow(row);\n };\n\n return {\n bottomSpacerPx,\n entries,\n range,\n topSpacerPx,\n totalSizePx,\n visibleRange,\n measureItem(key, sizePx) {\n measuredSizes.set(key, clampPositiveSize(sizePx));\n refresh();\n },\n refresh,\n scrollToIndex,\n scrollToKey(key) {\n const index = options\n .items()\n .findIndex((item, itemIndex) => options.getKey(item, itemIndex) === key);\n if (index === -1) {\n return undefined;\n }\n return scrollToIndex(index);\n },\n };\n}\n\nfunction createSnapshot<TItem>(\n options: VirtualGridOptions<TItem>,\n measuredSizes: ReadonlyMap<VirtualKey, number>,\n): VirtualSnapshot<TItem> {\n const items = options.items();\n const itemCount = items.length;\n const columnCount = clampInteger(options.getColumnCount(), 1);\n const overscan = clampInteger(options.overscan ?? 0, 0);\n const rowCount = Math.ceil(itemCount / columnCount);\n\n if (measuredSizes.size === 0) {\n return createFixedSnapshot(options, items, itemCount, columnCount, rowCount, overscan);\n }\n\n const rowSizes = computeRowSizes(items, columnCount, options, measuredSizes);\n const rowOffsets = computeRowOffsets(rowSizes);\n const totalSizePx = rowSizes.reduce((total, size) => total + size, 0);\n\n if (itemCount === 0 || rowCount === 0) {\n const range = emptyRange(columnCount);\n return {\n entries: [],\n offsetForRow: () => 0,\n range,\n rowOffsets,\n rowSizes,\n visibleRange: rangeToVisibleRange(range),\n };\n }\n\n const scrollOffset = clampSize(options.scrollOffset());\n const viewportEnd = scrollOffset + clampSize(options.viewportSize());\n const visibleStartRow = findVisibleStartRow(rowOffsets, rowSizes, scrollOffset);\n const visibleEndRow = findVisibleEndRow(rowOffsets, rowSizes, viewportEnd);\n const startRow = Math.max(0, visibleStartRow - overscan);\n const endRow = Math.min(rowCount, visibleEndRow + overscan);\n const range = createRange({\n columnCount,\n endRow,\n itemCount,\n rowCount,\n rowOffsets,\n rowSizes,\n startRow,\n totalSizePx,\n visibleEndRow,\n visibleStartRow,\n });\n\n return {\n entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),\n offsetForRow: (row) => rowOffsets[row] ?? totalSizePx,\n range,\n rowOffsets,\n rowSizes,\n visibleRange: rangeToVisibleRange(range),\n };\n}\n\nfunction createFixedSnapshot<TItem>(\n options: VirtualGridOptions<TItem>,\n items: readonly TItem[],\n itemCount: number,\n columnCount: number,\n rowCount: number,\n overscan: number,\n): VirtualSnapshot<TItem> {\n const itemSize = clampPositiveSize(options.estimateItemSize(0, items[0]));\n const range = calculateVirtualRange({\n columnCount,\n itemCount,\n itemSize,\n overscan,\n scrollOffset: options.scrollOffset(),\n viewportSize: options.viewportSize(),\n });\n\n return {\n entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),\n offsetForRow: (row) => row * itemSize,\n range,\n rowOffsets: [],\n rowSizes: [],\n visibleRange: rangeToVisibleRange(range),\n };\n}\n\nfunction computeRowSizes<TItem>(\n items: readonly TItem[],\n columnCount: number,\n options: VirtualGridOptions<TItem>,\n measuredSizes: ReadonlyMap<VirtualKey, number>,\n): number[] {\n const rowCount = Math.ceil(items.length / columnCount);\n return Array.from({ length: rowCount }, (_unused, row) => {\n let measuredMax: number | undefined;\n const startIndex = row * columnCount;\n const endIndex = Math.min(items.length, startIndex + columnCount);\n\n for (let index = startIndex; index < endIndex; index += 1) {\n const item = items[index];\n if (item === undefined) {\n continue;\n }\n const measured = measuredSizes.get(options.getKey(item, index));\n if (measured !== undefined) {\n measuredMax = Math.max(measuredMax ?? 0, measured);\n }\n }\n\n if (measuredMax !== undefined) {\n return measuredMax;\n }\n\n return clampPositiveSize(options.estimateItemSize(startIndex, items[startIndex]));\n });\n}\n\nfunction computeRowOffsets(rowSizes: readonly number[]): number[] {\n const offsets: number[] = [];\n let current = 0;\n\n for (const size of rowSizes) {\n offsets.push(current);\n current += size;\n }\n\n return offsets;\n}\n\nfunction createEntries<TItem>(\n items: readonly TItem[],\n getKey: (item: TItem, index: number) => VirtualKey,\n startIndex: number,\n endIndex: number,\n columnCount: number,\n): VirtualEntry<TItem>[] {\n const entries: VirtualEntry<TItem>[] = [];\n\n for (let index = startIndex; index < endIndex; index += 1) {\n const item = items[index];\n if (item === undefined) {\n continue;\n }\n entries.push({\n index,\n item,\n key: getKey(item, index),\n row: Math.floor(index / columnCount),\n });\n }\n\n return entries;\n}\n\nfunction createRange(options: {\n columnCount: number;\n endRow: number;\n itemCount: number;\n rowCount: number;\n rowOffsets: readonly number[];\n rowSizes: readonly number[];\n startRow: number;\n totalSizePx: number;\n visibleEndRow: number;\n visibleStartRow: number;\n}): VirtualRange {\n const startIndex = Math.min(options.itemCount, options.startRow * options.columnCount);\n const endIndex = Math.min(options.itemCount, options.endRow * options.columnCount);\n const visibleStartIndex = Math.min(\n options.itemCount,\n options.visibleStartRow * options.columnCount,\n );\n const visibleEndIndex = Math.min(options.itemCount, options.visibleEndRow * options.columnCount);\n const endOffset = options.endRow >= options.rowCount\n ? options.totalSizePx\n : (options.rowOffsets[options.endRow] ?? options.totalSizePx);\n const startOffset = options.startRow >= options.rowCount\n ? options.totalSizePx\n : (options.rowOffsets[options.startRow] ?? 0);\n\n return {\n bottomSpacerPx: Math.max(0, options.totalSizePx - endOffset),\n columnCount: options.columnCount,\n endIndex,\n endRow: options.endRow,\n itemCount: options.itemCount,\n rowCount: options.rowCount,\n startIndex,\n startRow: options.startRow,\n topSpacerPx: startOffset,\n totalSizePx: options.totalSizePx,\n visibleEndIndex,\n visibleEndRow: options.visibleEndRow,\n visibleStartIndex,\n visibleStartRow: options.visibleStartRow,\n };\n}\n\nfunction emptyRange(columnCount: number): VirtualRange {\n return {\n bottomSpacerPx: 0,\n columnCount,\n endIndex: 0,\n endRow: 0,\n itemCount: 0,\n rowCount: 0,\n startIndex: 0,\n startRow: 0,\n topSpacerPx: 0,\n totalSizePx: 0,\n visibleEndIndex: 0,\n visibleEndRow: 0,\n visibleStartIndex: 0,\n visibleStartRow: 0,\n };\n}\n\nfunction rangeToVisibleRange(range: VirtualRange): VisibleRange {\n return {\n endIndex: range.visibleEndIndex,\n endRow: range.visibleEndRow,\n startIndex: range.visibleStartIndex,\n startRow: range.visibleStartRow,\n };\n}\n\nfunction findVisibleStartRow(\n rowOffsets: readonly number[],\n rowSizes: readonly number[],\n scrollOffset: number,\n): number {\n for (let row = 0; row < rowSizes.length; row += 1) {\n if ((rowOffsets[row] ?? 0) + (rowSizes[row] ?? 0) > scrollOffset) {\n return row;\n }\n }\n\n return rowSizes.length;\n}\n\nfunction findVisibleEndRow(\n rowOffsets: readonly number[],\n rowSizes: readonly number[],\n viewportEnd: number,\n): number {\n let visibleEndRow = 0;\n\n for (let row = 0; row < rowSizes.length; row += 1) {\n if ((rowOffsets[row] ?? 0) < viewportEnd) {\n visibleEndRow = row + 1;\n }\n }\n\n return visibleEndRow;\n}\n\nfunction clampInteger(value: number, min: number, max = Number.MAX_SAFE_INTEGER): number {\n if (!Number.isFinite(value)) {\n return min;\n }\n\n return Math.min(max, Math.max(min, Math.floor(value)));\n}\n\nfunction clampSize(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n return Math.max(0, value);\n}\n\nfunction clampPositiveSize(value: number): number {\n if (!Number.isFinite(value)) {\n return 1;\n }\n\n return Math.max(1, value);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reckona/mreact-virtual",
|
|
3
|
+
"version": "0.0.82",
|
|
4
|
+
"description": "Reactive list and grid virtualization primitives for mreact.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"grid",
|
|
7
|
+
"jsx",
|
|
8
|
+
"mreact",
|
|
9
|
+
"reactive",
|
|
10
|
+
"virtual",
|
|
11
|
+
"virtualizer"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/t-k/mreact/tree/main/packages/virtual#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/t-k/mreact/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/t-k/mreact.git",
|
|
21
|
+
"directory": "packages/virtual"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/**/*.js",
|
|
25
|
+
"dist/**/*.js.map",
|
|
26
|
+
"dist/**/*.d.ts",
|
|
27
|
+
"dist/**/*.d.ts.map",
|
|
28
|
+
"src/**/*"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@reckona/mreact-reactive-core": "0.0.82"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { cell, type ReadonlyCell } from "@reckona/mreact-reactive-core";
|
|
2
|
+
|
|
3
|
+
export type VirtualKey = string | number;
|
|
4
|
+
|
|
5
|
+
export interface VirtualRangeOptions {
|
|
6
|
+
columnCount?: number;
|
|
7
|
+
itemCount: number;
|
|
8
|
+
itemSize: number;
|
|
9
|
+
overscan?: number;
|
|
10
|
+
scrollOffset: number;
|
|
11
|
+
viewportSize: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VirtualRange {
|
|
15
|
+
bottomSpacerPx: number;
|
|
16
|
+
columnCount: number;
|
|
17
|
+
endIndex: number;
|
|
18
|
+
endRow: number;
|
|
19
|
+
itemCount: number;
|
|
20
|
+
rowCount: number;
|
|
21
|
+
startIndex: number;
|
|
22
|
+
startRow: number;
|
|
23
|
+
topSpacerPx: number;
|
|
24
|
+
totalSizePx: number;
|
|
25
|
+
visibleEndIndex: number;
|
|
26
|
+
visibleEndRow: number;
|
|
27
|
+
visibleStartIndex: number;
|
|
28
|
+
visibleStartRow: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface VisibleRange {
|
|
32
|
+
endIndex: number;
|
|
33
|
+
endRow: number;
|
|
34
|
+
startIndex: number;
|
|
35
|
+
startRow: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VirtualEntry<TItem> {
|
|
39
|
+
index: number;
|
|
40
|
+
item: TItem;
|
|
41
|
+
key: VirtualKey;
|
|
42
|
+
row: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface VirtualListOptions<TItem> {
|
|
46
|
+
estimateItemSize: (index: number, item: TItem | undefined) => number;
|
|
47
|
+
getKey: (item: TItem, index: number) => VirtualKey;
|
|
48
|
+
items: () => readonly TItem[];
|
|
49
|
+
overscan?: number;
|
|
50
|
+
scrollOffset: () => number;
|
|
51
|
+
viewportSize: () => number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface VirtualGridOptions<TItem> extends VirtualListOptions<TItem> {
|
|
55
|
+
getColumnCount: () => number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Virtualizer<TItem> {
|
|
59
|
+
readonly bottomSpacerPx: ReadonlyCell<number>;
|
|
60
|
+
readonly entries: ReadonlyCell<readonly VirtualEntry<TItem>[]>;
|
|
61
|
+
readonly range: ReadonlyCell<VirtualRange>;
|
|
62
|
+
readonly topSpacerPx: ReadonlyCell<number>;
|
|
63
|
+
readonly totalSizePx: ReadonlyCell<number>;
|
|
64
|
+
readonly visibleRange: ReadonlyCell<VisibleRange>;
|
|
65
|
+
measureItem(key: VirtualKey, sizePx: number): void;
|
|
66
|
+
refresh(): void;
|
|
67
|
+
scrollToIndex(index: number): number;
|
|
68
|
+
scrollToKey(key: VirtualKey): number | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface VirtualSnapshot<TItem> {
|
|
72
|
+
entries: readonly VirtualEntry<TItem>[];
|
|
73
|
+
offsetForRow: (row: number) => number;
|
|
74
|
+
range: VirtualRange;
|
|
75
|
+
rowOffsets: readonly number[];
|
|
76
|
+
rowSizes: readonly number[];
|
|
77
|
+
visibleRange: VisibleRange;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function calculateVirtualRange(options: VirtualRangeOptions): VirtualRange {
|
|
81
|
+
const itemCount = clampInteger(options.itemCount, 0);
|
|
82
|
+
const columnCount = clampInteger(options.columnCount ?? 1, 1);
|
|
83
|
+
const itemSize = clampPositiveSize(options.itemSize);
|
|
84
|
+
const overscan = clampInteger(options.overscan ?? 0, 0);
|
|
85
|
+
const scrollOffset = clampSize(options.scrollOffset);
|
|
86
|
+
const viewportSize = clampSize(options.viewportSize);
|
|
87
|
+
const rowCount = Math.ceil(itemCount / columnCount);
|
|
88
|
+
const totalSizePx = rowCount * itemSize;
|
|
89
|
+
|
|
90
|
+
if (itemCount === 0 || rowCount === 0) {
|
|
91
|
+
return emptyRange(columnCount);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const visibleStartRow = Math.min(rowCount, Math.floor(scrollOffset / itemSize));
|
|
95
|
+
const visibleEndRow = Math.min(rowCount, Math.ceil((scrollOffset + viewportSize) / itemSize));
|
|
96
|
+
const startRow = Math.max(0, visibleStartRow - overscan);
|
|
97
|
+
const endRow = Math.min(rowCount, visibleEndRow + overscan);
|
|
98
|
+
const startIndex = Math.min(itemCount, startRow * columnCount);
|
|
99
|
+
const endIndex = Math.min(itemCount, endRow * columnCount);
|
|
100
|
+
const visibleStartIndex = Math.min(itemCount, visibleStartRow * columnCount);
|
|
101
|
+
const visibleEndIndex = Math.min(itemCount, visibleEndRow * columnCount);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
bottomSpacerPx: Math.max(0, (rowCount - endRow) * itemSize),
|
|
105
|
+
columnCount,
|
|
106
|
+
endIndex,
|
|
107
|
+
endRow,
|
|
108
|
+
itemCount,
|
|
109
|
+
rowCount,
|
|
110
|
+
startIndex,
|
|
111
|
+
startRow,
|
|
112
|
+
topSpacerPx: startRow * itemSize,
|
|
113
|
+
totalSizePx,
|
|
114
|
+
visibleEndIndex,
|
|
115
|
+
visibleEndRow,
|
|
116
|
+
visibleStartIndex,
|
|
117
|
+
visibleStartRow,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createVirtualList<TItem>(options: VirtualListOptions<TItem>): Virtualizer<TItem> {
|
|
122
|
+
return createVirtualizer({
|
|
123
|
+
...options,
|
|
124
|
+
getColumnCount: () => 1,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createVirtualGrid<TItem>(options: VirtualGridOptions<TItem>): Virtualizer<TItem> {
|
|
129
|
+
return createVirtualizer(options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createVirtualizer<TItem>(options: VirtualGridOptions<TItem>): Virtualizer<TItem> {
|
|
133
|
+
const measuredSizes = new Map<VirtualKey, number>();
|
|
134
|
+
let snapshot = createSnapshot(options, measuredSizes);
|
|
135
|
+
const range = cell(snapshot.range);
|
|
136
|
+
const visibleRange = cell(snapshot.visibleRange);
|
|
137
|
+
const entries = cell(snapshot.entries);
|
|
138
|
+
const topSpacerPx = cell(snapshot.range.topSpacerPx);
|
|
139
|
+
const bottomSpacerPx = cell(snapshot.range.bottomSpacerPx);
|
|
140
|
+
const totalSizePx = cell(snapshot.range.totalSizePx);
|
|
141
|
+
|
|
142
|
+
const refresh = () => {
|
|
143
|
+
snapshot = createSnapshot(options, measuredSizes);
|
|
144
|
+
range.set(snapshot.range);
|
|
145
|
+
visibleRange.set(snapshot.visibleRange);
|
|
146
|
+
entries.set(snapshot.entries);
|
|
147
|
+
topSpacerPx.set(snapshot.range.topSpacerPx);
|
|
148
|
+
bottomSpacerPx.set(snapshot.range.bottomSpacerPx);
|
|
149
|
+
totalSizePx.set(snapshot.range.totalSizePx);
|
|
150
|
+
};
|
|
151
|
+
const scrollToIndex = (index: number) => {
|
|
152
|
+
const itemCount = options.items().length;
|
|
153
|
+
if (itemCount === 0) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
const columnCount = clampInteger(options.getColumnCount(), 1);
|
|
157
|
+
const row = Math.floor(clampInteger(index, 0, itemCount - 1) / columnCount);
|
|
158
|
+
return snapshot.offsetForRow(row);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
bottomSpacerPx,
|
|
163
|
+
entries,
|
|
164
|
+
range,
|
|
165
|
+
topSpacerPx,
|
|
166
|
+
totalSizePx,
|
|
167
|
+
visibleRange,
|
|
168
|
+
measureItem(key, sizePx) {
|
|
169
|
+
measuredSizes.set(key, clampPositiveSize(sizePx));
|
|
170
|
+
refresh();
|
|
171
|
+
},
|
|
172
|
+
refresh,
|
|
173
|
+
scrollToIndex,
|
|
174
|
+
scrollToKey(key) {
|
|
175
|
+
const index = options
|
|
176
|
+
.items()
|
|
177
|
+
.findIndex((item, itemIndex) => options.getKey(item, itemIndex) === key);
|
|
178
|
+
if (index === -1) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
return scrollToIndex(index);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createSnapshot<TItem>(
|
|
187
|
+
options: VirtualGridOptions<TItem>,
|
|
188
|
+
measuredSizes: ReadonlyMap<VirtualKey, number>,
|
|
189
|
+
): VirtualSnapshot<TItem> {
|
|
190
|
+
const items = options.items();
|
|
191
|
+
const itemCount = items.length;
|
|
192
|
+
const columnCount = clampInteger(options.getColumnCount(), 1);
|
|
193
|
+
const overscan = clampInteger(options.overscan ?? 0, 0);
|
|
194
|
+
const rowCount = Math.ceil(itemCount / columnCount);
|
|
195
|
+
|
|
196
|
+
if (measuredSizes.size === 0) {
|
|
197
|
+
return createFixedSnapshot(options, items, itemCount, columnCount, rowCount, overscan);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rowSizes = computeRowSizes(items, columnCount, options, measuredSizes);
|
|
201
|
+
const rowOffsets = computeRowOffsets(rowSizes);
|
|
202
|
+
const totalSizePx = rowSizes.reduce((total, size) => total + size, 0);
|
|
203
|
+
|
|
204
|
+
if (itemCount === 0 || rowCount === 0) {
|
|
205
|
+
const range = emptyRange(columnCount);
|
|
206
|
+
return {
|
|
207
|
+
entries: [],
|
|
208
|
+
offsetForRow: () => 0,
|
|
209
|
+
range,
|
|
210
|
+
rowOffsets,
|
|
211
|
+
rowSizes,
|
|
212
|
+
visibleRange: rangeToVisibleRange(range),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const scrollOffset = clampSize(options.scrollOffset());
|
|
217
|
+
const viewportEnd = scrollOffset + clampSize(options.viewportSize());
|
|
218
|
+
const visibleStartRow = findVisibleStartRow(rowOffsets, rowSizes, scrollOffset);
|
|
219
|
+
const visibleEndRow = findVisibleEndRow(rowOffsets, rowSizes, viewportEnd);
|
|
220
|
+
const startRow = Math.max(0, visibleStartRow - overscan);
|
|
221
|
+
const endRow = Math.min(rowCount, visibleEndRow + overscan);
|
|
222
|
+
const range = createRange({
|
|
223
|
+
columnCount,
|
|
224
|
+
endRow,
|
|
225
|
+
itemCount,
|
|
226
|
+
rowCount,
|
|
227
|
+
rowOffsets,
|
|
228
|
+
rowSizes,
|
|
229
|
+
startRow,
|
|
230
|
+
totalSizePx,
|
|
231
|
+
visibleEndRow,
|
|
232
|
+
visibleStartRow,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),
|
|
237
|
+
offsetForRow: (row) => rowOffsets[row] ?? totalSizePx,
|
|
238
|
+
range,
|
|
239
|
+
rowOffsets,
|
|
240
|
+
rowSizes,
|
|
241
|
+
visibleRange: rangeToVisibleRange(range),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createFixedSnapshot<TItem>(
|
|
246
|
+
options: VirtualGridOptions<TItem>,
|
|
247
|
+
items: readonly TItem[],
|
|
248
|
+
itemCount: number,
|
|
249
|
+
columnCount: number,
|
|
250
|
+
rowCount: number,
|
|
251
|
+
overscan: number,
|
|
252
|
+
): VirtualSnapshot<TItem> {
|
|
253
|
+
const itemSize = clampPositiveSize(options.estimateItemSize(0, items[0]));
|
|
254
|
+
const range = calculateVirtualRange({
|
|
255
|
+
columnCount,
|
|
256
|
+
itemCount,
|
|
257
|
+
itemSize,
|
|
258
|
+
overscan,
|
|
259
|
+
scrollOffset: options.scrollOffset(),
|
|
260
|
+
viewportSize: options.viewportSize(),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
entries: createEntries(items, options.getKey, range.startIndex, range.endIndex, columnCount),
|
|
265
|
+
offsetForRow: (row) => row * itemSize,
|
|
266
|
+
range,
|
|
267
|
+
rowOffsets: [],
|
|
268
|
+
rowSizes: [],
|
|
269
|
+
visibleRange: rangeToVisibleRange(range),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function computeRowSizes<TItem>(
|
|
274
|
+
items: readonly TItem[],
|
|
275
|
+
columnCount: number,
|
|
276
|
+
options: VirtualGridOptions<TItem>,
|
|
277
|
+
measuredSizes: ReadonlyMap<VirtualKey, number>,
|
|
278
|
+
): number[] {
|
|
279
|
+
const rowCount = Math.ceil(items.length / columnCount);
|
|
280
|
+
return Array.from({ length: rowCount }, (_unused, row) => {
|
|
281
|
+
let measuredMax: number | undefined;
|
|
282
|
+
const startIndex = row * columnCount;
|
|
283
|
+
const endIndex = Math.min(items.length, startIndex + columnCount);
|
|
284
|
+
|
|
285
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
286
|
+
const item = items[index];
|
|
287
|
+
if (item === undefined) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const measured = measuredSizes.get(options.getKey(item, index));
|
|
291
|
+
if (measured !== undefined) {
|
|
292
|
+
measuredMax = Math.max(measuredMax ?? 0, measured);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (measuredMax !== undefined) {
|
|
297
|
+
return measuredMax;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return clampPositiveSize(options.estimateItemSize(startIndex, items[startIndex]));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function computeRowOffsets(rowSizes: readonly number[]): number[] {
|
|
305
|
+
const offsets: number[] = [];
|
|
306
|
+
let current = 0;
|
|
307
|
+
|
|
308
|
+
for (const size of rowSizes) {
|
|
309
|
+
offsets.push(current);
|
|
310
|
+
current += size;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return offsets;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createEntries<TItem>(
|
|
317
|
+
items: readonly TItem[],
|
|
318
|
+
getKey: (item: TItem, index: number) => VirtualKey,
|
|
319
|
+
startIndex: number,
|
|
320
|
+
endIndex: number,
|
|
321
|
+
columnCount: number,
|
|
322
|
+
): VirtualEntry<TItem>[] {
|
|
323
|
+
const entries: VirtualEntry<TItem>[] = [];
|
|
324
|
+
|
|
325
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
326
|
+
const item = items[index];
|
|
327
|
+
if (item === undefined) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
entries.push({
|
|
331
|
+
index,
|
|
332
|
+
item,
|
|
333
|
+
key: getKey(item, index),
|
|
334
|
+
row: Math.floor(index / columnCount),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return entries;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function createRange(options: {
|
|
342
|
+
columnCount: number;
|
|
343
|
+
endRow: number;
|
|
344
|
+
itemCount: number;
|
|
345
|
+
rowCount: number;
|
|
346
|
+
rowOffsets: readonly number[];
|
|
347
|
+
rowSizes: readonly number[];
|
|
348
|
+
startRow: number;
|
|
349
|
+
totalSizePx: number;
|
|
350
|
+
visibleEndRow: number;
|
|
351
|
+
visibleStartRow: number;
|
|
352
|
+
}): VirtualRange {
|
|
353
|
+
const startIndex = Math.min(options.itemCount, options.startRow * options.columnCount);
|
|
354
|
+
const endIndex = Math.min(options.itemCount, options.endRow * options.columnCount);
|
|
355
|
+
const visibleStartIndex = Math.min(
|
|
356
|
+
options.itemCount,
|
|
357
|
+
options.visibleStartRow * options.columnCount,
|
|
358
|
+
);
|
|
359
|
+
const visibleEndIndex = Math.min(options.itemCount, options.visibleEndRow * options.columnCount);
|
|
360
|
+
const endOffset = options.endRow >= options.rowCount
|
|
361
|
+
? options.totalSizePx
|
|
362
|
+
: (options.rowOffsets[options.endRow] ?? options.totalSizePx);
|
|
363
|
+
const startOffset = options.startRow >= options.rowCount
|
|
364
|
+
? options.totalSizePx
|
|
365
|
+
: (options.rowOffsets[options.startRow] ?? 0);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
bottomSpacerPx: Math.max(0, options.totalSizePx - endOffset),
|
|
369
|
+
columnCount: options.columnCount,
|
|
370
|
+
endIndex,
|
|
371
|
+
endRow: options.endRow,
|
|
372
|
+
itemCount: options.itemCount,
|
|
373
|
+
rowCount: options.rowCount,
|
|
374
|
+
startIndex,
|
|
375
|
+
startRow: options.startRow,
|
|
376
|
+
topSpacerPx: startOffset,
|
|
377
|
+
totalSizePx: options.totalSizePx,
|
|
378
|
+
visibleEndIndex,
|
|
379
|
+
visibleEndRow: options.visibleEndRow,
|
|
380
|
+
visibleStartIndex,
|
|
381
|
+
visibleStartRow: options.visibleStartRow,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function emptyRange(columnCount: number): VirtualRange {
|
|
386
|
+
return {
|
|
387
|
+
bottomSpacerPx: 0,
|
|
388
|
+
columnCount,
|
|
389
|
+
endIndex: 0,
|
|
390
|
+
endRow: 0,
|
|
391
|
+
itemCount: 0,
|
|
392
|
+
rowCount: 0,
|
|
393
|
+
startIndex: 0,
|
|
394
|
+
startRow: 0,
|
|
395
|
+
topSpacerPx: 0,
|
|
396
|
+
totalSizePx: 0,
|
|
397
|
+
visibleEndIndex: 0,
|
|
398
|
+
visibleEndRow: 0,
|
|
399
|
+
visibleStartIndex: 0,
|
|
400
|
+
visibleStartRow: 0,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function rangeToVisibleRange(range: VirtualRange): VisibleRange {
|
|
405
|
+
return {
|
|
406
|
+
endIndex: range.visibleEndIndex,
|
|
407
|
+
endRow: range.visibleEndRow,
|
|
408
|
+
startIndex: range.visibleStartIndex,
|
|
409
|
+
startRow: range.visibleStartRow,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function findVisibleStartRow(
|
|
414
|
+
rowOffsets: readonly number[],
|
|
415
|
+
rowSizes: readonly number[],
|
|
416
|
+
scrollOffset: number,
|
|
417
|
+
): number {
|
|
418
|
+
for (let row = 0; row < rowSizes.length; row += 1) {
|
|
419
|
+
if ((rowOffsets[row] ?? 0) + (rowSizes[row] ?? 0) > scrollOffset) {
|
|
420
|
+
return row;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return rowSizes.length;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function findVisibleEndRow(
|
|
428
|
+
rowOffsets: readonly number[],
|
|
429
|
+
rowSizes: readonly number[],
|
|
430
|
+
viewportEnd: number,
|
|
431
|
+
): number {
|
|
432
|
+
let visibleEndRow = 0;
|
|
433
|
+
|
|
434
|
+
for (let row = 0; row < rowSizes.length; row += 1) {
|
|
435
|
+
if ((rowOffsets[row] ?? 0) < viewportEnd) {
|
|
436
|
+
visibleEndRow = row + 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return visibleEndRow;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function clampInteger(value: number, min: number, max = Number.MAX_SAFE_INTEGER): number {
|
|
444
|
+
if (!Number.isFinite(value)) {
|
|
445
|
+
return min;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clampSize(value: number): number {
|
|
452
|
+
if (!Number.isFinite(value)) {
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return Math.max(0, value);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function clampPositiveSize(value: number): number {
|
|
460
|
+
if (!Number.isFinite(value)) {
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return Math.max(1, value);
|
|
465
|
+
}
|