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
@@ -0,0 +1,130 @@
1
+ // src/core/viewport/features/events.ts
2
+
3
+ /**
4
+ * Events Feature - Centralized event system for viewport
5
+ * Provides event emission and subscription for inter-feature communication
6
+ */
7
+
8
+ import type { ViewportContext, ViewportComponent } from "../types";
9
+
10
+ export interface EventsConfig {
11
+ debug?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Events feature for viewport
16
+ * Centralizes all event handling for viewport features
17
+ */
18
+ export const withEvents = (config: EventsConfig = {}) => {
19
+ return <T extends ViewportContext & ViewportComponent>(component: T): T => {
20
+ const { debug = false } = config;
21
+
22
+ // Event listeners map
23
+ const listeners = new Map<string, Set<Function>>();
24
+
25
+ // Emit an event
26
+ const emit = (event: string, data?: any) => {
27
+ // if (debug) {
28
+ // console.log(`[Events] Emit: ${event}`, data);
29
+ // }
30
+
31
+ const eventListeners = listeners.get(event);
32
+ if (eventListeners) {
33
+ eventListeners.forEach((listener) => {
34
+ try {
35
+ listener(data);
36
+ } catch (error) {
37
+ console.error(`[Events] Error in listener for ${event}:`, error);
38
+ }
39
+ });
40
+ }
41
+ };
42
+
43
+ // Subscribe to an event
44
+ const on = (event: string, handler: Function): (() => void) => {
45
+ if (!listeners.has(event)) {
46
+ listeners.set(event, new Set());
47
+ }
48
+
49
+ listeners.get(event)!.add(handler);
50
+
51
+ // if (debug) {
52
+ // console.log(`[Events] Subscribed to: ${event}`);
53
+ // }
54
+
55
+ // Return unsubscribe function
56
+ return () => {
57
+ const eventListeners = listeners.get(event);
58
+ if (eventListeners) {
59
+ eventListeners.delete(handler);
60
+ if (eventListeners.size === 0) {
61
+ listeners.delete(event);
62
+ }
63
+ }
64
+ };
65
+ };
66
+
67
+ // Subscribe to an event once
68
+ const once = (event: string, handler: Function): (() => void) => {
69
+ const wrappedHandler = (data: any) => {
70
+ handler(data);
71
+ off(event, wrappedHandler);
72
+ };
73
+ return on(event, wrappedHandler);
74
+ };
75
+
76
+ // Unsubscribe from an event
77
+ const off = (event: string, handler: Function) => {
78
+ const eventListeners = listeners.get(event);
79
+ if (eventListeners) {
80
+ eventListeners.delete(handler);
81
+ if (eventListeners.size === 0) {
82
+ listeners.delete(event);
83
+ }
84
+ }
85
+ };
86
+
87
+ // Clear all listeners for an event
88
+ const clear = (event?: string) => {
89
+ if (event) {
90
+ listeners.delete(event);
91
+ } else {
92
+ listeners.clear();
93
+ }
94
+ };
95
+
96
+ // Add event methods to component
97
+ component.emit = emit;
98
+ component.on = on;
99
+ component.once = once;
100
+ component.off = off;
101
+
102
+ // Add events API to viewport
103
+ (component.viewport as any).events = {
104
+ emit,
105
+ on,
106
+ once,
107
+ off,
108
+ clear,
109
+ getListenerCount: (event?: string) => {
110
+ if (event) {
111
+ return listeners.get(event)?.size || 0;
112
+ }
113
+ let total = 0;
114
+ listeners.forEach((set) => (total += set.size));
115
+ return total;
116
+ },
117
+ };
118
+
119
+ // Clean up on destroy
120
+ if ("destroy" in component && typeof component.destroy === "function") {
121
+ const originalDestroy = component.destroy;
122
+ component.destroy = () => {
123
+ clear();
124
+ originalDestroy?.();
125
+ };
126
+ }
127
+
128
+ return component;
129
+ };
130
+ };
@@ -0,0 +1,20 @@
1
+ // src/core/list-manager/features/viewport/index.ts
2
+
3
+ export { withBase } from "./base";
4
+ export { withCollection } from "./collection";
5
+ export { withEvents } from "./events";
6
+ export { withPlaceholders } from "./placeholders";
7
+ export { withRendering } from "./rendering";
8
+ export { withScrollbar } from "./scrollbar";
9
+ export { withScrolling } from "./scrolling";
10
+ export { withVirtual } from "./virtual";
11
+ export { withPerformance } from "./performance";
12
+ export { withMomentum } from "./momentum";
13
+
14
+ // Utility exports
15
+ export { createItemSizeManager } from "./item-size";
16
+ export { createLoadingManager } from "./loading";
17
+ // No createElementFromTemplate util here; remove incorrect export
18
+
19
+ // Types
20
+ export type { PlaceholderComponent, PlaceholderConfig } from "./placeholders";
@@ -9,7 +9,7 @@ export interface ItemSizeManager {
9
9
  measureItem(
10
10
  element: HTMLElement,
11
11
  index: number,
12
- orientation?: "vertical" | "horizontal"
12
+ orientation?: "vertical" | "horizontal",
13
13
  ): number;
14
14
 
15
15
  // Cache management
@@ -20,8 +20,8 @@ export interface ItemSizeManager {
20
20
  clearCache(): void;
21
21
 
22
22
  // Size estimation
23
- getEstimatedItemSize(): number;
24
- updateEstimatedSize(): void;
23
+ getItemSize(): number;
24
+ updateItemSize(): void;
25
25
 
26
26
  // Additional utilities
27
27
  calculateTotalSize(totalItems?: number): number;
@@ -29,7 +29,7 @@ export interface ItemSizeManager {
29
29
 
30
30
  // Callbacks
31
31
  onSizeUpdated?: (totalSize: number) => void;
32
- onEstimatedSizeChanged?: (newEstimate: number) => void;
32
+ onItemSizeChanged?: (newEstimate: number) => void;
33
33
  }
34
34
 
35
35
  export interface ItemSizeConfig {
@@ -37,26 +37,26 @@ export interface ItemSizeConfig {
37
37
  orientation?: "vertical" | "horizontal";
38
38
  cacheSize?: number;
39
39
  onSizeUpdated?: (totalSize: number) => void;
40
- onEstimatedSizeChanged?: (newEstimate: number) => void;
40
+ onItemSizeChanged?: (newEstimate: number) => void;
41
41
  }
42
42
 
43
43
  /**
44
44
  * Creates an item size manager for measuring and caching item dimensions
45
45
  */
46
46
  export const createItemSizeManager = (
47
- config: ItemSizeConfig = {}
47
+ config: ItemSizeConfig = {},
48
48
  ): ItemSizeManager => {
49
49
  const {
50
50
  initialEstimate = 60,
51
51
  orientation = "vertical",
52
52
  cacheSize = 1000,
53
53
  onSizeUpdated,
54
- onEstimatedSizeChanged,
54
+ onItemSizeChanged,
55
55
  } = config;
56
56
 
57
57
  // Size cache - stores actual measured sizes
58
58
  const measuredSizes = new Map<number, number>();
59
- let currentEstimatedItemSize = initialEstimate;
59
+ let currentItemSize = initialEstimate;
60
60
 
61
61
  // Batching state for performance optimization
62
62
  let batchUpdateTimeout: number | null = null;
@@ -82,7 +82,7 @@ export const createItemSizeManager = (
82
82
  */
83
83
  const triggerBatchedUpdates = (): void => {
84
84
  // Update estimated size based on all measurements
85
- updateEstimatedSize();
85
+ updateItemSize();
86
86
 
87
87
  // Notify about total size update
88
88
  if (onSizeUpdated) {
@@ -116,10 +116,10 @@ export const createItemSizeManager = (
116
116
  const measureItem = (
117
117
  element: HTMLElement,
118
118
  index: number,
119
- measureOrientation?: "vertical" | "horizontal"
119
+ measureOrientation?: "vertical" | "horizontal",
120
120
  ): number => {
121
121
  if (!element || index < 0) {
122
- return currentEstimatedItemSize;
122
+ return currentItemSize;
123
123
  }
124
124
 
125
125
  const actualOrientation = measureOrientation || orientation;
@@ -144,13 +144,13 @@ export const createItemSizeManager = (
144
144
  return size;
145
145
  }
146
146
 
147
- return currentEstimatedItemSize;
147
+ return currentItemSize;
148
148
  };
149
149
 
150
150
  /**
151
151
  * Update estimated item size based on measured sizes (with change threshold)
152
152
  */
153
- const updateEstimatedSize = (): void => {
153
+ const updateItemSize = (): void => {
154
154
  if (measuredSizes.size === 0) return;
155
155
 
156
156
  const sizes = Array.from(measuredSizes.values());
@@ -158,22 +158,19 @@ export const createItemSizeManager = (
158
158
  const newEstimate = Math.max(1, Math.round(average));
159
159
 
160
160
  // Only update if the change is significant (>2px or >5% change)
161
- const changeThreshold = Math.max(
162
- 2,
163
- Math.round(currentEstimatedItemSize * 0.05)
164
- );
165
- const absoluteChange = Math.abs(newEstimate - currentEstimatedItemSize);
161
+ const changeThreshold = Math.max(2, Math.round(currentItemSize * 0.05));
162
+ const absoluteChange = Math.abs(newEstimate - currentItemSize);
166
163
 
167
164
  if (absoluteChange >= changeThreshold) {
168
- const previousEstimate = currentEstimatedItemSize;
169
- currentEstimatedItemSize = newEstimate;
165
+ const previousEstimate = currentItemSize;
166
+ currentItemSize = newEstimate;
170
167
 
171
- if (onEstimatedSizeChanged) {
172
- onEstimatedSizeChanged(newEstimate);
168
+ if (onItemSizeChanged) {
169
+ onItemSizeChanged(newEstimate);
173
170
  }
174
171
  } else if (absoluteChange > 0) {
175
172
  // Silent update for small changes
176
- currentEstimatedItemSize = newEstimate;
173
+ currentItemSize = newEstimate;
177
174
  }
178
175
  };
179
176
 
@@ -185,13 +182,13 @@ export const createItemSizeManager = (
185
182
  // Calculate based on measured items only
186
183
  return Array.from(measuredSizes.values()).reduce(
187
184
  (sum, size) => sum + size,
188
- 0
185
+ 0,
189
186
  );
190
187
  }
191
188
 
192
189
  let totalSize = 0;
193
190
  for (let i = 0; i < totalItems; i++) {
194
- totalSize += measuredSizes.get(i) || currentEstimatedItemSize;
191
+ totalSize += measuredSizes.get(i) || currentItemSize;
195
192
  }
196
193
  return totalSize;
197
194
  };
@@ -200,7 +197,7 @@ export const createItemSizeManager = (
200
197
  * Get size for a specific item (measured or estimated)
201
198
  */
202
199
  const getMeasuredSize = (index: number): number => {
203
- return measuredSizes.get(index) || currentEstimatedItemSize;
200
+ return measuredSizes.get(index) || currentItemSize;
204
201
  };
205
202
 
206
203
  /**
@@ -221,8 +218,8 @@ export const createItemSizeManager = (
221
218
  /**
222
219
  * Get current estimated item size
223
220
  */
224
- const getEstimatedItemSize = (): number => {
225
- return currentEstimatedItemSize;
221
+ const getItemSize = (): number => {
222
+ return currentItemSize;
226
223
  };
227
224
 
228
225
  /**
@@ -238,16 +235,16 @@ export const createItemSizeManager = (
238
235
  const getStats = () => {
239
236
  return {
240
237
  cachedItems: measuredSizes.size,
241
- estimatedSize: currentEstimatedItemSize,
238
+ estimatedSize: currentItemSize,
242
239
  cacheSize: cacheSize,
243
240
  minSize:
244
241
  measuredSizes.size > 0
245
242
  ? Math.min(...measuredSizes.values())
246
- : currentEstimatedItemSize,
243
+ : currentItemSize,
247
244
  maxSize:
248
245
  measuredSizes.size > 0
249
246
  ? Math.max(...measuredSizes.values())
250
- : currentEstimatedItemSize,
247
+ : currentItemSize,
251
248
  orientation: orientation,
252
249
  };
253
250
  };
@@ -259,8 +256,8 @@ export const createItemSizeManager = (
259
256
  getMeasuredSize,
260
257
  getMeasuredSizes,
261
258
  clearCache,
262
- getEstimatedItemSize,
263
- updateEstimatedSize,
259
+ getItemSize,
260
+ updateItemSize,
264
261
  cacheItemSize,
265
262
 
266
263
  // Additional utilities
@@ -269,6 +266,6 @@ export const createItemSizeManager = (
269
266
 
270
267
  // Callbacks
271
268
  onSizeUpdated,
272
- onEstimatedSizeChanged,
269
+ onItemSizeChanged,
273
270
  };
274
271
  };
@@ -5,9 +5,9 @@
5
5
  * When scrolling fast, it cancels loads to prevent server overload.
6
6
  */
7
7
 
8
- import type { ListManagerComponent, ItemRange } from "../../types";
9
- import { LIST_MANAGER_CONSTANTS } from "../../constants";
10
- import { VIEWPORT_CONSTANTS } from "./constants";
8
+ import type { ItemRange } from "../types";
9
+ type ListManagerComponent = any;
10
+ import { VIEWPORT_CONSTANTS } from "../constants";
11
11
 
12
12
  export interface LoadingConfig {
13
13
  cancelLoadThreshold?: number; // Velocity (px/ms) above which all loads are cancelled
@@ -196,7 +196,7 @@ export const createLoadingManager = (
196
196
  const collection = (component as any).collection;
197
197
  if (collection && typeof collection.loadMissingRanges === "function") {
198
198
  collection
199
- .loadMissingRanges(range)
199
+ .loadMissingRanges(range, "loading:loadRange")
200
200
  .then(() => {
201
201
  activeRequests--;
202
202
  activeRanges.delete(rangeKey);
@@ -0,0 +1,269 @@
1
+ // src/core/viewport/features/momentum.ts
2
+
3
+ /**
4
+ * Momentum Feature - Adds inertial scrolling to viewport
5
+ * Provides smooth deceleration after touch/mouse drag gestures
6
+ */
7
+
8
+ import type { ViewportComponent, ViewportContext } from "../types";
9
+ import { VIEWPORT_CONSTANTS } from "../constants";
10
+
11
+ interface MomentumConfig {
12
+ enabled?: boolean;
13
+ deceleration?: number;
14
+ minVelocity?: number;
15
+ minDuration?: number;
16
+ minVelocityThreshold?: number;
17
+ }
18
+
19
+ /**
20
+ * Adds momentum scrolling to viewport
21
+ */
22
+ export const withMomentum = (config: MomentumConfig = {}) => {
23
+ return <T extends ViewportContext & ViewportComponent>(component: T): T => {
24
+ const {
25
+ enabled = VIEWPORT_CONSTANTS.MOMENTUM.ENABLED,
26
+ deceleration = VIEWPORT_CONSTANTS.MOMENTUM.DECELERATION_FACTOR,
27
+ minVelocity = VIEWPORT_CONSTANTS.MOMENTUM.MIN_VELOCITY,
28
+ minDuration = VIEWPORT_CONSTANTS.MOMENTUM.MIN_DURATION,
29
+ minVelocityThreshold = VIEWPORT_CONSTANTS.MOMENTUM.MIN_VELOCITY_THRESHOLD,
30
+ } = config;
31
+
32
+ // Always apply the feature, but check isEnabled at runtime
33
+
34
+ // Momentum state
35
+ let momentumAnimationId: number | null = null;
36
+ let isTouching = false;
37
+ let isMouseDragging = false;
38
+ let lastTouchPosition = 0;
39
+ let lastMousePosition = 0;
40
+ let touchStartTime = 0;
41
+ let viewportState: any;
42
+ let scrollingState: any;
43
+ let isEnabled = enabled; // Track current enabled state
44
+
45
+ // Get references after initialization
46
+ const originalInitialize = component.viewport.initialize;
47
+ component.viewport.initialize = () => {
48
+ const result = originalInitialize();
49
+ // Skip if already initialized
50
+ if (result === false) {
51
+ return false;
52
+ }
53
+
54
+ // Get viewport and scrolling states
55
+ viewportState = (component.viewport as any).state;
56
+ scrollingState = (component.viewport as any).scrollingState;
57
+ return result;
58
+ };
59
+
60
+ // Start momentum animation
61
+ const startMomentum = (initialVelocity: number) => {
62
+ // Cancel any existing momentum
63
+ if (momentumAnimationId) {
64
+ cancelAnimationFrame(momentumAnimationId);
65
+ }
66
+
67
+ let velocity = initialVelocity;
68
+
69
+ const animate = () => {
70
+ // Apply deceleration
71
+ velocity *= deceleration;
72
+
73
+ // Stop if velocity is too small
74
+ if (Math.abs(velocity) < minVelocity) {
75
+ momentumAnimationId = null;
76
+ // Reset velocity in scrolling feature
77
+ if (scrollingState) {
78
+ scrollingState.setVelocityToZero?.();
79
+ }
80
+ return;
81
+ }
82
+
83
+ // Calculate scroll delta (velocity is in px/ms, so multiply by frame time)
84
+ const frameDelta = velocity * VIEWPORT_CONSTANTS.MOMENTUM.FRAME_TIME;
85
+
86
+ // Use scrolling feature's API to update position
87
+ if (component.viewport.scrollBy) {
88
+ component.viewport.scrollBy(frameDelta);
89
+
90
+ // Continue animation
91
+ momentumAnimationId = requestAnimationFrame(animate);
92
+ } else {
93
+ // No scrollBy method, stop momentum
94
+ momentumAnimationId = null;
95
+ }
96
+ };
97
+
98
+ momentumAnimationId = requestAnimationFrame(animate);
99
+ };
100
+
101
+ // Touch event handlers
102
+ const handleTouchStart = (e: TouchEvent) => {
103
+ isTouching = true;
104
+ const touch = e.touches[0];
105
+ const orientation = viewportState?.orientation || "vertical";
106
+ lastTouchPosition =
107
+ orientation === "vertical" ? touch.clientY : touch.clientX;
108
+ touchStartTime = Date.now();
109
+
110
+ // Cancel any ongoing momentum
111
+ if (momentumAnimationId) {
112
+ cancelAnimationFrame(momentumAnimationId);
113
+ momentumAnimationId = null;
114
+ }
115
+ };
116
+
117
+ const handleTouchMove = (e: TouchEvent) => {
118
+ if (!isTouching) return;
119
+
120
+ e.preventDefault(); // Prevent native scrolling
121
+
122
+ const touch = e.touches[0];
123
+ const orientation = viewportState?.orientation || "vertical";
124
+ const currentPosition =
125
+ orientation === "vertical" ? touch.clientY : touch.clientX;
126
+ const delta = lastTouchPosition - currentPosition; // Inverted for natural scrolling
127
+ lastTouchPosition = currentPosition;
128
+
129
+ // Use scrolling feature's API
130
+ if (component.viewport.scrollBy) {
131
+ component.viewport.scrollBy(delta);
132
+ }
133
+ };
134
+
135
+ const handleTouchEnd = () => {
136
+ if (!isTouching) return;
137
+ isTouching = false;
138
+
139
+ const touchEndTime = Date.now();
140
+ const touchDuration = touchEndTime - touchStartTime;
141
+
142
+ // Get current velocity from scrolling feature
143
+ const velocity = component.viewport.getVelocity?.() || 0;
144
+
145
+ // Only start momentum if the touch was quick enough and we have velocity
146
+ if (
147
+ touchDuration < minDuration &&
148
+ Math.abs(velocity) > minVelocityThreshold
149
+ ) {
150
+ startMomentum(velocity);
151
+ }
152
+ };
153
+
154
+ // Mouse event handlers for desktop
155
+ const handleMouseDown = (e: MouseEvent) => {
156
+ isMouseDragging = true;
157
+ const orientation = viewportState?.orientation || "vertical";
158
+ lastMousePosition = orientation === "vertical" ? e.clientY : e.clientX;
159
+ touchStartTime = Date.now();
160
+
161
+ // Cancel any ongoing momentum
162
+ if (momentumAnimationId) {
163
+ cancelAnimationFrame(momentumAnimationId);
164
+ momentumAnimationId = null;
165
+ }
166
+
167
+ // Prevent text selection
168
+ e.preventDefault();
169
+ };
170
+
171
+ const handleMouseMove = (e: MouseEvent) => {
172
+ if (!isMouseDragging) return;
173
+
174
+ const orientation = viewportState?.orientation || "vertical";
175
+ const currentPosition =
176
+ orientation === "vertical" ? e.clientY : e.clientX;
177
+ const delta = lastMousePosition - currentPosition;
178
+ lastMousePosition = currentPosition;
179
+
180
+ // Use scrolling feature's API
181
+ if (component.viewport.scrollBy) {
182
+ component.viewport.scrollBy(delta);
183
+ }
184
+ };
185
+
186
+ const handleMouseUp = () => {
187
+ if (!isMouseDragging) return;
188
+ isMouseDragging = false;
189
+
190
+ const mouseUpTime = Date.now();
191
+ const dragDuration = mouseUpTime - touchStartTime;
192
+
193
+ // Get current velocity from scrolling feature
194
+ const velocity = component.viewport.getVelocity?.() || 0;
195
+
196
+ // Start momentum for quick drags
197
+ if (
198
+ dragDuration < minDuration &&
199
+ Math.abs(velocity) > minVelocityThreshold
200
+ ) {
201
+ startMomentum(velocity);
202
+ }
203
+ };
204
+
205
+ // Override initialize to add event listeners
206
+ const originalInit = component.viewport.initialize;
207
+ component.viewport.initialize = () => {
208
+ const result = originalInit();
209
+ // Skip if already initialized
210
+ if (result === false) {
211
+ return false;
212
+ }
213
+
214
+ const viewportElement =
215
+ (component as any).viewportElement ||
216
+ (component.viewport as any).state?.viewportElement;
217
+
218
+ if (viewportElement) {
219
+ // Touch events
220
+ viewportElement.addEventListener("touchstart", handleTouchStart, {
221
+ passive: true,
222
+ });
223
+ viewportElement.addEventListener("touchmove", handleTouchMove, {
224
+ passive: false,
225
+ });
226
+ viewportElement.addEventListener("touchend", handleTouchEnd, {
227
+ passive: true,
228
+ });
229
+
230
+ // Mouse events for desktop dragging
231
+ viewportElement.addEventListener("mousedown", handleMouseDown);
232
+ viewportElement.addEventListener("mousemove", handleMouseMove);
233
+ viewportElement.addEventListener("mouseup", handleMouseUp);
234
+ viewportElement.addEventListener("mouseleave", handleMouseUp);
235
+
236
+ // Store reference for cleanup
237
+ (component as any)._momentumViewportElement = viewportElement;
238
+ }
239
+ };
240
+
241
+ // Clean up on destroy
242
+ if ("destroy" in component && typeof component.destroy === "function") {
243
+ const originalDestroy = component.destroy;
244
+ component.destroy = () => {
245
+ // Remove event listeners
246
+ const viewportElement = (component as any)._momentumViewportElement;
247
+ if (viewportElement) {
248
+ viewportElement.removeEventListener("touchstart", handleTouchStart);
249
+ viewportElement.removeEventListener("touchmove", handleTouchMove);
250
+ viewportElement.removeEventListener("touchend", handleTouchEnd);
251
+ viewportElement.removeEventListener("mousedown", handleMouseDown);
252
+ viewportElement.removeEventListener("mousemove", handleMouseMove);
253
+ viewportElement.removeEventListener("mouseup", handleMouseUp);
254
+ viewportElement.removeEventListener("mouseleave", handleMouseUp);
255
+ }
256
+
257
+ // Cancel momentum animation
258
+ if (momentumAnimationId) {
259
+ cancelAnimationFrame(momentumAnimationId);
260
+ momentumAnimationId = null;
261
+ }
262
+
263
+ originalDestroy();
264
+ };
265
+ }
266
+
267
+ return component;
268
+ };
269
+ };