mtrl-addons 0.1.2 → 0.2.2
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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /package/src/core/{compose → viewport}/features/performance.ts +0 -0
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Viewport and range calculation utilities
|
|
3
|
-
* Orientation-agnostic math functions for virtual scrolling
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ItemRange, ViewportInfo, ListManagerConfig } from "../types";
|
|
7
|
-
import { LIST_MANAGER_CONSTANTS } from "../constants";
|
|
8
|
-
|
|
9
|
-
// Removed calculateVisibleRange - using unified index-based approach in virtual.ts
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Calculate total virtual size based on items and estimated size
|
|
13
|
-
*/
|
|
14
|
-
export function calculateTotalVirtualSize(
|
|
15
|
-
totalItems: number,
|
|
16
|
-
estimatedItemSize: number,
|
|
17
|
-
measuredSizes?: Map<number, number>
|
|
18
|
-
): number {
|
|
19
|
-
if (!measuredSizes || measuredSizes.size === 0) {
|
|
20
|
-
return totalItems * estimatedItemSize;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Use measured sizes where available, estimated for the rest
|
|
24
|
-
let totalSize = 0;
|
|
25
|
-
for (let i = 0; i < totalItems; i++) {
|
|
26
|
-
totalSize += measuredSizes.get(i) || estimatedItemSize;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return totalSize;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Calculate container position for virtual scrolling
|
|
34
|
-
* Returns the transform value for the container
|
|
35
|
-
*/
|
|
36
|
-
export function calculateContainerPosition(
|
|
37
|
-
virtualScrollPosition: number,
|
|
38
|
-
visibleRange: ItemRange,
|
|
39
|
-
measuredSizes?: Map<number, number>,
|
|
40
|
-
estimatedItemSize: number = LIST_MANAGER_CONSTANTS.VIRTUAL_SCROLL
|
|
41
|
-
.DEFAULT_ITEM_SIZE
|
|
42
|
-
): number {
|
|
43
|
-
let offset = 0;
|
|
44
|
-
|
|
45
|
-
// Calculate offset based on measured sizes if available
|
|
46
|
-
if (measuredSizes && measuredSizes.size > 0) {
|
|
47
|
-
for (let i = 0; i < visibleRange.start; i++) {
|
|
48
|
-
offset += measuredSizes.get(i) || estimatedItemSize;
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// Use estimated size for all items before visible range
|
|
52
|
-
offset = visibleRange.start * estimatedItemSize;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return offset;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Calculate scroll position for specific index
|
|
60
|
-
*/
|
|
61
|
-
export function calculateScrollPositionForIndex(
|
|
62
|
-
targetIndex: number,
|
|
63
|
-
estimatedItemSize: number,
|
|
64
|
-
measuredSizes?: Map<number, number>,
|
|
65
|
-
alignment: "start" | "center" | "end" = "start",
|
|
66
|
-
containerSize: number = 0
|
|
67
|
-
): number {
|
|
68
|
-
let scrollPosition = 0;
|
|
69
|
-
|
|
70
|
-
// Calculate position based on measured sizes if available
|
|
71
|
-
if (measuredSizes && measuredSizes.size > 0) {
|
|
72
|
-
for (let i = 0; i < targetIndex; i++) {
|
|
73
|
-
scrollPosition += measuredSizes.get(i) || estimatedItemSize;
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
scrollPosition = targetIndex * estimatedItemSize;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Apply alignment
|
|
80
|
-
if (alignment === "center") {
|
|
81
|
-
const itemSize = measuredSizes?.get(targetIndex) || estimatedItemSize;
|
|
82
|
-
scrollPosition -= (containerSize - itemSize) / 2;
|
|
83
|
-
} else if (alignment === "end") {
|
|
84
|
-
const itemSize = measuredSizes?.get(targetIndex) || estimatedItemSize;
|
|
85
|
-
scrollPosition -= containerSize - itemSize;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return Math.max(0, scrollPosition);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Calculate page-based scroll position
|
|
93
|
-
*/
|
|
94
|
-
export function calculateScrollPositionForPage(
|
|
95
|
-
targetPage: number,
|
|
96
|
-
pageSize: number,
|
|
97
|
-
estimatedItemSize: number,
|
|
98
|
-
measuredSizes?: Map<number, number>,
|
|
99
|
-
alignment: "start" | "center" | "end" = "start",
|
|
100
|
-
containerSize: number = 0
|
|
101
|
-
): number {
|
|
102
|
-
const targetIndex = (targetPage - 1) * pageSize;
|
|
103
|
-
return calculateScrollPositionForIndex(
|
|
104
|
-
targetIndex,
|
|
105
|
-
estimatedItemSize,
|
|
106
|
-
measuredSizes,
|
|
107
|
-
alignment,
|
|
108
|
-
containerSize
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Calculate scrollbar thumb position and size
|
|
114
|
-
*/
|
|
115
|
-
export function calculateScrollbarMetrics(
|
|
116
|
-
virtualScrollPosition: number,
|
|
117
|
-
totalVirtualSize: number,
|
|
118
|
-
containerSize: number,
|
|
119
|
-
scrollbarTrackSize: number
|
|
120
|
-
): { thumbPosition: number; thumbSize: number } {
|
|
121
|
-
// Calculate thumb size based on container to total ratio
|
|
122
|
-
const thumbSize = Math.max(
|
|
123
|
-
LIST_MANAGER_CONSTANTS.SCROLLBAR.THUMB_MIN_SIZE,
|
|
124
|
-
(containerSize / totalVirtualSize) * scrollbarTrackSize
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Calculate thumb position based on scroll position
|
|
128
|
-
const scrollRatio =
|
|
129
|
-
virtualScrollPosition / (totalVirtualSize - containerSize);
|
|
130
|
-
const thumbPosition = scrollRatio * (scrollbarTrackSize - thumbSize);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
thumbPosition: Math.max(0, thumbPosition),
|
|
134
|
-
thumbSize: Math.min(thumbSize, scrollbarTrackSize),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Removed calculateViewportInfo - viewport.ts now uses virtualManager directly
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Calculate optimal overscan based on viewport capacity
|
|
142
|
-
* Uses a percentage-based approach that scales with viewport size
|
|
143
|
-
*/
|
|
144
|
-
export function calculateOptimalOverscan(
|
|
145
|
-
containerSize: number,
|
|
146
|
-
estimatedItemSize: number,
|
|
147
|
-
scrollSpeed?: number
|
|
148
|
-
): number {
|
|
149
|
-
// Calculate how many items fit in the viewport
|
|
150
|
-
const viewportCapacity = Math.ceil(containerSize / estimatedItemSize);
|
|
151
|
-
|
|
152
|
-
// Base overscan is 15% of viewport capacity
|
|
153
|
-
let overscan = Math.ceil(viewportCapacity * 0.15);
|
|
154
|
-
|
|
155
|
-
// Adjust based on scroll speed if provided
|
|
156
|
-
if (scrollSpeed !== undefined) {
|
|
157
|
-
if (scrollSpeed > 1000) {
|
|
158
|
-
// Fast scrolling: increase overscan to 25% of viewport
|
|
159
|
-
overscan = Math.ceil(viewportCapacity * 0.25);
|
|
160
|
-
} else if (scrollSpeed < 100) {
|
|
161
|
-
// Slow scrolling: reduce overscan to 10% of viewport
|
|
162
|
-
overscan = Math.ceil(viewportCapacity * 0.1);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Apply min/max constraints
|
|
167
|
-
const MIN_OVERSCAN = 1;
|
|
168
|
-
const MAX_OVERSCAN = Math.min(10, Math.ceil(viewportCapacity * 0.3)); // Max 30% of viewport
|
|
169
|
-
|
|
170
|
-
return Math.max(MIN_OVERSCAN, Math.min(MAX_OVERSCAN, overscan));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Calculate range size based on viewport capacity
|
|
175
|
-
*/
|
|
176
|
-
export function calculateInitialRangeSize(
|
|
177
|
-
containerSize: number,
|
|
178
|
-
estimatedItemSize: number,
|
|
179
|
-
viewportMultiplier: number = LIST_MANAGER_CONSTANTS.INITIAL_LOAD
|
|
180
|
-
.VIEWPORT_MULTIPLIER
|
|
181
|
-
): number {
|
|
182
|
-
const viewportCapacity = Math.ceil(containerSize / estimatedItemSize);
|
|
183
|
-
const rangeSize = Math.ceil(viewportCapacity * viewportMultiplier);
|
|
184
|
-
|
|
185
|
-
return Math.max(
|
|
186
|
-
LIST_MANAGER_CONSTANTS.INITIAL_LOAD.MIN_ITEMS,
|
|
187
|
-
Math.min(rangeSize, LIST_MANAGER_CONSTANTS.INITIAL_LOAD.MAX_ITEMS)
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Calculate missing ranges that need to be loaded
|
|
193
|
-
*/
|
|
194
|
-
export function calculateMissingRanges(
|
|
195
|
-
visibleRange: ItemRange,
|
|
196
|
-
loadedRanges: Set<number>,
|
|
197
|
-
rangeSize: number
|
|
198
|
-
): ItemRange[] {
|
|
199
|
-
const missingRanges: ItemRange[] = [];
|
|
200
|
-
|
|
201
|
-
const startRange = Math.floor(visibleRange.start / rangeSize);
|
|
202
|
-
const endRange = Math.floor(visibleRange.end / rangeSize);
|
|
203
|
-
|
|
204
|
-
for (let range = startRange; range <= endRange; range++) {
|
|
205
|
-
if (!loadedRanges.has(range)) {
|
|
206
|
-
const rangeStart = range * rangeSize;
|
|
207
|
-
const rangeEnd = Math.min(rangeStart + rangeSize - 1, visibleRange.end);
|
|
208
|
-
|
|
209
|
-
missingRanges.push({
|
|
210
|
-
start: rangeStart,
|
|
211
|
-
end: rangeEnd,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return missingRanges;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Calculate buffer ranges for preloading
|
|
221
|
-
*/
|
|
222
|
-
export function calculateBufferRanges(
|
|
223
|
-
visibleRange: ItemRange,
|
|
224
|
-
loadedRanges: Set<number>,
|
|
225
|
-
rangeSize: number,
|
|
226
|
-
prefetchRanges: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.PREFETCH_RANGES
|
|
227
|
-
): ItemRange[] {
|
|
228
|
-
const bufferRanges: ItemRange[] = [];
|
|
229
|
-
|
|
230
|
-
const startRange = Math.floor(visibleRange.start / rangeSize);
|
|
231
|
-
const endRange = Math.floor(visibleRange.end / rangeSize);
|
|
232
|
-
|
|
233
|
-
// Add ranges before visible range
|
|
234
|
-
for (let i = 1; i <= prefetchRanges; i++) {
|
|
235
|
-
const range = startRange - i;
|
|
236
|
-
if (range >= 0 && !loadedRanges.has(range)) {
|
|
237
|
-
const rangeStart = range * rangeSize;
|
|
238
|
-
const rangeEnd = rangeStart + rangeSize - 1;
|
|
239
|
-
|
|
240
|
-
bufferRanges.push({
|
|
241
|
-
start: rangeStart,
|
|
242
|
-
end: rangeEnd,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Add ranges after visible range
|
|
248
|
-
for (let i = 1; i <= prefetchRanges; i++) {
|
|
249
|
-
const range = endRange + i;
|
|
250
|
-
if (!loadedRanges.has(range)) {
|
|
251
|
-
const rangeStart = range * rangeSize;
|
|
252
|
-
const rangeEnd = rangeStart + rangeSize - 1;
|
|
253
|
-
|
|
254
|
-
bufferRanges.push({
|
|
255
|
-
start: rangeStart,
|
|
256
|
-
end: rangeEnd,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return bufferRanges;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Clamp value within boundaries
|
|
266
|
-
*/
|
|
267
|
-
export function clamp(value: number, min: number, max: number): number {
|
|
268
|
-
return Math.max(min, Math.min(max, value));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Apply boundary resistance to scroll position
|
|
273
|
-
*/
|
|
274
|
-
export function applyBoundaryResistance(
|
|
275
|
-
scrollPosition: number,
|
|
276
|
-
totalVirtualSize: number,
|
|
277
|
-
containerSize: number,
|
|
278
|
-
resistance: number = LIST_MANAGER_CONSTANTS.BOUNDARIES.BOUNDARY_RESISTANCE
|
|
279
|
-
): number {
|
|
280
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
281
|
-
|
|
282
|
-
// Apply resistance at boundaries
|
|
283
|
-
if (scrollPosition < 0) {
|
|
284
|
-
return scrollPosition * resistance;
|
|
285
|
-
} else if (scrollPosition > maxScroll) {
|
|
286
|
-
return maxScroll + (scrollPosition - maxScroll) * resistance;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return scrollPosition;
|
|
290
|
-
}
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Range calculation utility for pagination and data loading
|
|
3
|
-
* Handles range-based pagination strategies and loading logic
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ItemRange, RangeCalculationResult } from "../types";
|
|
7
|
-
import { LIST_MANAGER_CONSTANTS } from "../constants";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Calculate range index from item index
|
|
11
|
-
*/
|
|
12
|
-
export function calculateRangeIndex(
|
|
13
|
-
itemIndex: number,
|
|
14
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
15
|
-
): number {
|
|
16
|
-
return Math.floor(itemIndex / rangeSize);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Calculate range bounds from range index
|
|
21
|
-
*/
|
|
22
|
-
export function calculateRangeBounds(
|
|
23
|
-
rangeIndex: number,
|
|
24
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
25
|
-
): ItemRange {
|
|
26
|
-
const start = rangeIndex * rangeSize;
|
|
27
|
-
const end = start + rangeSize - 1;
|
|
28
|
-
|
|
29
|
-
return { start, end };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Calculate all required ranges for a visible range
|
|
34
|
-
*/
|
|
35
|
-
export function calculateRequiredRanges(
|
|
36
|
-
visibleRange: ItemRange,
|
|
37
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
38
|
-
): number[] {
|
|
39
|
-
const ranges: number[] = [];
|
|
40
|
-
|
|
41
|
-
const startRange = calculateRangeIndex(visibleRange.start, rangeSize);
|
|
42
|
-
const endRange = calculateRangeIndex(visibleRange.end, rangeSize);
|
|
43
|
-
|
|
44
|
-
for (let range = startRange; range <= endRange; range++) {
|
|
45
|
-
ranges.push(range);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return ranges;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Calculate missing ranges that need to be loaded
|
|
53
|
-
*/
|
|
54
|
-
export function calculateMissingRanges(
|
|
55
|
-
visibleRange: ItemRange,
|
|
56
|
-
loadedRanges: Set<number>,
|
|
57
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
58
|
-
): ItemRange[] {
|
|
59
|
-
const requiredRanges = calculateRequiredRanges(visibleRange, rangeSize);
|
|
60
|
-
const missingRanges: ItemRange[] = [];
|
|
61
|
-
|
|
62
|
-
for (const rangeIndex of requiredRanges) {
|
|
63
|
-
if (!loadedRanges.has(rangeIndex)) {
|
|
64
|
-
missingRanges.push(calculateRangeBounds(rangeIndex, rangeSize));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return missingRanges;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Calculate prefetch ranges based on scroll direction and speed
|
|
73
|
-
*/
|
|
74
|
-
export function calculatePrefetchRanges(
|
|
75
|
-
visibleRange: ItemRange,
|
|
76
|
-
loadedRanges: Set<number>,
|
|
77
|
-
direction: "forward" | "backward",
|
|
78
|
-
prefetchCount: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.PREFETCH_RANGES,
|
|
79
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
80
|
-
): ItemRange[] {
|
|
81
|
-
const prefetchRanges: ItemRange[] = [];
|
|
82
|
-
|
|
83
|
-
const startRange = calculateRangeIndex(visibleRange.start, rangeSize);
|
|
84
|
-
const endRange = calculateRangeIndex(visibleRange.end, rangeSize);
|
|
85
|
-
|
|
86
|
-
if (direction === "forward") {
|
|
87
|
-
// Prefetch ranges after visible range
|
|
88
|
-
for (let i = 1; i <= prefetchCount; i++) {
|
|
89
|
-
const rangeIndex = endRange + i;
|
|
90
|
-
if (!loadedRanges.has(rangeIndex)) {
|
|
91
|
-
prefetchRanges.push(calculateRangeBounds(rangeIndex, rangeSize));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// Prefetch ranges before visible range
|
|
96
|
-
for (let i = 1; i <= prefetchCount; i++) {
|
|
97
|
-
const rangeIndex = startRange - i;
|
|
98
|
-
if (rangeIndex >= 0 && !loadedRanges.has(rangeIndex)) {
|
|
99
|
-
prefetchRanges.push(calculateRangeBounds(rangeIndex, rangeSize));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return prefetchRanges;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Calculate comprehensive range result
|
|
109
|
-
*/
|
|
110
|
-
export function calculateRangeResult(
|
|
111
|
-
visibleRange: ItemRange,
|
|
112
|
-
loadedRanges: Set<number>,
|
|
113
|
-
direction: "forward" | "backward",
|
|
114
|
-
prefetchCount: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.PREFETCH_RANGES,
|
|
115
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
116
|
-
): RangeCalculationResult {
|
|
117
|
-
const missingRanges = calculateMissingRanges(
|
|
118
|
-
visibleRange,
|
|
119
|
-
loadedRanges,
|
|
120
|
-
rangeSize
|
|
121
|
-
);
|
|
122
|
-
const bufferRanges = calculatePrefetchRanges(
|
|
123
|
-
visibleRange,
|
|
124
|
-
loadedRanges,
|
|
125
|
-
direction,
|
|
126
|
-
prefetchCount,
|
|
127
|
-
rangeSize
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
visibleRange,
|
|
132
|
-
loadedRanges,
|
|
133
|
-
missingRanges,
|
|
134
|
-
bufferRanges,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Convert range to pagination parameters (page strategy)
|
|
140
|
-
*/
|
|
141
|
-
export function rangeToPaginationParams(
|
|
142
|
-
range: ItemRange,
|
|
143
|
-
strategy: "page" | "offset" | "cursor" = "page"
|
|
144
|
-
): { page?: number; offset?: number; limit: number } {
|
|
145
|
-
const limit = range.end - range.start + 1;
|
|
146
|
-
|
|
147
|
-
switch (strategy) {
|
|
148
|
-
case "page":
|
|
149
|
-
return {
|
|
150
|
-
page: Math.floor(range.start / limit) + 1,
|
|
151
|
-
limit,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
case "offset":
|
|
155
|
-
return {
|
|
156
|
-
offset: range.start,
|
|
157
|
-
limit,
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
case "cursor":
|
|
161
|
-
return {
|
|
162
|
-
limit,
|
|
163
|
-
// cursor would be handled separately based on last item
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
default:
|
|
167
|
-
return { page: 1, limit };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Convert multiple ranges to batch pagination parameters
|
|
173
|
-
*/
|
|
174
|
-
export function rangesToBatchParams(
|
|
175
|
-
ranges: ItemRange[],
|
|
176
|
-
strategy: "page" | "offset" | "cursor" = "page"
|
|
177
|
-
): Array<{ page?: number; offset?: number; limit: number }> {
|
|
178
|
-
return ranges.map((range) => rangeToPaginationParams(range, strategy));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Calculate optimal range size based on viewport
|
|
183
|
-
*/
|
|
184
|
-
export function calculateOptimalRangeSize(
|
|
185
|
-
containerSize: number,
|
|
186
|
-
estimatedItemSize: number,
|
|
187
|
-
targetRanges: number = 3
|
|
188
|
-
): number {
|
|
189
|
-
const viewportCapacity = Math.ceil(containerSize / estimatedItemSize);
|
|
190
|
-
const optimalRangeSize = Math.ceil(viewportCapacity / targetRanges);
|
|
191
|
-
|
|
192
|
-
return Math.max(
|
|
193
|
-
LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE,
|
|
194
|
-
optimalRangeSize
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Check if range is within viewport bounds
|
|
200
|
-
*/
|
|
201
|
-
export function isRangeInViewport(
|
|
202
|
-
range: ItemRange,
|
|
203
|
-
visibleRange: ItemRange,
|
|
204
|
-
buffer: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.BUFFER_SIZE
|
|
205
|
-
): boolean {
|
|
206
|
-
const bufferedStart = visibleRange.start - buffer;
|
|
207
|
-
const bufferedEnd = visibleRange.end + buffer;
|
|
208
|
-
|
|
209
|
-
return !(range.end < bufferedStart || range.start > bufferedEnd);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Calculate range priority based on distance from visible range
|
|
214
|
-
*/
|
|
215
|
-
export function calculateRangePriority(
|
|
216
|
-
range: ItemRange,
|
|
217
|
-
visibleRange: ItemRange
|
|
218
|
-
): number {
|
|
219
|
-
const rangeCenter = (range.start + range.end) / 2;
|
|
220
|
-
const visibleCenter = (visibleRange.start + visibleRange.end) / 2;
|
|
221
|
-
|
|
222
|
-
// Priority decreases with distance (lower number = higher priority)
|
|
223
|
-
return Math.abs(rangeCenter - visibleCenter);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Sort ranges by priority (closest to visible range first)
|
|
228
|
-
*/
|
|
229
|
-
export function sortRangesByPriority(
|
|
230
|
-
ranges: ItemRange[],
|
|
231
|
-
visibleRange: ItemRange
|
|
232
|
-
): ItemRange[] {
|
|
233
|
-
return ranges.sort((a, b) => {
|
|
234
|
-
const priorityA = calculateRangePriority(a, visibleRange);
|
|
235
|
-
const priorityB = calculateRangePriority(b, visibleRange);
|
|
236
|
-
return priorityA - priorityB;
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Merge adjacent ranges to reduce request count
|
|
242
|
-
*/
|
|
243
|
-
export function mergeAdjacentRanges(
|
|
244
|
-
ranges: ItemRange[],
|
|
245
|
-
maxGap: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
246
|
-
): ItemRange[] {
|
|
247
|
-
if (ranges.length <= 1) return ranges;
|
|
248
|
-
|
|
249
|
-
// Sort ranges by start position
|
|
250
|
-
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
|
|
251
|
-
const mergedRanges: ItemRange[] = [];
|
|
252
|
-
|
|
253
|
-
let currentRange = sortedRanges[0];
|
|
254
|
-
|
|
255
|
-
for (let i = 1; i < sortedRanges.length; i++) {
|
|
256
|
-
const nextRange = sortedRanges[i];
|
|
257
|
-
|
|
258
|
-
// Check if ranges are adjacent or close enough to merge
|
|
259
|
-
if (nextRange.start - currentRange.end <= maxGap) {
|
|
260
|
-
// Merge ranges
|
|
261
|
-
currentRange = {
|
|
262
|
-
start: currentRange.start,
|
|
263
|
-
end: Math.max(currentRange.end, nextRange.end),
|
|
264
|
-
};
|
|
265
|
-
} else {
|
|
266
|
-
// Ranges are too far apart, add current and start new
|
|
267
|
-
mergedRanges.push(currentRange);
|
|
268
|
-
currentRange = nextRange;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Add the last range
|
|
273
|
-
mergedRanges.push(currentRange);
|
|
274
|
-
|
|
275
|
-
return mergedRanges;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Calculate range cleanup candidates (far from visible range)
|
|
280
|
-
*/
|
|
281
|
-
export function calculateRangeCleanupCandidates(
|
|
282
|
-
loadedRanges: Set<number>,
|
|
283
|
-
visibleRange: ItemRange,
|
|
284
|
-
maxDistance: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING
|
|
285
|
-
.DEFAULT_RANGE_SIZE * 5,
|
|
286
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
287
|
-
): number[] {
|
|
288
|
-
const cleanupCandidates: number[] = [];
|
|
289
|
-
const visibleCenter = (visibleRange.start + visibleRange.end) / 2;
|
|
290
|
-
|
|
291
|
-
for (const rangeIndex of loadedRanges) {
|
|
292
|
-
const range = calculateRangeBounds(rangeIndex, rangeSize);
|
|
293
|
-
const rangeCenter = (range.start + range.end) / 2;
|
|
294
|
-
const distance = Math.abs(rangeCenter - visibleCenter);
|
|
295
|
-
|
|
296
|
-
if (distance > maxDistance) {
|
|
297
|
-
cleanupCandidates.push(rangeIndex);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return cleanupCandidates;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Calculate range loading order with priority
|
|
306
|
-
*/
|
|
307
|
-
export function calculateRangeLoadingOrder(
|
|
308
|
-
missingRanges: ItemRange[],
|
|
309
|
-
prefetchRanges: ItemRange[],
|
|
310
|
-
visibleRange: ItemRange
|
|
311
|
-
): { priority: "high" | "medium" | "low"; ranges: ItemRange[] }[] {
|
|
312
|
-
const highPriority = sortRangesByPriority(missingRanges, visibleRange);
|
|
313
|
-
const lowPriority = sortRangesByPriority(prefetchRanges, visibleRange);
|
|
314
|
-
|
|
315
|
-
return [
|
|
316
|
-
{ priority: "high", ranges: highPriority },
|
|
317
|
-
{ priority: "low", ranges: lowPriority },
|
|
318
|
-
];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Get range debug information
|
|
323
|
-
*/
|
|
324
|
-
export function getRangeDebugInfo(
|
|
325
|
-
visibleRange: ItemRange,
|
|
326
|
-
loadedRanges: Set<number>,
|
|
327
|
-
rangeSize: number = LIST_MANAGER_CONSTANTS.RANGE_LOADING.DEFAULT_RANGE_SIZE
|
|
328
|
-
): {
|
|
329
|
-
visibleRange: string;
|
|
330
|
-
requiredRanges: number[];
|
|
331
|
-
loadedRanges: number[];
|
|
332
|
-
missingRanges: ItemRange[];
|
|
333
|
-
rangeSize: number;
|
|
334
|
-
} {
|
|
335
|
-
const requiredRanges = calculateRequiredRanges(visibleRange, rangeSize);
|
|
336
|
-
const missingRanges = calculateMissingRanges(
|
|
337
|
-
visibleRange,
|
|
338
|
-
loadedRanges,
|
|
339
|
-
rangeSize
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
visibleRange: `${visibleRange.start}-${visibleRange.end}`,
|
|
344
|
-
requiredRanges,
|
|
345
|
-
loadedRanges: Array.from(loadedRanges).sort((a, b) => a - b),
|
|
346
|
-
missingRanges,
|
|
347
|
-
rangeSize,
|
|
348
|
-
};
|
|
349
|
-
}
|