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.
Files changed (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /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
- };