mtrl-addons 0.2.2 → 0.2.4
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 +16 -2
- package/.cursorrules +0 -117
- package/AI.md +0 -39
- package/CLAUDE.md +0 -882
- package/build.js +0 -377
- package/index.ts +0 -7
- 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,634 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scrolling Feature - Virtual scrolling with integrated velocity tracking
|
|
3
|
-
* Handles wheel events, touch/mouse events, scroll position management, velocity measurement, and momentum scrolling
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ViewportContext, ViewportComponent } from "../types";
|
|
7
|
-
import { VIEWPORT_CONSTANTS } from "../constants";
|
|
8
|
-
import { wrapInitialize, getViewportState, clamp } from "./utils";
|
|
9
|
-
|
|
10
|
-
export interface ScrollingConfig {
|
|
11
|
-
orientation?: "vertical" | "horizontal";
|
|
12
|
-
sensitivity?: number;
|
|
13
|
-
smoothing?: boolean;
|
|
14
|
-
idleTimeout?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Speed tracker interface
|
|
18
|
-
interface SpeedTracker {
|
|
19
|
-
velocity: number;
|
|
20
|
-
lastPosition: number;
|
|
21
|
-
lastTime: number;
|
|
22
|
-
direction: "forward" | "backward";
|
|
23
|
-
samples: Array<{ position: number; time: number }>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Creates a new speed tracker
|
|
28
|
-
*/
|
|
29
|
-
const createSpeedTracker = (): SpeedTracker => ({
|
|
30
|
-
velocity: 0,
|
|
31
|
-
lastPosition: 0,
|
|
32
|
-
lastTime: Date.now(),
|
|
33
|
-
direction: "forward",
|
|
34
|
-
samples: [],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Updates speed tracker with new position
|
|
39
|
-
*/
|
|
40
|
-
const updateSpeedTracker = (
|
|
41
|
-
tracker: SpeedTracker,
|
|
42
|
-
newPosition: number,
|
|
43
|
-
previousPosition: number,
|
|
44
|
-
): SpeedTracker => {
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const timeDelta = now - tracker.lastTime;
|
|
47
|
-
|
|
48
|
-
if (timeDelta === 0) return tracker;
|
|
49
|
-
|
|
50
|
-
const positionDelta = newPosition - previousPosition;
|
|
51
|
-
const instantVelocity = Math.abs(positionDelta) / timeDelta;
|
|
52
|
-
|
|
53
|
-
// Add new sample
|
|
54
|
-
const samples = [...tracker.samples, { position: newPosition, time: now }];
|
|
55
|
-
|
|
56
|
-
// Keep only recent samples (last 100ms)
|
|
57
|
-
const recentSamples = samples.filter((s) => now - s.time < 100);
|
|
58
|
-
|
|
59
|
-
// Calculate average velocity from recent samples
|
|
60
|
-
let avgVelocity = instantVelocity;
|
|
61
|
-
if (recentSamples.length > 1) {
|
|
62
|
-
const oldestSample = recentSamples[0];
|
|
63
|
-
const totalDistance = Math.abs(newPosition - oldestSample.position);
|
|
64
|
-
const totalTime = now - oldestSample.time;
|
|
65
|
-
avgVelocity = totalTime > 0 ? totalDistance / totalTime : instantVelocity;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
velocity: avgVelocity,
|
|
70
|
-
lastPosition: newPosition,
|
|
71
|
-
lastTime: now,
|
|
72
|
-
direction: positionDelta >= 0 ? "forward" : "backward",
|
|
73
|
-
samples: recentSamples,
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Scrolling feature for viewport
|
|
79
|
-
* Handles wheel events, velocity tracking, and idle detection
|
|
80
|
-
*/
|
|
81
|
-
export const withScrolling = (config: ScrollingConfig = {}) => {
|
|
82
|
-
return <T extends ViewportContext & ViewportComponent>(component: T): T => {
|
|
83
|
-
const {
|
|
84
|
-
orientation = "vertical",
|
|
85
|
-
sensitivity = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.SCROLL_SENSITIVITY,
|
|
86
|
-
smoothing = false,
|
|
87
|
-
idleTimeout = 100, // Default idle timeout in ms
|
|
88
|
-
} = config;
|
|
89
|
-
|
|
90
|
-
// State
|
|
91
|
-
let scrollPosition = 0;
|
|
92
|
-
let totalVirtualSize = 0;
|
|
93
|
-
let containerSize = 0;
|
|
94
|
-
let isScrolling = false;
|
|
95
|
-
let lastScrollTime = 0;
|
|
96
|
-
let speedTracker = createSpeedTracker();
|
|
97
|
-
let idleTimeoutId: number | null = null;
|
|
98
|
-
let idleCheckFrame: number | null = null;
|
|
99
|
-
let lastIdleCheckPosition = 0;
|
|
100
|
-
let hasEmittedIdle = false; // Track if we've already emitted idle
|
|
101
|
-
|
|
102
|
-
// console.log(`[Scrolling] Initial state - position: ${scrollPosition}`);
|
|
103
|
-
|
|
104
|
-
// Get viewport state
|
|
105
|
-
let viewportState: any;
|
|
106
|
-
|
|
107
|
-
// Use shared initialization wrapper
|
|
108
|
-
wrapInitialize(component, () => {
|
|
109
|
-
viewportState = getViewportState(component);
|
|
110
|
-
|
|
111
|
-
// Initialize state values
|
|
112
|
-
if (viewportState) {
|
|
113
|
-
totalVirtualSize = viewportState.virtualTotalSize || 0;
|
|
114
|
-
containerSize = viewportState.containerSize || 0;
|
|
115
|
-
// Sync scrollPosition from viewportState (important for initialScrollIndex)
|
|
116
|
-
if (viewportState.scrollPosition > 0) {
|
|
117
|
-
scrollPosition = viewportState.scrollPosition;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Listen for virtual size changes
|
|
122
|
-
component.on?.("viewport:virtual-size-changed", (data: any) => {
|
|
123
|
-
// console.log("[Scrolling] Virtual size changed:", data);
|
|
124
|
-
updateScrollBounds(data.totalVirtualSize, containerSize);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Listen for scroll position changes from other features (e.g., virtual.ts after item size detection)
|
|
128
|
-
component.on?.("viewport:scroll-position-sync", (data: any) => {
|
|
129
|
-
if (data.position !== undefined && data.position !== scrollPosition) {
|
|
130
|
-
scrollPosition = data.position;
|
|
131
|
-
// Also update local tracking vars
|
|
132
|
-
totalVirtualSize =
|
|
133
|
-
viewportState?.virtualTotalSize || totalVirtualSize;
|
|
134
|
-
containerSize = viewportState?.containerSize || containerSize;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Listen for container size changes
|
|
139
|
-
component.on?.("viewport:container-size-changed", (data: any) => {
|
|
140
|
-
if (data.containerSize) {
|
|
141
|
-
containerSize = data.containerSize;
|
|
142
|
-
updateScrollBounds(totalVirtualSize, containerSize);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Attach wheel event listener to viewport element
|
|
147
|
-
const viewportElement =
|
|
148
|
-
viewportState?.viewportElement || (component as any).viewportElement;
|
|
149
|
-
if (viewportElement) {
|
|
150
|
-
// console.log(`[Scrolling] Attaching wheel event to viewport element`);
|
|
151
|
-
viewportElement.addEventListener("wheel", handleWheel, {
|
|
152
|
-
passive: false,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Store reference for cleanup
|
|
156
|
-
(component as any)._scrollingViewportElement = viewportElement;
|
|
157
|
-
} else {
|
|
158
|
-
console.warn(`[Scrolling] No viewport element found for wheel events`);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Use clamp from utils
|
|
163
|
-
|
|
164
|
-
// Start idle detection
|
|
165
|
-
const startIdleDetection = () => {
|
|
166
|
-
// console.log("[Scrolling] Starting idle detection");
|
|
167
|
-
|
|
168
|
-
// Stop any existing idle detection first
|
|
169
|
-
if (idleCheckFrame !== null) {
|
|
170
|
-
cancelAnimationFrame(idleCheckFrame);
|
|
171
|
-
idleCheckFrame = null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
hasEmittedIdle = false; // Reset idle emission flag
|
|
175
|
-
const checkIdle = () => {
|
|
176
|
-
if (scrollPosition === lastIdleCheckPosition) {
|
|
177
|
-
// Position hasn't changed - we're idle
|
|
178
|
-
if (!hasEmittedIdle && (speedTracker.velocity > 0 || isScrolling)) {
|
|
179
|
-
// console.log(
|
|
180
|
-
// "[Scrolling] Idle detected - position stable, setting velocity to zero"
|
|
181
|
-
// );
|
|
182
|
-
hasEmittedIdle = true; // Mark that we've emitted idle
|
|
183
|
-
setVelocityToZero();
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
// Position changed, reset the flag
|
|
187
|
-
hasEmittedIdle = false;
|
|
188
|
-
}
|
|
189
|
-
lastIdleCheckPosition = scrollPosition;
|
|
190
|
-
idleCheckFrame = requestAnimationFrame(checkIdle);
|
|
191
|
-
};
|
|
192
|
-
idleCheckFrame = requestAnimationFrame(checkIdle);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Stop idle detection
|
|
196
|
-
const stopIdleDetection = () => {
|
|
197
|
-
if (idleCheckFrame) {
|
|
198
|
-
// console.log("[Scrolling] Stopping idle detection");
|
|
199
|
-
cancelAnimationFrame(idleCheckFrame);
|
|
200
|
-
idleCheckFrame = null;
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// Set velocity to zero and emit idle event
|
|
205
|
-
const setVelocityToZero = () => {
|
|
206
|
-
// console.log("[Scrolling] Setting velocity to zero and emitting idle");
|
|
207
|
-
|
|
208
|
-
// Stop idle detection since we're now idle
|
|
209
|
-
stopIdleDetection();
|
|
210
|
-
|
|
211
|
-
speedTracker = createSpeedTracker();
|
|
212
|
-
isScrolling = false; // Reset scrolling state
|
|
213
|
-
|
|
214
|
-
// Emit velocity change
|
|
215
|
-
component.emit?.("viewport:velocity-changed", {
|
|
216
|
-
velocity: 0,
|
|
217
|
-
direction: speedTracker.direction,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Emit idle state
|
|
221
|
-
component.emit?.("viewport:idle", {
|
|
222
|
-
position: scrollPosition,
|
|
223
|
-
lastScrollTime,
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
// Update container position
|
|
227
|
-
const updateContainerPosition = () => {
|
|
228
|
-
if (!viewportState || !viewportState.itemsContainer) return;
|
|
229
|
-
|
|
230
|
-
// Items container doesn't move - items inside it are positioned
|
|
231
|
-
// This is handled by the rendering feature
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Handle wheel event
|
|
235
|
-
const handleWheel = (event: WheelEvent) => {
|
|
236
|
-
event.preventDefault();
|
|
237
|
-
|
|
238
|
-
const delta = orientation === "vertical" ? event.deltaY : event.deltaX;
|
|
239
|
-
const scrollDelta = delta * sensitivity;
|
|
240
|
-
|
|
241
|
-
const previousPosition = scrollPosition;
|
|
242
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
243
|
-
|
|
244
|
-
let newPosition = scrollPosition + scrollDelta;
|
|
245
|
-
|
|
246
|
-
// Apply smoothing if enabled
|
|
247
|
-
if (smoothing) {
|
|
248
|
-
const smoothingFactor = 0.3;
|
|
249
|
-
newPosition = scrollPosition + scrollDelta * smoothingFactor;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
newPosition = clamp(newPosition, 0, maxScroll);
|
|
253
|
-
|
|
254
|
-
if (newPosition !== scrollPosition) {
|
|
255
|
-
scrollPosition = newPosition;
|
|
256
|
-
const now = Date.now();
|
|
257
|
-
|
|
258
|
-
// Update scroll state
|
|
259
|
-
if (!isScrolling) {
|
|
260
|
-
isScrolling = true;
|
|
261
|
-
// Stop any existing idle detection before starting new one
|
|
262
|
-
stopIdleDetection();
|
|
263
|
-
startIdleDetection();
|
|
264
|
-
}
|
|
265
|
-
lastScrollTime = now;
|
|
266
|
-
|
|
267
|
-
// Update speed tracker
|
|
268
|
-
speedTracker = updateSpeedTracker(
|
|
269
|
-
speedTracker,
|
|
270
|
-
scrollPosition,
|
|
271
|
-
previousPosition,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Update viewport state
|
|
275
|
-
if (viewportState) {
|
|
276
|
-
viewportState.scrollPosition = scrollPosition;
|
|
277
|
-
viewportState.velocity = speedTracker.velocity;
|
|
278
|
-
viewportState.scrollDirection = speedTracker.direction;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Emit events
|
|
282
|
-
component.emit?.("viewport:scroll", {
|
|
283
|
-
position: scrollPosition,
|
|
284
|
-
direction: speedTracker.direction,
|
|
285
|
-
previousPosition,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
component.emit?.("viewport:velocity-changed", {
|
|
289
|
-
velocity: speedTracker.velocity,
|
|
290
|
-
direction: speedTracker.direction,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Trigger render
|
|
294
|
-
component.viewport.renderItems();
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
// Scroll to position
|
|
299
|
-
const scrollToPosition = (position: number, source?: string) => {
|
|
300
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
301
|
-
const clampedPosition = clamp(position, 0, maxScroll);
|
|
302
|
-
|
|
303
|
-
// console.log(
|
|
304
|
-
// `[Scrolling] scrollToPosition: pos=${position} -> ${clampedPosition}, source=${source}, currentPos=${scrollPosition}, velocity=${speedTracker.velocity.toFixed(
|
|
305
|
-
// 3
|
|
306
|
-
// )}`
|
|
307
|
-
// );
|
|
308
|
-
|
|
309
|
-
if (clampedPosition !== scrollPosition) {
|
|
310
|
-
const previousPosition = scrollPosition;
|
|
311
|
-
scrollPosition = clampedPosition;
|
|
312
|
-
|
|
313
|
-
// Update speed tracker to calculate velocity
|
|
314
|
-
speedTracker = updateSpeedTracker(
|
|
315
|
-
speedTracker,
|
|
316
|
-
scrollPosition,
|
|
317
|
-
previousPosition,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// Update viewport state
|
|
321
|
-
if (viewportState) {
|
|
322
|
-
viewportState.scrollPosition = scrollPosition;
|
|
323
|
-
viewportState.velocity = speedTracker.velocity;
|
|
324
|
-
viewportState.scrollDirection = speedTracker.direction;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const direction =
|
|
328
|
-
clampedPosition > previousPosition ? "forward" : "backward";
|
|
329
|
-
|
|
330
|
-
component.emit?.("viewport:scroll", {
|
|
331
|
-
position: scrollPosition,
|
|
332
|
-
direction,
|
|
333
|
-
previousPosition,
|
|
334
|
-
source,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Emit velocity change event so collection can track it
|
|
338
|
-
component.emit?.("viewport:velocity-changed", {
|
|
339
|
-
velocity: speedTracker.velocity,
|
|
340
|
-
direction: speedTracker.direction,
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Update scroll state and idle detection
|
|
344
|
-
if (!isScrolling) {
|
|
345
|
-
isScrolling = true;
|
|
346
|
-
startIdleDetection();
|
|
347
|
-
}
|
|
348
|
-
lastScrollTime = Date.now();
|
|
349
|
-
|
|
350
|
-
// Trigger render
|
|
351
|
-
component.viewport.renderItems();
|
|
352
|
-
} else {
|
|
353
|
-
// console.log(`[Scrolling] Position unchanged: ${scrollPosition}`);
|
|
354
|
-
// console.log(
|
|
355
|
-
// `[Scrolling] Position unchanged: ${scrollPosition}, not resetting idle timeout`
|
|
356
|
-
// );
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Scroll to index
|
|
361
|
-
const scrollToIndex = (
|
|
362
|
-
index: number,
|
|
363
|
-
alignment: "start" | "center" | "end" = "start",
|
|
364
|
-
) => {
|
|
365
|
-
// console.log(
|
|
366
|
-
// `[Scrolling] scrollToIndex called: index=${index}, alignment=${alignment}`
|
|
367
|
-
// );
|
|
368
|
-
if (!viewportState) {
|
|
369
|
-
//console.log(`[Scrolling] scrollToIndex aborted: no viewport state`);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const itemSize = viewportState.itemSize || 50;
|
|
374
|
-
const totalItems = viewportState.totalItems || 0;
|
|
375
|
-
const actualTotalSize = totalItems * itemSize;
|
|
376
|
-
const MAX_VIRTUAL_SIZE =
|
|
377
|
-
VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
|
|
378
|
-
const isCompressed = actualTotalSize > MAX_VIRTUAL_SIZE;
|
|
379
|
-
|
|
380
|
-
let targetPosition: number;
|
|
381
|
-
|
|
382
|
-
if (isCompressed) {
|
|
383
|
-
// In compressed space, map index to virtual position
|
|
384
|
-
const ratio = index / totalItems;
|
|
385
|
-
targetPosition = ratio * Math.min(actualTotalSize, MAX_VIRTUAL_SIZE);
|
|
386
|
-
} else {
|
|
387
|
-
// Direct calculation when not compressed
|
|
388
|
-
targetPosition = index * itemSize;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Adjust position based on alignment
|
|
392
|
-
switch (alignment) {
|
|
393
|
-
case "center":
|
|
394
|
-
targetPosition -= containerSize / 2 - itemSize / 2;
|
|
395
|
-
break;
|
|
396
|
-
case "end":
|
|
397
|
-
targetPosition -= containerSize - itemSize;
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// console.log(
|
|
402
|
-
// `[Scrolling] Target position: ${targetPosition}, isCompressed: ${isCompressed}`
|
|
403
|
-
// );
|
|
404
|
-
|
|
405
|
-
// console.log(
|
|
406
|
-
// `[Scrolling] ScrollToIndex: index=${index}, position=${targetPosition}, alignment=${alignment}`
|
|
407
|
-
// );
|
|
408
|
-
|
|
409
|
-
scrollToPosition(targetPosition, "scrollToIndex");
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Scroll to a specific page
|
|
413
|
-
const scrollToPage = (
|
|
414
|
-
page: number,
|
|
415
|
-
limit: number = 20,
|
|
416
|
-
alignment: "start" | "center" | "end" = "start",
|
|
417
|
-
) => {
|
|
418
|
-
// Validate alignment parameter
|
|
419
|
-
if (
|
|
420
|
-
typeof alignment !== "string" ||
|
|
421
|
-
!["start", "center", "end"].includes(alignment)
|
|
422
|
-
) {
|
|
423
|
-
console.warn(
|
|
424
|
-
`[Scrolling] Invalid alignment "${alignment}", using "start"`,
|
|
425
|
-
);
|
|
426
|
-
alignment = "start";
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Check if we're in cursor mode
|
|
430
|
-
const viewportConfig = (component as any).config;
|
|
431
|
-
const isCursorMode = viewportConfig?.pagination?.strategy === "cursor";
|
|
432
|
-
|
|
433
|
-
if (isCursorMode) {
|
|
434
|
-
// In cursor mode, check if we can scroll to this page
|
|
435
|
-
const collection = (component.viewport as any).collection;
|
|
436
|
-
if (collection) {
|
|
437
|
-
const highestLoadedPage = Math.floor(
|
|
438
|
-
collection.getLoadedRanges().size,
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
if (page > highestLoadedPage + 1) {
|
|
442
|
-
// Limit how many pages we'll load at once to prevent excessive API calls
|
|
443
|
-
const maxPagesToLoad = 10; // Reasonable limit
|
|
444
|
-
const targetPage = Math.min(
|
|
445
|
-
page,
|
|
446
|
-
highestLoadedPage + maxPagesToLoad,
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
console.warn(
|
|
450
|
-
`[Scrolling] Cannot jump directly to page ${page} in cursor mode. ` +
|
|
451
|
-
`Pages must be loaded sequentially. Current highest page: ${highestLoadedPage}. ` +
|
|
452
|
-
`Will load up to page ${targetPage}`,
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
// Trigger sequential loading to the target page (limited)
|
|
456
|
-
const targetOffset = (targetPage - 1) * limit;
|
|
457
|
-
const currentOffset = highestLoadedPage * limit;
|
|
458
|
-
|
|
459
|
-
// Load pages sequentially up to the limited target
|
|
460
|
-
// console.log(
|
|
461
|
-
// `[Scrolling] Initiating sequential load from page ${
|
|
462
|
-
// highestLoadedPage + 1
|
|
463
|
-
// } to ${targetPage}`,
|
|
464
|
-
// );
|
|
465
|
-
|
|
466
|
-
// Scroll to the last loaded position first
|
|
467
|
-
const lastLoadedIndex = highestLoadedPage * limit;
|
|
468
|
-
scrollToIndex(lastLoadedIndex, alignment);
|
|
469
|
-
|
|
470
|
-
// The collection feature will handle sequential loading
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Convert page to index (page 1 = index 0)
|
|
477
|
-
const index = (page - 1) * limit;
|
|
478
|
-
|
|
479
|
-
// console.log(
|
|
480
|
-
// `[Scrolling] ScrollToPage: page=${page}, limit=${limit}, targetIndex=${index}, alignment=${alignment}`
|
|
481
|
-
// );
|
|
482
|
-
|
|
483
|
-
// Just scroll to the index - let the normal rendering flow handle data loading and placeholders
|
|
484
|
-
scrollToIndex(index, alignment);
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// Update scroll bounds
|
|
488
|
-
const updateScrollBounds = (
|
|
489
|
-
newTotalSize: number,
|
|
490
|
-
newContainerSize: number,
|
|
491
|
-
) => {
|
|
492
|
-
totalVirtualSize = newTotalSize;
|
|
493
|
-
containerSize = newContainerSize;
|
|
494
|
-
|
|
495
|
-
if (viewportState) {
|
|
496
|
-
viewportState.virtualTotalSize = newTotalSize;
|
|
497
|
-
viewportState.containerSize = newContainerSize;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Don't clamp scroll position until we have real data loaded
|
|
501
|
-
// This prevents resetting initialScrollIndex position before data arrives
|
|
502
|
-
// Check totalItems instead of totalVirtualSize since virtualSize can be non-zero from padding
|
|
503
|
-
const totalItems = viewportState?.totalItems || 0;
|
|
504
|
-
if (totalItems <= 0) {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Clamp current position to new bounds
|
|
509
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
510
|
-
if (scrollPosition > maxScroll) {
|
|
511
|
-
scrollToPosition(maxScroll);
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
// Extend viewport API
|
|
516
|
-
const originalScrollToIndex = component.viewport.scrollToIndex;
|
|
517
|
-
component.viewport.scrollToIndex = (
|
|
518
|
-
index: number,
|
|
519
|
-
alignment?: "start" | "center" | "end",
|
|
520
|
-
) => {
|
|
521
|
-
scrollToIndex(index, alignment);
|
|
522
|
-
originalScrollToIndex?.(index, alignment);
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// Add scrollToPage to viewport API (new method, no original to preserve)
|
|
526
|
-
(component.viewport as any).scrollToPage = (
|
|
527
|
-
page: number,
|
|
528
|
-
limit?: number,
|
|
529
|
-
alignment?: "start" | "center" | "end",
|
|
530
|
-
) => {
|
|
531
|
-
scrollToPage(page, limit, alignment);
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
const originalScrollToPosition = component.viewport.scrollToPosition;
|
|
535
|
-
component.viewport.scrollToPosition = (position: number) => {
|
|
536
|
-
scrollToPosition(position, "api");
|
|
537
|
-
originalScrollToPosition?.(position);
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
const originalGetScrollPosition = component.viewport.getScrollPosition;
|
|
541
|
-
component.viewport.getScrollPosition = () => {
|
|
542
|
-
return scrollPosition;
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
// Add wheel event listener on initialization
|
|
546
|
-
// This block is removed as per the edit hint.
|
|
547
|
-
|
|
548
|
-
// Clean up on destroy
|
|
549
|
-
if ("destroy" in component && typeof component.destroy === "function") {
|
|
550
|
-
const originalDestroy = component.destroy;
|
|
551
|
-
component.destroy = () => {
|
|
552
|
-
// Remove event listeners
|
|
553
|
-
const viewportElement = (component as any)._scrollingViewportElement;
|
|
554
|
-
if (viewportElement) {
|
|
555
|
-
viewportElement.removeEventListener("wheel", handleWheel);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Clear timeouts
|
|
559
|
-
if (idleTimeoutId) {
|
|
560
|
-
clearTimeout(idleTimeoutId);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
stopIdleDetection();
|
|
564
|
-
originalDestroy?.();
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Add scrollBy method
|
|
569
|
-
const scrollBy = (delta: number) => {
|
|
570
|
-
const previousPosition = scrollPosition;
|
|
571
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
572
|
-
const newPosition = clamp(scrollPosition + delta, 0, maxScroll);
|
|
573
|
-
|
|
574
|
-
if (newPosition !== previousPosition) {
|
|
575
|
-
scrollPosition = newPosition;
|
|
576
|
-
|
|
577
|
-
// Update speed tracker
|
|
578
|
-
speedTracker = updateSpeedTracker(
|
|
579
|
-
speedTracker,
|
|
580
|
-
scrollPosition,
|
|
581
|
-
previousPosition,
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
// Update viewport state
|
|
585
|
-
if (viewportState) {
|
|
586
|
-
viewportState.scrollPosition = scrollPosition;
|
|
587
|
-
viewportState.velocity = speedTracker.velocity;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Emit scroll event
|
|
591
|
-
component.emit?.("viewport:scroll", {
|
|
592
|
-
position: scrollPosition,
|
|
593
|
-
velocity: speedTracker.velocity,
|
|
594
|
-
direction: speedTracker.direction,
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
// Trigger render
|
|
598
|
-
component.viewport.renderItems?.();
|
|
599
|
-
|
|
600
|
-
// Start idle detection if not scrolling
|
|
601
|
-
if (!isScrolling) {
|
|
602
|
-
isScrolling = true;
|
|
603
|
-
startIdleDetection();
|
|
604
|
-
}
|
|
605
|
-
lastScrollTime = Date.now();
|
|
606
|
-
}
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
// Expose scrolling state for other features
|
|
610
|
-
(component.viewport as any).scrollingState = {
|
|
611
|
-
setVelocityToZero,
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
// Add scrollBy to viewport API
|
|
615
|
-
component.viewport.scrollBy = scrollBy;
|
|
616
|
-
component.viewport.getVelocity = () => speedTracker.velocity;
|
|
617
|
-
|
|
618
|
-
// Expose scrolling API
|
|
619
|
-
(component as any).scrolling = {
|
|
620
|
-
handleWheel,
|
|
621
|
-
scrollToPosition,
|
|
622
|
-
scrollToIndex,
|
|
623
|
-
scrollToPage,
|
|
624
|
-
scrollBy,
|
|
625
|
-
getScrollPosition: () => scrollPosition,
|
|
626
|
-
updateScrollBounds,
|
|
627
|
-
getVelocity: () => speedTracker.velocity,
|
|
628
|
-
getDirection: () => speedTracker.direction,
|
|
629
|
-
isScrolling: () => isScrolling,
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
return component;
|
|
633
|
-
};
|
|
634
|
-
};
|