mtrl-addons 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{src/components/index.ts → dist/components/index.d.ts} +0 -2
- package/dist/components/vlist/config.d.ts +86 -0
- package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
- package/dist/components/vlist/features/api.d.ts +7 -0
- package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
- package/dist/components/vlist/features/selection.d.ts +6 -0
- package/dist/components/vlist/features/viewport.d.ts +9 -0
- package/dist/components/vlist/features.d.ts +31 -0
- package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -10
- package/dist/components/vlist/types.d.ts +596 -0
- package/dist/components/vlist/vlist.d.ts +29 -0
- package/dist/core/compose/features/gestures/index.d.ts +86 -0
- package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
- package/dist/core/compose/features/gestures/pan.d.ts +108 -0
- package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
- package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
- package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
- package/dist/core/compose/features/gestures/tap.d.ts +79 -0
- package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
- package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
- package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
- package/dist/core/gestures/longpress.d.ts +23 -0
- package/dist/core/gestures/manager.d.ts +14 -0
- package/dist/core/gestures/pan.d.ts +12 -0
- package/dist/core/gestures/pinch.d.ts +14 -0
- package/dist/core/gestures/rotate.d.ts +14 -0
- package/dist/core/gestures/swipe.d.ts +20 -0
- package/dist/core/gestures/tap.d.ts +12 -0
- package/dist/core/gestures/types.d.ts +320 -0
- package/dist/core/gestures/utils.d.ts +57 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/layout/config.d.ts +33 -0
- package/dist/core/layout/index.d.ts +51 -0
- package/dist/core/layout/jsx.d.ts +65 -0
- package/dist/core/layout/schema.d.ts +112 -0
- package/dist/core/layout/types.d.ts +69 -0
- package/dist/core/viewport/constants.d.ts +105 -0
- package/dist/core/viewport/features/base.d.ts +14 -0
- package/dist/core/viewport/features/collection.d.ts +41 -0
- package/dist/core/viewport/features/events.d.ts +13 -0
- package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
- package/dist/core/viewport/features/item-size.d.ts +30 -0
- package/dist/core/viewport/features/loading.d.ts +34 -0
- package/dist/core/viewport/features/momentum.d.ts +17 -0
- package/dist/core/viewport/features/performance.d.ts +53 -0
- package/dist/core/viewport/features/placeholders.d.ts +38 -0
- package/dist/core/viewport/features/rendering.d.ts +16 -0
- package/dist/core/viewport/features/scrollbar.d.ts +26 -0
- package/dist/core/viewport/features/scrolling.d.ts +16 -0
- package/dist/core/viewport/features/utils.d.ts +43 -0
- package/dist/core/viewport/features/virtual.d.ts +18 -0
- package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
- package/dist/core/viewport/types.d.ts +96 -0
- package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
- package/dist/core/viewport/viewport.d.ts +11 -0
- package/{src/index.ts → dist/index.d.ts} +0 -4
- package/dist/index.js +5143 -0
- package/dist/index.mjs +5111 -0
- package/dist/styles.css +254 -0
- package/dist/styles.css.map +1 -0
- package/package.json +5 -1
- package/.cursorrules +0 -117
- package/AI.md +0 -39
- package/CLAUDE.md +0 -882
- package/build.js +0 -377
- package/scripts/analyze-orphaned-functions.ts +0 -387
- package/scripts/debug/vlist-selection.ts +0 -121
- package/src/components/vlist/config.ts +0 -323
- package/src/components/vlist/features/api.ts +0 -626
- package/src/components/vlist/features/selection.ts +0 -436
- package/src/components/vlist/features/viewport.ts +0 -59
- package/src/components/vlist/features.ts +0 -112
- package/src/components/vlist/types.ts +0 -723
- package/src/components/vlist/vlist.ts +0 -92
- package/src/core/compose/features/gestures/index.ts +0 -227
- package/src/core/compose/features/gestures/longpress.ts +0 -383
- package/src/core/compose/features/gestures/pan.ts +0 -424
- package/src/core/compose/features/gestures/pinch.ts +0 -475
- package/src/core/compose/features/gestures/rotate.ts +0 -485
- package/src/core/compose/features/gestures/swipe.ts +0 -492
- package/src/core/compose/features/gestures/tap.ts +0 -334
- package/src/core/gestures/longpress.ts +0 -68
- package/src/core/gestures/manager.ts +0 -418
- package/src/core/gestures/pan.ts +0 -48
- package/src/core/gestures/pinch.ts +0 -58
- package/src/core/gestures/rotate.ts +0 -58
- package/src/core/gestures/swipe.ts +0 -66
- package/src/core/gestures/tap.ts +0 -45
- package/src/core/gestures/types.ts +0 -387
- package/src/core/gestures/utils.ts +0 -128
- package/src/core/index.ts +0 -43
- package/src/core/layout/config.ts +0 -102
- package/src/core/layout/index.ts +0 -168
- package/src/core/layout/jsx.ts +0 -174
- package/src/core/layout/schema.ts +0 -1044
- package/src/core/layout/types.ts +0 -95
- package/src/core/viewport/constants.ts +0 -145
- package/src/core/viewport/features/base.ts +0 -73
- package/src/core/viewport/features/collection.ts +0 -1182
- package/src/core/viewport/features/events.ts +0 -130
- package/src/core/viewport/features/item-size.ts +0 -271
- package/src/core/viewport/features/loading.ts +0 -263
- package/src/core/viewport/features/momentum.ts +0 -269
- package/src/core/viewport/features/performance.ts +0 -161
- package/src/core/viewport/features/placeholders.ts +0 -335
- package/src/core/viewport/features/rendering.ts +0 -962
- package/src/core/viewport/features/scrollbar.ts +0 -434
- package/src/core/viewport/features/scrolling.ts +0 -634
- package/src/core/viewport/features/utils.ts +0 -94
- package/src/core/viewport/features/virtual.ts +0 -525
- package/src/core/viewport/types.ts +0 -133
- package/src/core/viewport/utils/speed-tracker.ts +0 -79
- package/src/core/viewport/viewport.ts +0 -265
- package/test/benchmarks/layout/advanced.test.ts +0 -656
- package/test/benchmarks/layout/comparison.test.ts +0 -519
- package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
- package/test/benchmarks/layout/real-components.test.ts +0 -733
- package/test/benchmarks/layout/simple.test.ts +0 -321
- package/test/benchmarks/layout/stress.test.ts +0 -990
- package/test/collection/basic.test.ts +0 -304
- package/test/components/vlist-selection.test.ts +0 -240
- package/test/components/vlist.test.ts +0 -63
- package/test/core/collection/adapter.test.ts +0 -161
- package/test/core/collection/collection.test.ts +0 -394
- package/test/core/layout/layout.test.ts +0 -201
- package/test/utils/dom-helpers.ts +0 -275
- package/test/utils/performance-helpers.ts +0 -392
- package/tsconfig.json +0 -20
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// src/core/viewport/features/utils.ts
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Shared utilities for viewport features
|
|
5
|
-
* Eliminates code duplication across features
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ViewportContext, ViewportComponent } from "../types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Wraps viewport initialization with feature-specific logic
|
|
12
|
-
* Eliminates the repeated initialization hook pattern
|
|
13
|
-
*/
|
|
14
|
-
export function wrapInitialize<T extends ViewportContext & ViewportComponent>(
|
|
15
|
-
component: T,
|
|
16
|
-
featureInit: () => void,
|
|
17
|
-
): void {
|
|
18
|
-
const originalInitialize = component.viewport.initialize;
|
|
19
|
-
component.viewport.initialize = () => {
|
|
20
|
-
// Check if already initialized (returns false if already done)
|
|
21
|
-
const result = originalInitialize();
|
|
22
|
-
// Only run feature init if base init actually ran (didn't return false)
|
|
23
|
-
if (result !== false) {
|
|
24
|
-
featureInit();
|
|
25
|
-
}
|
|
26
|
-
// Propagate the result through the chain so outer wrappers know to skip
|
|
27
|
-
return result;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Wraps component destroy with cleanup logic
|
|
33
|
-
* Eliminates the repeated destroy hook pattern
|
|
34
|
-
*/
|
|
35
|
-
export function wrapDestroy<T extends Record<string, any>>(
|
|
36
|
-
component: T,
|
|
37
|
-
cleanup: () => void,
|
|
38
|
-
): void {
|
|
39
|
-
if ("destroy" in component && typeof component.destroy === "function") {
|
|
40
|
-
const originalDestroy = component.destroy;
|
|
41
|
-
(component as any).destroy = () => {
|
|
42
|
-
cleanup();
|
|
43
|
-
originalDestroy?.();
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Gets viewport state with proper typing
|
|
50
|
-
* Eliminates repeated (component.viewport as any).state
|
|
51
|
-
*/
|
|
52
|
-
export function getViewportState(component: ViewportComponent): any {
|
|
53
|
-
return (component.viewport as any).state;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Checks if an item is a placeholder
|
|
58
|
-
* Eliminates duplicated placeholder detection logic
|
|
59
|
-
*/
|
|
60
|
-
export function isPlaceholder(item: any): boolean {
|
|
61
|
-
return (
|
|
62
|
-
item &&
|
|
63
|
-
typeof item === "object" &&
|
|
64
|
-
(item._placeholder === true || item["_placeholder"] === true) // Support both access patterns
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Creates a range key for deduplication
|
|
70
|
-
* Used by multiple features for tracking ranges
|
|
71
|
-
*/
|
|
72
|
-
export function getRangeKey(range: { start: number; end: number }): string {
|
|
73
|
-
return `${range.start}-${range.end}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Clamps a value between min and max
|
|
78
|
-
* Used by multiple features for boundary checking
|
|
79
|
-
*/
|
|
80
|
-
export function clamp(value: number, min: number, max: number): number {
|
|
81
|
-
return Math.max(min, Math.min(max, value));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Stores a function reference on the component for later access
|
|
86
|
-
* Eliminates pattern like (component as any)._featureFunction = fn
|
|
87
|
-
*/
|
|
88
|
-
export function storeFeatureFunction<T extends Record<string, any>>(
|
|
89
|
-
component: T,
|
|
90
|
-
name: string,
|
|
91
|
-
fn: Function,
|
|
92
|
-
): void {
|
|
93
|
-
(component as any)[name] = fn;
|
|
94
|
-
}
|
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Virtual Feature - Core virtual scrolling calculations
|
|
3
|
-
* Handles visible range calculation and total virtual size management
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ViewportContext, ViewportComponent } from "../types";
|
|
7
|
-
import { VIEWPORT_CONSTANTS } from "../constants";
|
|
8
|
-
import { wrapInitialize, getViewportState } from "./utils";
|
|
9
|
-
|
|
10
|
-
export interface VirtualConfig {
|
|
11
|
-
itemSize?: number;
|
|
12
|
-
overscan?: number;
|
|
13
|
-
orientation?: "vertical" | "horizontal";
|
|
14
|
-
autoDetectItemSize?: boolean;
|
|
15
|
-
debug?: boolean;
|
|
16
|
-
initialScrollIndex?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Virtual scrolling feature for viewport
|
|
21
|
-
* Handles visible range calculations with compression for large datasets
|
|
22
|
-
*/
|
|
23
|
-
export const withVirtual = (config: VirtualConfig = {}) => {
|
|
24
|
-
return <T extends ViewportContext & ViewportComponent>(component: T): T => {
|
|
25
|
-
const {
|
|
26
|
-
itemSize,
|
|
27
|
-
overscan = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.OVERSCAN_BUFFER,
|
|
28
|
-
orientation = "vertical",
|
|
29
|
-
autoDetectItemSize = itemSize === undefined
|
|
30
|
-
? VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.AUTO_DETECT_ITEM_SIZE
|
|
31
|
-
: false,
|
|
32
|
-
debug = false,
|
|
33
|
-
initialScrollIndex = 0,
|
|
34
|
-
} = config;
|
|
35
|
-
|
|
36
|
-
// Use provided itemSize or default, but mark if we should auto-detect
|
|
37
|
-
const initialItemSize =
|
|
38
|
-
itemSize || VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.DEFAULT_ITEM_SIZE;
|
|
39
|
-
|
|
40
|
-
const MAX_VIRTUAL_SIZE = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
|
|
41
|
-
let viewportState: any;
|
|
42
|
-
let hasCalculatedItemSize = false;
|
|
43
|
-
let hasRecalculatedScrollForCompression = false; // Track if we've recalculated scroll position for compression
|
|
44
|
-
|
|
45
|
-
// Initialize using shared wrapper
|
|
46
|
-
wrapInitialize(component, () => {
|
|
47
|
-
viewportState = getViewportState(component);
|
|
48
|
-
if (!viewportState) return;
|
|
49
|
-
|
|
50
|
-
Object.assign(viewportState, {
|
|
51
|
-
itemSize: initialItemSize,
|
|
52
|
-
overscan,
|
|
53
|
-
containerSize:
|
|
54
|
-
viewportState.viewportElement?.[
|
|
55
|
-
orientation === "horizontal" ? "offsetWidth" : "offsetHeight"
|
|
56
|
-
] || 600,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
updateTotalVirtualSize(viewportState.totalItems);
|
|
60
|
-
|
|
61
|
-
// If we have an initial scroll index, calculate initial scroll position
|
|
62
|
-
// Note: We store the initialScrollIndex to use directly in range calculations
|
|
63
|
-
// because scroll position may be compressed for large lists
|
|
64
|
-
let initialScrollPosition = viewportState.scrollPosition || 0;
|
|
65
|
-
|
|
66
|
-
if (initialScrollIndex > 0) {
|
|
67
|
-
// Store the target index for use in range calculations
|
|
68
|
-
(viewportState as any).targetScrollIndex = initialScrollIndex;
|
|
69
|
-
|
|
70
|
-
// Calculate scroll position (may be compressed for large lists)
|
|
71
|
-
initialScrollPosition =
|
|
72
|
-
initialScrollIndex * (viewportState.itemSize || initialItemSize);
|
|
73
|
-
viewportState.scrollPosition = initialScrollPosition;
|
|
74
|
-
|
|
75
|
-
// Notify scrolling feature to sync its local scroll position
|
|
76
|
-
// Use setTimeout to ensure scrolling feature has initialized
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
component.emit?.("viewport:scroll-position-sync", {
|
|
79
|
-
position: initialScrollPosition,
|
|
80
|
-
source: "initial-scroll-index",
|
|
81
|
-
});
|
|
82
|
-
}, 0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
updateVisibleRange(initialScrollPosition);
|
|
86
|
-
|
|
87
|
-
// Ensure container size is measured after DOM is ready
|
|
88
|
-
requestAnimationFrame(() => {
|
|
89
|
-
updateContainerSize();
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Helper functions
|
|
94
|
-
const getCompressionRatio = (): number => {
|
|
95
|
-
if (!viewportState?.virtualTotalSize) return 1;
|
|
96
|
-
const actualSize = viewportState.totalItems * viewportState.itemSize;
|
|
97
|
-
return actualSize <= MAX_VIRTUAL_SIZE ? 1 : MAX_VIRTUAL_SIZE / actualSize;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const log = (message: string, data?: any) => {
|
|
101
|
-
if (debug) console.log(`[Virtual] ${message}`, data);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Calculate visible range
|
|
105
|
-
const calculateVisibleRange = (
|
|
106
|
-
scrollPosition: number,
|
|
107
|
-
): { start: number; end: number } => {
|
|
108
|
-
if (!viewportState) {
|
|
109
|
-
console.warn("[Virtual] No viewport state, returning empty range");
|
|
110
|
-
return { start: 0, end: 0 };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const { containerSize, totalItems } = viewportState;
|
|
114
|
-
|
|
115
|
-
// Early returns for invalid states
|
|
116
|
-
if (
|
|
117
|
-
!containerSize ||
|
|
118
|
-
containerSize <= 0 ||
|
|
119
|
-
!totalItems ||
|
|
120
|
-
totalItems <= 0
|
|
121
|
-
) {
|
|
122
|
-
// If we have an initialScrollIndex, use it to calculate the range
|
|
123
|
-
// even when totalItems is 0 (not yet loaded from API)
|
|
124
|
-
if (initialScrollIndex > 0) {
|
|
125
|
-
const visibleCount = Math.ceil(
|
|
126
|
-
(containerSize || 600) /
|
|
127
|
-
(viewportState.itemSize || initialItemSize),
|
|
128
|
-
);
|
|
129
|
-
const start = Math.max(0, initialScrollIndex - overscan);
|
|
130
|
-
const end = initialScrollIndex + visibleCount + overscan;
|
|
131
|
-
return { start, end };
|
|
132
|
-
}
|
|
133
|
-
log(`Invalid state: container=${containerSize}, items=${totalItems}`);
|
|
134
|
-
return { start: 0, end: 0 };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const virtualSize =
|
|
138
|
-
viewportState.virtualTotalSize || totalItems * viewportState.itemSize;
|
|
139
|
-
const visibleCount = Math.ceil(containerSize / viewportState.itemSize);
|
|
140
|
-
const compressionRatio = getCompressionRatio();
|
|
141
|
-
|
|
142
|
-
let start: number, end: number;
|
|
143
|
-
|
|
144
|
-
// Check if we have a target scroll index (for initialScrollIndex with compression)
|
|
145
|
-
const targetScrollIndex = (viewportState as any).targetScrollIndex;
|
|
146
|
-
|
|
147
|
-
if (compressionRatio < 1) {
|
|
148
|
-
// Compressed space calculation
|
|
149
|
-
// If we have a targetScrollIndex, use it directly instead of calculating from scroll position
|
|
150
|
-
// This ensures we show the correct items even when virtual space is compressed
|
|
151
|
-
if (targetScrollIndex !== undefined && targetScrollIndex > 0) {
|
|
152
|
-
start = Math.max(0, targetScrollIndex - overscan);
|
|
153
|
-
end = Math.min(
|
|
154
|
-
totalItems - 1,
|
|
155
|
-
targetScrollIndex + visibleCount + overscan,
|
|
156
|
-
);
|
|
157
|
-
// Clear targetScrollIndex after first use so normal scrolling works
|
|
158
|
-
delete (viewportState as any).targetScrollIndex;
|
|
159
|
-
} else {
|
|
160
|
-
const scrollRatio = scrollPosition / virtualSize;
|
|
161
|
-
const exactIndex = scrollRatio * totalItems;
|
|
162
|
-
start = Math.floor(exactIndex);
|
|
163
|
-
end = Math.ceil(exactIndex) + visibleCount;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Near-bottom handling
|
|
167
|
-
const maxScroll = virtualSize - containerSize;
|
|
168
|
-
const distanceFromBottom = maxScroll - scrollPosition;
|
|
169
|
-
|
|
170
|
-
if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
|
|
171
|
-
const itemsAtBottom = Math.floor(
|
|
172
|
-
containerSize / viewportState.itemSize,
|
|
173
|
-
);
|
|
174
|
-
const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
|
|
175
|
-
const interpolation = Math.max(
|
|
176
|
-
0,
|
|
177
|
-
Math.min(1, 1 - distanceFromBottom / containerSize),
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
start = Math.floor(
|
|
181
|
-
start + (firstVisibleAtBottom - start) * interpolation,
|
|
182
|
-
);
|
|
183
|
-
end =
|
|
184
|
-
distanceFromBottom <= 1
|
|
185
|
-
? totalItems - 1
|
|
186
|
-
: Math.min(totalItems - 1, start + visibleCount + overscan);
|
|
187
|
-
|
|
188
|
-
log("Near bottom calculation:", {
|
|
189
|
-
distanceFromBottom,
|
|
190
|
-
interpolation,
|
|
191
|
-
start,
|
|
192
|
-
end,
|
|
193
|
-
scrollPosition,
|
|
194
|
-
totalItems,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Apply overscan
|
|
199
|
-
start = Math.max(0, start - overscan);
|
|
200
|
-
end = Math.min(totalItems - 1, end + overscan);
|
|
201
|
-
} else {
|
|
202
|
-
// Direct calculation
|
|
203
|
-
start = Math.max(
|
|
204
|
-
0,
|
|
205
|
-
Math.floor(scrollPosition / viewportState.itemSize) - overscan,
|
|
206
|
-
);
|
|
207
|
-
end = Math.min(totalItems - 1, start + visibleCount + overscan * 2);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Validate output
|
|
211
|
-
if (isNaN(start) || isNaN(end)) {
|
|
212
|
-
console.error("[Virtual] NaN in range calculation:", {
|
|
213
|
-
scrollPosition,
|
|
214
|
-
containerSize,
|
|
215
|
-
totalItems,
|
|
216
|
-
itemSize: viewportState.itemSize,
|
|
217
|
-
compressionRatio,
|
|
218
|
-
});
|
|
219
|
-
return { start: 0, end: 0 };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// log(`Range: ${start}-${end} (scroll: ${scrollPosition})`);
|
|
223
|
-
|
|
224
|
-
// Strategic log for last range
|
|
225
|
-
// if (end >= totalItems - 10) {
|
|
226
|
-
// console.log(
|
|
227
|
-
// `[Virtual] Near end range: ${start}-${end}, totalItems=${totalItems}, lastItemPos=${
|
|
228
|
-
// end * viewportState.itemSize
|
|
229
|
-
// }px, virtualSize=${viewportState.virtualTotalSize}px`
|
|
230
|
-
// );
|
|
231
|
-
// }
|
|
232
|
-
|
|
233
|
-
return { start, end };
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
// Calculate actual visible range (without overscan buffer)
|
|
237
|
-
const calculateActualVisibleRange = (scrollPosition: number) => {
|
|
238
|
-
if (!viewportState) return { start: 0, end: 0 };
|
|
239
|
-
|
|
240
|
-
const { containerSize, totalItems } = viewportState;
|
|
241
|
-
if (!containerSize || !totalItems) return { start: 0, end: 0 };
|
|
242
|
-
|
|
243
|
-
const itemSize = viewportState.itemSize;
|
|
244
|
-
const visibleCount = Math.ceil(containerSize / itemSize);
|
|
245
|
-
const compressionRatio = viewportState.virtualTotalSize
|
|
246
|
-
? (totalItems * itemSize) / viewportState.virtualTotalSize
|
|
247
|
-
: 1;
|
|
248
|
-
|
|
249
|
-
let start: number, end: number;
|
|
250
|
-
|
|
251
|
-
if (compressionRatio < 1) {
|
|
252
|
-
// Compressed space - calculate based on scroll ratio
|
|
253
|
-
const virtualSize =
|
|
254
|
-
viewportState.virtualTotalSize || totalItems * itemSize;
|
|
255
|
-
const scrollRatio = scrollPosition / virtualSize;
|
|
256
|
-
start = Math.floor(scrollRatio * totalItems);
|
|
257
|
-
end = Math.min(totalItems - 1, start + visibleCount - 1);
|
|
258
|
-
} else {
|
|
259
|
-
// Direct calculation
|
|
260
|
-
start = Math.floor(scrollPosition / itemSize);
|
|
261
|
-
end = Math.min(totalItems - 1, start + visibleCount - 1);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Ensure valid range
|
|
265
|
-
start = Math.max(0, start);
|
|
266
|
-
end = Math.max(start, end);
|
|
267
|
-
|
|
268
|
-
return { start, end };
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Update functions
|
|
272
|
-
const updateVisibleRange = (scrollPosition: number) => {
|
|
273
|
-
if (!viewportState) return;
|
|
274
|
-
viewportState.visibleRange = calculateVisibleRange(scrollPosition);
|
|
275
|
-
|
|
276
|
-
// DEBUG: Log who calls updateVisibleRange with 0 when it should be non-zero
|
|
277
|
-
|
|
278
|
-
// Calculate actual visible range (without overscan) for UI display
|
|
279
|
-
const actualVisibleRange = calculateActualVisibleRange(scrollPosition);
|
|
280
|
-
|
|
281
|
-
component.emit?.("viewport:range-changed", {
|
|
282
|
-
range: viewportState.visibleRange,
|
|
283
|
-
visibleRange: actualVisibleRange,
|
|
284
|
-
scrollPosition,
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// Update total virtual size
|
|
289
|
-
const updateTotalVirtualSize = (totalItems: number) => {
|
|
290
|
-
if (!viewportState) return;
|
|
291
|
-
|
|
292
|
-
const oldSize = viewportState.virtualTotalSize;
|
|
293
|
-
viewportState.totalItems = totalItems;
|
|
294
|
-
const actualSize = totalItems * viewportState.itemSize;
|
|
295
|
-
|
|
296
|
-
// Get padding from the items container if available
|
|
297
|
-
let totalPadding = 0;
|
|
298
|
-
if (viewportState.itemsContainer) {
|
|
299
|
-
const computedStyle = window.getComputedStyle(
|
|
300
|
-
viewportState.itemsContainer,
|
|
301
|
-
);
|
|
302
|
-
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
|
|
303
|
-
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
|
304
|
-
totalPadding = paddingTop + paddingBottom;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Include padding in the virtual size
|
|
308
|
-
viewportState.virtualTotalSize = Math.min(
|
|
309
|
-
actualSize + totalPadding,
|
|
310
|
-
MAX_VIRTUAL_SIZE,
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
// Strategic log for debugging gap issue
|
|
314
|
-
// console.log(
|
|
315
|
-
// `[Virtual] Total size update: items=${totalItems}, itemSize=${viewportState.itemSize}px, padding=${totalPadding}px, virtualSize=${viewportState.virtualTotalSize}px (was ${oldSize}px)`
|
|
316
|
-
// );
|
|
317
|
-
|
|
318
|
-
component.emit?.("viewport:virtual-size-changed", {
|
|
319
|
-
totalVirtualSize: viewportState.virtualTotalSize,
|
|
320
|
-
totalItems: totalItems,
|
|
321
|
-
compressionRatio: getCompressionRatio(),
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// Update container size
|
|
326
|
-
const updateContainerSize = () => {
|
|
327
|
-
if (!viewportState || !viewportState.viewportElement) return;
|
|
328
|
-
|
|
329
|
-
const size =
|
|
330
|
-
viewportState.viewportElement[
|
|
331
|
-
viewportState.orientation === "horizontal"
|
|
332
|
-
? "offsetWidth"
|
|
333
|
-
: "offsetHeight"
|
|
334
|
-
];
|
|
335
|
-
|
|
336
|
-
// Log the actual measurement
|
|
337
|
-
// console.log(
|
|
338
|
-
// `[Virtual] Container size measured: ${size}px from viewport element`
|
|
339
|
-
// );
|
|
340
|
-
|
|
341
|
-
if (size !== viewportState.containerSize) {
|
|
342
|
-
viewportState.containerSize = size;
|
|
343
|
-
updateVisibleRange(viewportState.scrollPosition || 0);
|
|
344
|
-
updateTotalVirtualSize(viewportState.totalItems); // Also update virtual size
|
|
345
|
-
component.emit?.("viewport:container-size-changed", {
|
|
346
|
-
containerSize: size,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Override viewport methods
|
|
352
|
-
component.viewport.getVisibleRange = () =>
|
|
353
|
-
viewportState?.visibleRange ||
|
|
354
|
-
calculateVisibleRange(viewportState?.scrollPosition || 0);
|
|
355
|
-
|
|
356
|
-
// Event listeners
|
|
357
|
-
component.on?.("viewport:scroll", (data: any) =>
|
|
358
|
-
updateVisibleRange(data.position),
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
component.on?.("viewport:items-changed", (data: any) => {
|
|
362
|
-
if (data.totalItems !== undefined) {
|
|
363
|
-
updateTotalVirtualSize(data.totalItems);
|
|
364
|
-
updateVisibleRange(viewportState?.scrollPosition || 0);
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Listen for total items changes (important for cursor pagination)
|
|
369
|
-
// Note: setTotalItems() updates viewportState.totalItems BEFORE emitting this event,
|
|
370
|
-
// so we can't rely on viewportState.totalItems to detect the first total.
|
|
371
|
-
// Instead we use the hasRecalculatedScrollForCompression flag.
|
|
372
|
-
component.on?.("viewport:total-items-changed", (data: any) => {
|
|
373
|
-
if (data.total === undefined) return;
|
|
374
|
-
|
|
375
|
-
// FIX: When we first receive totalItems with initialScrollIndex and compression,
|
|
376
|
-
// recalculate scroll position to account for compression.
|
|
377
|
-
// Without this, initialScrollIndex calculates position as index * itemSize,
|
|
378
|
-
// but rendering uses compressed space, causing items to appear off-screen.
|
|
379
|
-
//
|
|
380
|
-
// We check this BEFORE updateTotalVirtualSize because we need to recalculate
|
|
381
|
-
// scroll position based on the new total, and the flag ensures we only do this once.
|
|
382
|
-
if (
|
|
383
|
-
initialScrollIndex > 0 &&
|
|
384
|
-
!hasRecalculatedScrollForCompression &&
|
|
385
|
-
data.total > 0
|
|
386
|
-
) {
|
|
387
|
-
const actualTotalSize = data.total * viewportState.itemSize;
|
|
388
|
-
const isCompressed = actualTotalSize > MAX_VIRTUAL_SIZE;
|
|
389
|
-
|
|
390
|
-
if (isCompressed) {
|
|
391
|
-
// Recalculate scroll position using compression-aware formula
|
|
392
|
-
// Same formula used in scrollToIndex
|
|
393
|
-
const ratio = initialScrollIndex / data.total;
|
|
394
|
-
const compressedPosition = ratio * MAX_VIRTUAL_SIZE;
|
|
395
|
-
|
|
396
|
-
viewportState.scrollPosition = compressedPosition;
|
|
397
|
-
|
|
398
|
-
// Notify scrolling feature to sync its local scroll position
|
|
399
|
-
component.emit?.("viewport:scroll-position-sync", {
|
|
400
|
-
position: compressedPosition,
|
|
401
|
-
source: "compression-recalculation",
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// Clear targetScrollIndex since we've now correctly calculated the scroll position
|
|
405
|
-
// The normal compressed scrolling formula will now work correctly
|
|
406
|
-
delete (viewportState as any).targetScrollIndex;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Mark as done even if not compressed - we only want to do this check once
|
|
410
|
-
hasRecalculatedScrollForCompression = true;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
updateTotalVirtualSize(data.total);
|
|
414
|
-
updateVisibleRange(viewportState?.scrollPosition || 0);
|
|
415
|
-
|
|
416
|
-
// Trigger a render to update the view
|
|
417
|
-
component.viewport?.renderItems?.();
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Listen for container size changes to recalculate virtual size
|
|
421
|
-
component.on?.("viewport:container-size-changed", (data: any) => {
|
|
422
|
-
if (viewportState && data.containerSize !== viewportState.containerSize) {
|
|
423
|
-
viewportState.containerSize = data.containerSize;
|
|
424
|
-
// Recalculate virtual size with new container size
|
|
425
|
-
updateTotalVirtualSize(viewportState.totalItems);
|
|
426
|
-
updateVisibleRange(viewportState.scrollPosition || 0);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
component.on?.("collection:range-loaded", (data: any) => {
|
|
431
|
-
if (
|
|
432
|
-
data.total !== undefined &&
|
|
433
|
-
data.total !== viewportState?.totalItems
|
|
434
|
-
) {
|
|
435
|
-
updateTotalVirtualSize(data.total);
|
|
436
|
-
}
|
|
437
|
-
updateVisibleRange(viewportState?.scrollPosition || 0);
|
|
438
|
-
component.viewport?.renderItems?.();
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// Listen for first items rendered to calculate item size (if auto-detection is enabled)
|
|
442
|
-
if (autoDetectItemSize) {
|
|
443
|
-
component.on?.("viewport:items-rendered", (data: any) => {
|
|
444
|
-
if (
|
|
445
|
-
!hasCalculatedItemSize &&
|
|
446
|
-
data.elements?.length > 0 &&
|
|
447
|
-
viewportState
|
|
448
|
-
) {
|
|
449
|
-
// Calculate average item size from first rendered batch
|
|
450
|
-
const sizes: number[] = [];
|
|
451
|
-
const sizeProperty =
|
|
452
|
-
orientation === "horizontal" ? "offsetWidth" : "offsetHeight";
|
|
453
|
-
|
|
454
|
-
data.elements.forEach((element: HTMLElement) => {
|
|
455
|
-
const size = element[sizeProperty];
|
|
456
|
-
if (size > 0) {
|
|
457
|
-
sizes.push(size);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
if (sizes.length > 0) {
|
|
462
|
-
const avgSize = Math.round(
|
|
463
|
-
sizes.reduce((sum, size) => sum + size, 0) / sizes.length,
|
|
464
|
-
);
|
|
465
|
-
const previousItemSize = viewportState.itemSize;
|
|
466
|
-
viewportState.itemSize = avgSize;
|
|
467
|
-
|
|
468
|
-
// If we have an initialScrollIndex, recalculate scroll position
|
|
469
|
-
// based on the newly detected item size
|
|
470
|
-
if (initialScrollIndex > 0 && avgSize !== previousItemSize) {
|
|
471
|
-
const newScrollPosition = initialScrollIndex * avgSize;
|
|
472
|
-
viewportState.scrollPosition = newScrollPosition;
|
|
473
|
-
|
|
474
|
-
// Notify scrolling feature to sync its local scroll position
|
|
475
|
-
component.emit?.("viewport:scroll-position-sync", {
|
|
476
|
-
position: newScrollPosition,
|
|
477
|
-
source: "item-size-detected",
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Recalculate everything with new size
|
|
482
|
-
updateTotalVirtualSize(viewportState.totalItems);
|
|
483
|
-
updateVisibleRange(viewportState.scrollPosition || 0);
|
|
484
|
-
|
|
485
|
-
// Re-render to adjust positions
|
|
486
|
-
component.viewport?.renderItems?.();
|
|
487
|
-
|
|
488
|
-
// Emit event for size change
|
|
489
|
-
component.emit?.("viewport:item-size-detected", {
|
|
490
|
-
previousSize: initialItemSize,
|
|
491
|
-
detectedSize: avgSize,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
hasCalculatedItemSize = true;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Expose virtual API
|
|
501
|
-
const compressionRatio = getCompressionRatio();
|
|
502
|
-
(component as any).virtual = {
|
|
503
|
-
calculateVisibleRange,
|
|
504
|
-
updateTotalVirtualSize,
|
|
505
|
-
getTotalVirtualSize: () => viewportState?.virtualTotalSize || 0,
|
|
506
|
-
getContainerSize: () => viewportState?.containerSize || 0,
|
|
507
|
-
updateContainerSize: (size: number) => {
|
|
508
|
-
if (viewportState) {
|
|
509
|
-
viewportState.containerSize = size;
|
|
510
|
-
updateVisibleRange(viewportState.scrollPosition || 0);
|
|
511
|
-
}
|
|
512
|
-
},
|
|
513
|
-
getItemSize: () => viewportState?.itemSize || initialItemSize,
|
|
514
|
-
calculateIndexFromPosition: (position: number) =>
|
|
515
|
-
Math.floor(
|
|
516
|
-
position /
|
|
517
|
-
((viewportState?.itemSize || initialItemSize) * compressionRatio),
|
|
518
|
-
),
|
|
519
|
-
calculatePositionForIndex: (index: number) =>
|
|
520
|
-
index * (viewportState?.itemSize || initialItemSize) * compressionRatio,
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
return component;
|
|
524
|
-
};
|
|
525
|
-
};
|