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,795 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scrolling Module - Virtual scrolling with integrated velocity tracking
|
|
3
|
-
* Handles wheel events, scroll position management, scrollbar interactions, and velocity measurement
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ListManagerComponent,
|
|
8
|
-
ItemRange,
|
|
9
|
-
SpeedTracker,
|
|
10
|
-
} from "../../types";
|
|
11
|
-
import { LIST_MANAGER_CONSTANTS } from "../../constants";
|
|
12
|
-
import { clamp } from "../../utils/calculations";
|
|
13
|
-
import type { ItemSizeManager } from "./item-size";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Configuration for scrolling functionality
|
|
17
|
-
*/
|
|
18
|
-
export interface ScrollingConfig {
|
|
19
|
-
orientation: "vertical" | "horizontal";
|
|
20
|
-
enableScrollbar: boolean;
|
|
21
|
-
// Tracking configuration
|
|
22
|
-
trackingEnabled?: boolean;
|
|
23
|
-
measurementWindow?: number;
|
|
24
|
-
decelerationFactor?: number;
|
|
25
|
-
decayCheckInterval?: number;
|
|
26
|
-
fastThreshold?: number;
|
|
27
|
-
slowThreshold?: number;
|
|
28
|
-
smoothingFactor?: number;
|
|
29
|
-
// Callbacks
|
|
30
|
-
onScrollPositionChanged?: (data: {
|
|
31
|
-
position: number;
|
|
32
|
-
direction: "forward" | "backward";
|
|
33
|
-
previousPosition?: number;
|
|
34
|
-
targetIndex?: number;
|
|
35
|
-
alignment?: string;
|
|
36
|
-
source?: string;
|
|
37
|
-
}) => void;
|
|
38
|
-
onVirtualRangeChanged?: (range: ItemRange) => void;
|
|
39
|
-
onSpeedChanged?: (data: {
|
|
40
|
-
speed: number;
|
|
41
|
-
smoothedSpeed: number;
|
|
42
|
-
direction: "forward" | "backward";
|
|
43
|
-
isAccelerating: boolean;
|
|
44
|
-
acceleration: number;
|
|
45
|
-
}) => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Scrolling state interface
|
|
50
|
-
*/
|
|
51
|
-
export interface ScrollingState {
|
|
52
|
-
virtualScrollPosition: number;
|
|
53
|
-
totalVirtualSize: number;
|
|
54
|
-
containerSize: number;
|
|
55
|
-
thumbPosition: number;
|
|
56
|
-
scrollbarVisible: boolean;
|
|
57
|
-
scrollbarFadeTimeout: number | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Scrolling manager interface with integrated tracking
|
|
62
|
-
*/
|
|
63
|
-
export interface ScrollingManager {
|
|
64
|
-
// Core scrolling
|
|
65
|
-
handleWheel(event: WheelEvent): void;
|
|
66
|
-
scrollToPosition(position: number, source?: string): void;
|
|
67
|
-
scrollToIndex(index: number, alignment?: "start" | "center" | "end"): void;
|
|
68
|
-
|
|
69
|
-
// Container positioning
|
|
70
|
-
updateContainerPosition(): void;
|
|
71
|
-
|
|
72
|
-
// Scrollbar management
|
|
73
|
-
updateScrollbar(): void;
|
|
74
|
-
showScrollbar(): void;
|
|
75
|
-
setupScrollbar(): void;
|
|
76
|
-
destroyScrollbar(): void;
|
|
77
|
-
|
|
78
|
-
// Wheel event management
|
|
79
|
-
setupWheelEvents(): void;
|
|
80
|
-
removeWheelEvents(): void;
|
|
81
|
-
|
|
82
|
-
// State
|
|
83
|
-
getScrollPosition(): number;
|
|
84
|
-
getContainerSize(): number;
|
|
85
|
-
getTotalVirtualSize(): number;
|
|
86
|
-
updateState(updates: Partial<ScrollingState>): void;
|
|
87
|
-
|
|
88
|
-
// Integrated tracking API
|
|
89
|
-
getCurrentSpeed(): number;
|
|
90
|
-
getDirection(): "forward" | "backward";
|
|
91
|
-
getAcceleration(): number;
|
|
92
|
-
isAccelerating(): boolean;
|
|
93
|
-
getSpeedHistory(): number[];
|
|
94
|
-
getSmoothedSpeed(): number;
|
|
95
|
-
getAverageSpeed(): number;
|
|
96
|
-
isFastScrolling(): boolean;
|
|
97
|
-
isSlowScrolling(): boolean;
|
|
98
|
-
isMediumScrolling(): boolean;
|
|
99
|
-
updateSpeedThresholds(fast: number, slow: number): void;
|
|
100
|
-
resetTracking(): void;
|
|
101
|
-
getTracker(): SpeedTracker;
|
|
102
|
-
isTrackingEnabled(): boolean;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Creates a scrolling manager for virtual scroll position and scrollbar management
|
|
107
|
-
*/
|
|
108
|
-
export const createScrollingManager = (
|
|
109
|
-
component: ListManagerComponent,
|
|
110
|
-
itemSizeManager: ItemSizeManager,
|
|
111
|
-
config: ScrollingConfig,
|
|
112
|
-
calculateVisibleRange: () => ItemRange,
|
|
113
|
-
renderItems: () => void,
|
|
114
|
-
getTotalItems?: () => number,
|
|
115
|
-
loadDataForRange?: (range: { start: number; end: number }) => void, // Change to loadDataForRange callback
|
|
116
|
-
getHeightCapInfo?: () => any, // Get height cap info from virtual manager
|
|
117
|
-
calculateVirtualPositionForIndex?: (index: number) => number // Calculate virtual position for index
|
|
118
|
-
): ScrollingManager => {
|
|
119
|
-
const {
|
|
120
|
-
orientation,
|
|
121
|
-
enableScrollbar,
|
|
122
|
-
trackingEnabled = true,
|
|
123
|
-
measurementWindow = LIST_MANAGER_CONSTANTS.SPEED_TRACKING
|
|
124
|
-
.MEASUREMENT_WINDOW,
|
|
125
|
-
decelerationFactor = 0.9,
|
|
126
|
-
smoothingFactor = 0.8,
|
|
127
|
-
decayCheckInterval = 50, // Check decay every 50ms instead of every frame
|
|
128
|
-
onScrollPositionChanged,
|
|
129
|
-
onVirtualRangeChanged,
|
|
130
|
-
onSpeedChanged,
|
|
131
|
-
} = config;
|
|
132
|
-
|
|
133
|
-
let fastThreshold =
|
|
134
|
-
config.fastThreshold ||
|
|
135
|
-
LIST_MANAGER_CONSTANTS.SPEED_TRACKING.FAST_SCROLL_THRESHOLD;
|
|
136
|
-
let slowThreshold =
|
|
137
|
-
config.slowThreshold ||
|
|
138
|
-
LIST_MANAGER_CONSTANTS.SPEED_TRACKING.SLOW_SCROLL_THRESHOLD;
|
|
139
|
-
|
|
140
|
-
// Scrolling state
|
|
141
|
-
let virtualScrollPosition = 0;
|
|
142
|
-
let totalVirtualSize = 0;
|
|
143
|
-
let containerSize = 0;
|
|
144
|
-
let thumbPosition = 0;
|
|
145
|
-
let scrollbarVisible = false;
|
|
146
|
-
let scrollbarFadeTimeout: number | null = null;
|
|
147
|
-
|
|
148
|
-
// Direct scrolling - no animation for immediate response
|
|
149
|
-
|
|
150
|
-
// Velocity tracking state
|
|
151
|
-
let speedTracker: SpeedTracker = {
|
|
152
|
-
velocity: 0,
|
|
153
|
-
direction: "forward",
|
|
154
|
-
isAccelerating: false,
|
|
155
|
-
lastMeasurement: Date.now(),
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
let speedHistory: number[] = [];
|
|
159
|
-
let positionHistory: { position: number; timestamp: number }[] = [];
|
|
160
|
-
let smoothedSpeed = 0;
|
|
161
|
-
let lastPosition = 0;
|
|
162
|
-
let lastTimestamp = Date.now();
|
|
163
|
-
let currentAcceleration = 0;
|
|
164
|
-
let lastIdleCheckPosition = 0;
|
|
165
|
-
let idleCheckFrame: number | null = null;
|
|
166
|
-
|
|
167
|
-
// Scrollbar state (managed by external plugin)
|
|
168
|
-
let scrollbarPlugin: any = null;
|
|
169
|
-
|
|
170
|
-
// Items container reference
|
|
171
|
-
let itemsContainer: HTMLElement | null = null;
|
|
172
|
-
|
|
173
|
-
// Track last render time to avoid excessive rendering
|
|
174
|
-
let lastRenderTime = 0;
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Initialize scrolling with container reference
|
|
178
|
-
*/
|
|
179
|
-
const setItemsContainer = (container: HTMLElement) => {
|
|
180
|
-
itemsContainer = container;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Initialize velocity tracking
|
|
185
|
-
*/
|
|
186
|
-
const initializeTracking = (): void => {
|
|
187
|
-
if (!trackingEnabled) return;
|
|
188
|
-
startIdleDetection();
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Start idle detection
|
|
193
|
-
*/
|
|
194
|
-
const startIdleDetection = (): void => {
|
|
195
|
-
const checkIdle = () => {
|
|
196
|
-
if (!trackingEnabled) {
|
|
197
|
-
idleCheckFrame = null;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check if position hasn't changed and velocity is not already 0
|
|
202
|
-
if (
|
|
203
|
-
virtualScrollPosition === lastIdleCheckPosition &&
|
|
204
|
-
speedTracker.velocity > 0
|
|
205
|
-
) {
|
|
206
|
-
setVelocityToZero();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
lastIdleCheckPosition = virtualScrollPosition;
|
|
210
|
-
idleCheckFrame = requestAnimationFrame(checkIdle);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
idleCheckFrame = requestAnimationFrame(checkIdle);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Set velocity to zero and trigger necessary updates
|
|
218
|
-
*/
|
|
219
|
-
const setVelocityToZero = (): void => {
|
|
220
|
-
// Reset velocity state
|
|
221
|
-
speedTracker.velocity = 0;
|
|
222
|
-
speedTracker.isAccelerating = false;
|
|
223
|
-
smoothedSpeed = 0;
|
|
224
|
-
currentAcceleration = 0;
|
|
225
|
-
|
|
226
|
-
// Emit speed change
|
|
227
|
-
onSpeedChanged?.({
|
|
228
|
-
speed: 0,
|
|
229
|
-
smoothedSpeed: 0,
|
|
230
|
-
direction: speedTracker.direction,
|
|
231
|
-
isAccelerating: false,
|
|
232
|
-
acceleration: 0,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
component.emit?.("speed:changed", {
|
|
236
|
-
speed: 0,
|
|
237
|
-
direction: speedTracker.direction,
|
|
238
|
-
isAccelerating: false,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Trigger rendering and data loading
|
|
242
|
-
renderItems();
|
|
243
|
-
const visibleRange = calculateVisibleRange();
|
|
244
|
-
if (loadDataForRange) {
|
|
245
|
-
loadDataForRange({
|
|
246
|
-
start: visibleRange.start,
|
|
247
|
-
end: visibleRange.end,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Update velocity tracking
|
|
254
|
-
*/
|
|
255
|
-
const updateVelocityTracking = (
|
|
256
|
-
deltaPosition: number,
|
|
257
|
-
deltaTime: number,
|
|
258
|
-
lastPosition: number
|
|
259
|
-
): void => {
|
|
260
|
-
if (!trackingEnabled) return;
|
|
261
|
-
|
|
262
|
-
const now = Date.now();
|
|
263
|
-
const position = lastPosition + deltaPosition;
|
|
264
|
-
|
|
265
|
-
// If delta is 0 or very small, set velocity to 0
|
|
266
|
-
if (Math.abs(deltaPosition) < 0.1) {
|
|
267
|
-
if (speedTracker.velocity > 0) {
|
|
268
|
-
setVelocityToZero();
|
|
269
|
-
}
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Calculate instantaneous velocity
|
|
274
|
-
const instantVelocity = Math.abs(deltaPosition) / deltaTime;
|
|
275
|
-
|
|
276
|
-
// Update direction
|
|
277
|
-
const newDirection = deltaPosition >= 0 ? "forward" : "backward";
|
|
278
|
-
|
|
279
|
-
// Calculate acceleration
|
|
280
|
-
const previousVelocity = speedTracker.velocity;
|
|
281
|
-
currentAcceleration = (instantVelocity - previousVelocity) / deltaTime;
|
|
282
|
-
|
|
283
|
-
// Update speed tracker
|
|
284
|
-
speedTracker = {
|
|
285
|
-
velocity: instantVelocity,
|
|
286
|
-
direction: newDirection,
|
|
287
|
-
isAccelerating: currentAcceleration > 0,
|
|
288
|
-
lastMeasurement: now,
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
// Add to position history for windowed calculations
|
|
292
|
-
positionHistory.push({ position, timestamp: now });
|
|
293
|
-
|
|
294
|
-
// Clean old history outside measurement window
|
|
295
|
-
const windowStart = now - measurementWindow;
|
|
296
|
-
positionHistory = positionHistory.filter(
|
|
297
|
-
(entry) => entry.timestamp >= windowStart
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
// Calculate windowed velocity if we have enough data
|
|
301
|
-
if (positionHistory.length >= 2) {
|
|
302
|
-
const windowedVelocity = calculateWindowedVelocity();
|
|
303
|
-
speedTracker.velocity = windowedVelocity;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Update speed history
|
|
307
|
-
speedHistory.push(speedTracker.velocity);
|
|
308
|
-
if (speedHistory.length > 20) {
|
|
309
|
-
speedHistory.shift();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Update smoothed speed using exponential smoothing
|
|
313
|
-
smoothedSpeed =
|
|
314
|
-
smoothedSpeed * smoothingFactor +
|
|
315
|
-
speedTracker.velocity * (1 - smoothingFactor);
|
|
316
|
-
|
|
317
|
-
// Update state
|
|
318
|
-
lastPosition = position;
|
|
319
|
-
lastTimestamp = now;
|
|
320
|
-
|
|
321
|
-
// Emit speed changed event
|
|
322
|
-
onSpeedChanged?.({
|
|
323
|
-
speed: speedTracker.velocity,
|
|
324
|
-
smoothedSpeed,
|
|
325
|
-
direction: speedTracker.direction,
|
|
326
|
-
isAccelerating: speedTracker.isAccelerating,
|
|
327
|
-
acceleration: currentAcceleration,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
component.emit?.("speed:changed", {
|
|
331
|
-
speed: speedTracker.velocity, // Use raw velocity for loading decisions
|
|
332
|
-
direction: speedTracker.direction,
|
|
333
|
-
isAccelerating: speedTracker.isAccelerating,
|
|
334
|
-
});
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Calculate windowed velocity for better accuracy
|
|
339
|
-
*/
|
|
340
|
-
const calculateWindowedVelocity = (): number => {
|
|
341
|
-
if (positionHistory.length < 2) return 0;
|
|
342
|
-
|
|
343
|
-
const latest = positionHistory[positionHistory.length - 1];
|
|
344
|
-
const earliest = positionHistory[0];
|
|
345
|
-
|
|
346
|
-
const totalDistance = Math.abs(latest.position - earliest.position);
|
|
347
|
-
const totalTime = latest.timestamp - earliest.timestamp;
|
|
348
|
-
|
|
349
|
-
return totalTime > 0 ? totalDistance / totalTime : 0;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Handle wheel events for scrolling
|
|
354
|
-
*/
|
|
355
|
-
const handleWheel = (event: WheelEvent): void => {
|
|
356
|
-
event.preventDefault();
|
|
357
|
-
|
|
358
|
-
const now = Date.now();
|
|
359
|
-
const actualDeltaTime = Math.max(1, now - lastTimestamp); // Minimum 1ms to avoid division by zero
|
|
360
|
-
lastTimestamp = now;
|
|
361
|
-
|
|
362
|
-
const sensitivity =
|
|
363
|
-
LIST_MANAGER_CONSTANTS.VIRTUAL_SCROLL.SCROLL_SENSITIVITY;
|
|
364
|
-
const delta = orientation === "vertical" ? event.deltaY : event.deltaX;
|
|
365
|
-
|
|
366
|
-
// Direct scroll delta for immediate response
|
|
367
|
-
let scrollDelta = delta * sensitivity;
|
|
368
|
-
|
|
369
|
-
const previousPosition = virtualScrollPosition;
|
|
370
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
371
|
-
|
|
372
|
-
// Check if we're using compressed virtual space
|
|
373
|
-
const actualTotalItems = getTotalItems
|
|
374
|
-
? getTotalItems()
|
|
375
|
-
: component.totalItems;
|
|
376
|
-
const itemSize = itemSizeManager.getEstimatedItemSize();
|
|
377
|
-
const actualTotalSize = actualTotalItems * itemSize;
|
|
378
|
-
const isCompressed = actualTotalSize > totalVirtualSize;
|
|
379
|
-
|
|
380
|
-
// Special handling when using compressed space and near boundaries
|
|
381
|
-
if (isCompressed) {
|
|
382
|
-
const isNearBottom = virtualScrollPosition >= maxScroll - 100;
|
|
383
|
-
const isNearTop = virtualScrollPosition <= 100;
|
|
384
|
-
|
|
385
|
-
if (isNearBottom || isNearTop) {
|
|
386
|
-
// We're using compressed virtual space
|
|
387
|
-
const compressionRatio = totalVirtualSize / actualTotalSize;
|
|
388
|
-
|
|
389
|
-
// Reduce scroll delta to account for compression
|
|
390
|
-
// This ensures we don't skip ranges when scrolling
|
|
391
|
-
scrollDelta = scrollDelta * compressionRatio;
|
|
392
|
-
|
|
393
|
-
// Further reduce delta when very close to boundaries
|
|
394
|
-
if ((isNearBottom && delta > 0) || (isNearTop && delta < 0)) {
|
|
395
|
-
scrollDelta = scrollDelta * 0.5; // Half speed at boundaries
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
let newPosition = virtualScrollPosition + scrollDelta;
|
|
401
|
-
|
|
402
|
-
// Apply boundary clamping
|
|
403
|
-
newPosition = clamp(newPosition, 0, maxScroll);
|
|
404
|
-
|
|
405
|
-
// Ensure we can reach the very end with mouse wheel
|
|
406
|
-
if (newPosition > maxScroll - 10 && delta > 0) {
|
|
407
|
-
// Close to the end and scrolling down, snap to max
|
|
408
|
-
newPosition = maxScroll;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Update scroll position immediately for instant feedback
|
|
412
|
-
virtualScrollPosition = newPosition;
|
|
413
|
-
|
|
414
|
-
// Update velocity tracking with the actual delta and real time
|
|
415
|
-
const deltaPosition = newPosition - previousPosition;
|
|
416
|
-
updateVelocityTracking(deltaPosition, actualDeltaTime, previousPosition);
|
|
417
|
-
|
|
418
|
-
// Update container position immediately for instant feedback
|
|
419
|
-
updateContainerPosition();
|
|
420
|
-
updateScrollbar();
|
|
421
|
-
showScrollbar();
|
|
422
|
-
|
|
423
|
-
// Trigger rendering for new visible range
|
|
424
|
-
renderItems();
|
|
425
|
-
|
|
426
|
-
// Emit events
|
|
427
|
-
const direction = newPosition > previousPosition ? "forward" : "backward";
|
|
428
|
-
onScrollPositionChanged?.({
|
|
429
|
-
position: virtualScrollPosition,
|
|
430
|
-
direction,
|
|
431
|
-
previousPosition,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const newVisibleRange = calculateVisibleRange();
|
|
435
|
-
onVirtualRangeChanged?.(newVisibleRange);
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Scroll to specific index
|
|
440
|
-
*/
|
|
441
|
-
const scrollToIndex = (
|
|
442
|
-
index: number,
|
|
443
|
-
alignment: "start" | "center" | "end" = "start"
|
|
444
|
-
): void => {
|
|
445
|
-
// Use getTotalItems callback if available, otherwise fall back to component.totalItems
|
|
446
|
-
const totalItems = getTotalItems ? getTotalItems() : component.totalItems;
|
|
447
|
-
|
|
448
|
-
if (index < 0 || index >= totalItems) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const previousPosition = virtualScrollPosition;
|
|
453
|
-
let targetPosition = 0;
|
|
454
|
-
|
|
455
|
-
// Calculate position based on measured sizes
|
|
456
|
-
for (let i = 0; i < index; i++) {
|
|
457
|
-
targetPosition +=
|
|
458
|
-
itemSizeManager.getMeasuredSize(i) ||
|
|
459
|
-
itemSizeManager.getEstimatedItemSize();
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Adjust position based on alignment
|
|
463
|
-
switch (alignment) {
|
|
464
|
-
case "center":
|
|
465
|
-
targetPosition -= containerSize / 2;
|
|
466
|
-
break;
|
|
467
|
-
case "end":
|
|
468
|
-
targetPosition -= containerSize;
|
|
469
|
-
break;
|
|
470
|
-
// 'start' is default - no adjustment needed
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Use virtual manager's position calculator for index-based scrolling
|
|
474
|
-
if (calculateVirtualPositionForIndex) {
|
|
475
|
-
const virtualPosition = calculateVirtualPositionForIndex(index);
|
|
476
|
-
|
|
477
|
-
// Only use virtual position if it differs from calculated position
|
|
478
|
-
if (Math.abs(virtualPosition - targetPosition) > 1) {
|
|
479
|
-
targetPosition = virtualPosition;
|
|
480
|
-
|
|
481
|
-
// Adjust for alignment after mapping to virtual space
|
|
482
|
-
switch (alignment) {
|
|
483
|
-
case "center":
|
|
484
|
-
targetPosition -= containerSize / 2;
|
|
485
|
-
break;
|
|
486
|
-
case "end":
|
|
487
|
-
targetPosition -= containerSize;
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Ensure position is within bounds
|
|
494
|
-
const maxPosition = Math.max(0, totalVirtualSize - containerSize);
|
|
495
|
-
targetPosition = Math.max(0, Math.min(targetPosition, maxPosition));
|
|
496
|
-
|
|
497
|
-
// Update scroll position immediately
|
|
498
|
-
virtualScrollPosition = targetPosition;
|
|
499
|
-
|
|
500
|
-
// Update UI immediately
|
|
501
|
-
updateContainerPosition();
|
|
502
|
-
updateScrollbar();
|
|
503
|
-
showScrollbar();
|
|
504
|
-
|
|
505
|
-
// Calculate new visible range and notify
|
|
506
|
-
const newVisibleRange = calculateVisibleRange();
|
|
507
|
-
|
|
508
|
-
// Notify about scroll position change
|
|
509
|
-
onScrollPositionChanged?.({
|
|
510
|
-
position: virtualScrollPosition,
|
|
511
|
-
previousPosition,
|
|
512
|
-
direction: targetPosition > previousPosition ? "forward" : "backward",
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// Notify about virtual range change
|
|
516
|
-
onVirtualRangeChanged?.(newVisibleRange);
|
|
517
|
-
|
|
518
|
-
// Trigger rendering for the new range
|
|
519
|
-
renderItems();
|
|
520
|
-
|
|
521
|
-
// Load data for the new range if needed
|
|
522
|
-
if (loadDataForRange) {
|
|
523
|
-
loadDataForRange({
|
|
524
|
-
start: newVisibleRange.start,
|
|
525
|
-
end: newVisibleRange.end,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Scroll to a specific position
|
|
532
|
-
*/
|
|
533
|
-
const scrollToPosition = (position: number, source?: string): void => {
|
|
534
|
-
const maxScroll = Math.max(0, totalVirtualSize - containerSize);
|
|
535
|
-
const clampedPosition = clamp(position, 0, maxScroll);
|
|
536
|
-
|
|
537
|
-
if (clampedPosition === virtualScrollPosition) {
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const previousPosition = virtualScrollPosition;
|
|
542
|
-
|
|
543
|
-
// Update scroll position immediately
|
|
544
|
-
virtualScrollPosition = clampedPosition;
|
|
545
|
-
|
|
546
|
-
// Update velocity tracking with actual time delta
|
|
547
|
-
const now = Date.now();
|
|
548
|
-
const actualDeltaTime = Math.max(1, now - lastTimestamp);
|
|
549
|
-
const deltaPosition = clampedPosition - previousPosition;
|
|
550
|
-
|
|
551
|
-
// For scrollbar dragging, use actual time between updates
|
|
552
|
-
if (source === "scrollbar-drag") {
|
|
553
|
-
updateVelocityTracking(deltaPosition, actualDeltaTime, previousPosition);
|
|
554
|
-
} else {
|
|
555
|
-
// For other sources, use default frame time
|
|
556
|
-
updateVelocityTracking(deltaPosition, 16, previousPosition);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
lastTimestamp = now;
|
|
560
|
-
|
|
561
|
-
// Update UI immediately
|
|
562
|
-
updateContainerPosition();
|
|
563
|
-
updateScrollbar();
|
|
564
|
-
showScrollbar();
|
|
565
|
-
|
|
566
|
-
// Trigger rendering immediately for all sources
|
|
567
|
-
renderItems();
|
|
568
|
-
|
|
569
|
-
// For scrollbar drag, also ensure we load data
|
|
570
|
-
if (source === "scrollbar-drag") {
|
|
571
|
-
const visibleRange = calculateVisibleRange();
|
|
572
|
-
if (loadDataForRange) {
|
|
573
|
-
loadDataForRange({
|
|
574
|
-
start: visibleRange.start,
|
|
575
|
-
end: visibleRange.end,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Emit events
|
|
581
|
-
const direction =
|
|
582
|
-
clampedPosition > previousPosition ? "forward" : "backward";
|
|
583
|
-
onScrollPositionChanged?.({
|
|
584
|
-
position: virtualScrollPosition,
|
|
585
|
-
direction,
|
|
586
|
-
previousPosition,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
const newVisibleRange = calculateVisibleRange();
|
|
590
|
-
onVirtualRangeChanged?.(newVisibleRange);
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Update container position for virtual scrolling with element accumulation
|
|
595
|
-
*/
|
|
596
|
-
const updateContainerPosition = (): void => {
|
|
597
|
-
if (!itemsContainer) return;
|
|
598
|
-
|
|
599
|
-
// Don't apply transform to container - rely on absolute positioning of items
|
|
600
|
-
// Just trigger item position updates
|
|
601
|
-
if ((component as any).viewport?.updateItemPositions) {
|
|
602
|
-
(component as any).viewport.updateItemPositions();
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Update scrollbar thumb position and size (delegated to external plugin)
|
|
608
|
-
*/
|
|
609
|
-
const updateScrollbar = (): void => {
|
|
610
|
-
if (!enableScrollbar || !scrollbarPlugin) return;
|
|
611
|
-
|
|
612
|
-
// Calculate scroll ratio for external scrollbar plugin
|
|
613
|
-
const scrollRatio =
|
|
614
|
-
totalVirtualSize > containerSize
|
|
615
|
-
? virtualScrollPosition / (totalVirtualSize - containerSize)
|
|
616
|
-
: 0;
|
|
617
|
-
|
|
618
|
-
// Update external scrollbar plugin
|
|
619
|
-
if (scrollbarPlugin.updateScrollPosition) {
|
|
620
|
-
scrollbarPlugin.updateScrollPosition(virtualScrollPosition);
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Setup scrollbar (delegated to external plugin)
|
|
626
|
-
*/
|
|
627
|
-
const setupScrollbar = (): void => {
|
|
628
|
-
// Scrollbar setup is handled by external plugin
|
|
629
|
-
// This method is kept for API compatibility
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Setup scrollbar events (delegated to external plugin)
|
|
634
|
-
*/
|
|
635
|
-
const setupScrollbarEvents = (): void => {
|
|
636
|
-
// Scrollbar events are handled by external plugin
|
|
637
|
-
// This method is kept for API compatibility
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Show scrollbar (delegated to external plugin)
|
|
642
|
-
*/
|
|
643
|
-
const showScrollbar = (): void => {
|
|
644
|
-
if (!enableScrollbar || !scrollbarPlugin) return;
|
|
645
|
-
|
|
646
|
-
// Show scrollbar through external plugin
|
|
647
|
-
if (scrollbarPlugin.showScrollbar) {
|
|
648
|
-
scrollbarPlugin.showScrollbar();
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Cleanup
|
|
654
|
-
*/
|
|
655
|
-
const destroy = (): void => {
|
|
656
|
-
destroyScrollbar();
|
|
657
|
-
if (idleCheckFrame) {
|
|
658
|
-
cancelAnimationFrame(idleCheckFrame);
|
|
659
|
-
idleCheckFrame = null;
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Destroy scrollbar (delegated to external plugin)
|
|
665
|
-
*/
|
|
666
|
-
const destroyScrollbar = (): void => {
|
|
667
|
-
if (scrollbarPlugin && scrollbarPlugin.destroy) {
|
|
668
|
-
scrollbarPlugin.destroy();
|
|
669
|
-
}
|
|
670
|
-
scrollbarPlugin = null;
|
|
671
|
-
|
|
672
|
-
if (scrollbarFadeTimeout) {
|
|
673
|
-
clearTimeout(scrollbarFadeTimeout);
|
|
674
|
-
scrollbarFadeTimeout = null;
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Get current scroll position
|
|
680
|
-
*/
|
|
681
|
-
const getScrollPosition = (): number => virtualScrollPosition;
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Get container size
|
|
685
|
-
*/
|
|
686
|
-
const getContainerSize = (): number => containerSize;
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Get total virtual size
|
|
690
|
-
*/
|
|
691
|
-
const getTotalVirtualSize = (): number => totalVirtualSize;
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Update scrolling state
|
|
695
|
-
*/
|
|
696
|
-
const updateState = (updates: Partial<ScrollingState>): void => {
|
|
697
|
-
if (updates.virtualScrollPosition !== undefined) {
|
|
698
|
-
virtualScrollPosition = updates.virtualScrollPosition;
|
|
699
|
-
}
|
|
700
|
-
if (updates.totalVirtualSize !== undefined) {
|
|
701
|
-
totalVirtualSize = updates.totalVirtualSize;
|
|
702
|
-
}
|
|
703
|
-
if (updates.containerSize !== undefined) {
|
|
704
|
-
containerSize = updates.containerSize;
|
|
705
|
-
}
|
|
706
|
-
if (updates.thumbPosition !== undefined) {
|
|
707
|
-
thumbPosition = updates.thumbPosition;
|
|
708
|
-
}
|
|
709
|
-
if (updates.scrollbarVisible !== undefined) {
|
|
710
|
-
scrollbarVisible = updates.scrollbarVisible;
|
|
711
|
-
}
|
|
712
|
-
if (updates.scrollbarFadeTimeout !== undefined) {
|
|
713
|
-
scrollbarFadeTimeout = updates.scrollbarFadeTimeout;
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
// Initialize tracking
|
|
718
|
-
initializeTracking();
|
|
719
|
-
|
|
720
|
-
return {
|
|
721
|
-
// Core scrolling
|
|
722
|
-
handleWheel,
|
|
723
|
-
scrollToIndex,
|
|
724
|
-
scrollToPosition,
|
|
725
|
-
|
|
726
|
-
// Container positioning
|
|
727
|
-
updateContainerPosition,
|
|
728
|
-
|
|
729
|
-
// Scrollbar management
|
|
730
|
-
updateScrollbar,
|
|
731
|
-
showScrollbar,
|
|
732
|
-
setupScrollbar,
|
|
733
|
-
destroyScrollbar: destroy,
|
|
734
|
-
|
|
735
|
-
// Wheel event management
|
|
736
|
-
setupWheelEvents: () => {
|
|
737
|
-
component.element.addEventListener("wheel", handleWheel, {
|
|
738
|
-
passive: false,
|
|
739
|
-
});
|
|
740
|
-
},
|
|
741
|
-
removeWheelEvents: () => {
|
|
742
|
-
component.element.removeEventListener("wheel", handleWheel);
|
|
743
|
-
},
|
|
744
|
-
|
|
745
|
-
// State
|
|
746
|
-
getScrollPosition,
|
|
747
|
-
getContainerSize,
|
|
748
|
-
getTotalVirtualSize,
|
|
749
|
-
updateState,
|
|
750
|
-
|
|
751
|
-
// Integrated tracking API
|
|
752
|
-
getCurrentSpeed: () => speedTracker.velocity,
|
|
753
|
-
getDirection: () => speedTracker.direction,
|
|
754
|
-
getAcceleration: () => currentAcceleration,
|
|
755
|
-
isAccelerating: () => speedTracker.isAccelerating,
|
|
756
|
-
getSpeedHistory: () => [...speedHistory],
|
|
757
|
-
getSmoothedSpeed: () => smoothedSpeed,
|
|
758
|
-
getAverageSpeed: () => {
|
|
759
|
-
if (speedHistory.length === 0) return 0;
|
|
760
|
-
return speedHistory.reduce((sum, v) => sum + v, 0) / speedHistory.length;
|
|
761
|
-
},
|
|
762
|
-
isFastScrolling: () => smoothedSpeed > fastThreshold,
|
|
763
|
-
isSlowScrolling: () => smoothedSpeed < slowThreshold,
|
|
764
|
-
isMediumScrolling: () =>
|
|
765
|
-
smoothedSpeed >= slowThreshold && smoothedSpeed <= fastThreshold,
|
|
766
|
-
updateSpeedThresholds: (fast: number, slow: number) => {
|
|
767
|
-
fastThreshold = fast;
|
|
768
|
-
slowThreshold = slow;
|
|
769
|
-
},
|
|
770
|
-
resetTracking: () => {
|
|
771
|
-
speedTracker = {
|
|
772
|
-
velocity: 0,
|
|
773
|
-
direction: "forward",
|
|
774
|
-
isAccelerating: false,
|
|
775
|
-
lastMeasurement: Date.now(),
|
|
776
|
-
};
|
|
777
|
-
speedHistory = [];
|
|
778
|
-
positionHistory = [];
|
|
779
|
-
smoothedSpeed = 0;
|
|
780
|
-
lastPosition = 0;
|
|
781
|
-
lastTimestamp = Date.now();
|
|
782
|
-
currentAcceleration = 0;
|
|
783
|
-
},
|
|
784
|
-
getTracker: () => ({ ...speedTracker }),
|
|
785
|
-
isTrackingEnabled: () => trackingEnabled,
|
|
786
|
-
|
|
787
|
-
// Internal (for viewport setup)
|
|
788
|
-
setItemsContainer,
|
|
789
|
-
setScrollbarPlugin: (plugin: any) => {
|
|
790
|
-
scrollbarPlugin = plugin;
|
|
791
|
-
},
|
|
792
|
-
} as ScrollingManager & {
|
|
793
|
-
setItemsContainer: (container: HTMLElement) => void;
|
|
794
|
-
};
|
|
795
|
-
};
|