mtrl-addons 0.1.2 → 0.2.1

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 (115) hide show
  1. package/build.js +139 -86
  2. package/package.json +13 -4
  3. package/scripts/debug/vlist-selection.ts +121 -0
  4. package/src/components/index.ts +5 -41
  5. package/src/components/{list → vlist}/config.ts +66 -95
  6. package/src/components/vlist/constants.ts +23 -0
  7. package/src/components/vlist/features/api.ts +322 -0
  8. package/src/components/vlist/features/index.ts +10 -0
  9. package/src/components/vlist/features/selection.ts +444 -0
  10. package/src/components/vlist/features/viewport.ts +65 -0
  11. package/src/components/vlist/index.ts +16 -0
  12. package/src/components/{list → vlist}/types.ts +104 -26
  13. package/src/components/vlist/vlist.ts +92 -0
  14. package/src/core/compose/features/gestures/index.ts +227 -0
  15. package/src/core/compose/features/gestures/longpress.ts +383 -0
  16. package/src/core/compose/features/gestures/pan.ts +424 -0
  17. package/src/core/compose/features/gestures/pinch.ts +475 -0
  18. package/src/core/compose/features/gestures/rotate.ts +485 -0
  19. package/src/core/compose/features/gestures/swipe.ts +492 -0
  20. package/src/core/compose/features/gestures/tap.ts +334 -0
  21. package/src/core/compose/features/index.ts +2 -38
  22. package/src/core/compose/index.ts +13 -29
  23. package/src/core/gestures/index.ts +31 -0
  24. package/src/core/gestures/longpress.ts +68 -0
  25. package/src/core/gestures/manager.ts +418 -0
  26. package/src/core/gestures/pan.ts +48 -0
  27. package/src/core/gestures/pinch.ts +58 -0
  28. package/src/core/gestures/rotate.ts +58 -0
  29. package/src/core/gestures/swipe.ts +66 -0
  30. package/src/core/gestures/tap.ts +45 -0
  31. package/src/core/gestures/types.ts +387 -0
  32. package/src/core/gestures/utils.ts +128 -0
  33. package/src/core/index.ts +27 -151
  34. package/src/core/layout/schema.ts +73 -35
  35. package/src/core/layout/types.ts +5 -2
  36. package/src/core/viewport/constants.ts +140 -0
  37. package/src/core/viewport/features/base.ts +73 -0
  38. package/src/core/viewport/features/collection.ts +882 -0
  39. package/src/core/viewport/features/events.ts +130 -0
  40. package/src/core/viewport/features/index.ts +20 -0
  41. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +27 -30
  42. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  43. package/src/core/viewport/features/momentum.ts +260 -0
  44. package/src/core/viewport/features/placeholders.ts +335 -0
  45. package/src/core/viewport/features/rendering.ts +568 -0
  46. package/src/core/viewport/features/scrollbar.ts +434 -0
  47. package/src/core/viewport/features/scrolling.ts +618 -0
  48. package/src/core/viewport/features/utils.ts +88 -0
  49. package/src/core/viewport/features/virtual.ts +384 -0
  50. package/src/core/viewport/index.ts +31 -0
  51. package/src/core/viewport/types.ts +133 -0
  52. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  53. package/src/core/viewport/viewport.ts +246 -0
  54. package/src/index.ts +0 -7
  55. package/src/styles/components/_vlist.scss +331 -0
  56. package/src/styles/index.scss +1 -1
  57. package/test/components/vlist-selection.test.ts +240 -0
  58. package/test/components/vlist.test.ts +63 -0
  59. package/test/core/collection/adapter.test.ts +161 -0
  60. package/bun.lock +0 -792
  61. package/src/components/list/api.ts +0 -314
  62. package/src/components/list/constants.ts +0 -56
  63. package/src/components/list/features/api.ts +0 -428
  64. package/src/components/list/features/index.ts +0 -31
  65. package/src/components/list/features/list-manager.ts +0 -502
  66. package/src/components/list/index.ts +0 -39
  67. package/src/components/list/list.ts +0 -234
  68. package/src/core/collection/base-collection.ts +0 -100
  69. package/src/core/collection/collection-composer.ts +0 -178
  70. package/src/core/collection/collection.ts +0 -745
  71. package/src/core/collection/constants.ts +0 -172
  72. package/src/core/collection/events.ts +0 -428
  73. package/src/core/collection/features/api/loading.ts +0 -279
  74. package/src/core/collection/features/operations/data-operations.ts +0 -147
  75. package/src/core/collection/index.ts +0 -104
  76. package/src/core/collection/state.ts +0 -497
  77. package/src/core/collection/types.ts +0 -404
  78. package/src/core/compose/features/collection.ts +0 -119
  79. package/src/core/compose/features/selection.ts +0 -213
  80. package/src/core/compose/features/styling.ts +0 -108
  81. package/src/core/list-manager/api.ts +0 -599
  82. package/src/core/list-manager/config.ts +0 -593
  83. package/src/core/list-manager/constants.ts +0 -268
  84. package/src/core/list-manager/features/api.ts +0 -58
  85. package/src/core/list-manager/features/collection/collection.ts +0 -705
  86. package/src/core/list-manager/features/collection/index.ts +0 -17
  87. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  88. package/src/core/list-manager/features/viewport/index.ts +0 -16
  89. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  90. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  91. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  92. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  93. package/src/core/list-manager/features/viewport/template.ts +0 -220
  94. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  95. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  96. package/src/core/list-manager/index.ts +0 -279
  97. package/src/core/list-manager/list-manager.ts +0 -206
  98. package/src/core/list-manager/types.ts +0 -439
  99. package/src/core/list-manager/utils/calculations.ts +0 -290
  100. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  101. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  102. package/src/styles/components/_list.scss +0 -244
  103. package/src/types/mtrl.d.ts +0 -6
  104. package/test/components/list.test.ts +0 -256
  105. package/test/core/collection/failed-ranges.test.ts +0 -270
  106. package/test/core/compose/features.test.ts +0 -183
  107. package/test/core/list-manager/features/collection.test.ts +0 -704
  108. package/test/core/list-manager/features/viewport.test.ts +0 -698
  109. package/test/core/list-manager/list-manager.test.ts +0 -593
  110. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  111. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  112. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  113. package/tsconfig.build.json +0 -23
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -0,0 +1,485 @@
1
+ // src/core/compose/features/gestures/rotate.ts
2
+ /**
3
+ * @module core/compose/features/gestures
4
+ * @description Adds rotate gesture recognition to components
5
+ */
6
+
7
+ import type { BaseComponent, ElementComponent } from "mtrl";
8
+ import { RotateEvent, GestureHandler } from "../../../gestures";
9
+ import { getAngle } from "../../../gestures/utils";
10
+ import { hasLifecycle, hasEmit } from "mtrl";
11
+
12
+ /**
13
+ * Extend the RotateEvent interface to support our custom event types
14
+ */
15
+ interface ExtendedRotateEvent extends Omit<RotateEvent, "type"> {
16
+ type: "rotate" | "rotatestart" | "rotateend";
17
+ }
18
+
19
+ /**
20
+ * Configuration for rotate gesture feature
21
+ */
22
+ export interface RotateGestureConfig {
23
+ /**
24
+ * Whether to prevent default behaviors on touch events
25
+ * @default true
26
+ */
27
+ preventDefault?: boolean;
28
+
29
+ /**
30
+ * Handler for rotate gesture
31
+ */
32
+ onRotate?: GestureHandler;
33
+
34
+ /**
35
+ * Handler for rotate start
36
+ */
37
+ onRotateStart?: GestureHandler;
38
+
39
+ /**
40
+ * Handler for rotate end
41
+ */
42
+ onRotateEnd?: GestureHandler;
43
+
44
+ /**
45
+ * Whether to enable rotate recognition immediately
46
+ * @default true
47
+ */
48
+ enabled?: boolean;
49
+
50
+ [key: string]: any;
51
+ }
52
+
53
+ /**
54
+ * Component with rotate gesture recognition capabilities
55
+ */
56
+ export interface RotateGestureComponent extends BaseComponent {
57
+ /**
58
+ * Add a rotate event handler
59
+ * @param handler - Event handler function
60
+ * @returns Component for chaining
61
+ */
62
+ onRotate: (handler: (event: RotateEvent) => void) => RotateGestureComponent;
63
+
64
+ /**
65
+ * Add a rotate start event handler
66
+ * @param handler - Event handler function
67
+ * @returns Component for chaining
68
+ */
69
+ onRotateStart: (
70
+ handler: (event: RotateEvent) => void
71
+ ) => RotateGestureComponent;
72
+
73
+ /**
74
+ * Add a rotate end event handler
75
+ * @param handler - Event handler function
76
+ * @returns Component for chaining
77
+ */
78
+ onRotateEnd: (
79
+ handler: (event: RotateEvent) => void
80
+ ) => RotateGestureComponent;
81
+
82
+ /**
83
+ * Remove a rotate event handler
84
+ * @param handler - Event handler function
85
+ * @returns Component for chaining
86
+ */
87
+ offRotate: (handler: (event: RotateEvent) => void) => RotateGestureComponent;
88
+
89
+ /**
90
+ * Remove a rotate start event handler
91
+ * @param handler - Event handler function
92
+ * @returns Component for chaining
93
+ */
94
+ offRotateStart: (
95
+ handler: (event: RotateEvent) => void
96
+ ) => RotateGestureComponent;
97
+
98
+ /**
99
+ * Remove a rotate end event handler
100
+ * @param handler - Event handler function
101
+ * @returns Component for chaining
102
+ */
103
+ offRotateEnd: (
104
+ handler: (event: RotateEvent) => void
105
+ ) => RotateGestureComponent;
106
+
107
+ /**
108
+ * Enable rotate recognition
109
+ * @returns Component for chaining
110
+ */
111
+ enableRotate: () => RotateGestureComponent;
112
+
113
+ /**
114
+ * Disable rotate recognition
115
+ * @returns Component for chaining
116
+ */
117
+ disableRotate: () => RotateGestureComponent;
118
+
119
+ /**
120
+ * Check if rotate gestures are supported on the current device
121
+ * @returns Whether rotate gestures are supported
122
+ */
123
+ isRotateSupported: () => boolean;
124
+ }
125
+
126
+ /**
127
+ * Adds rotate gesture recognition to a component.
128
+ * This is a lightweight alternative to the full gesture system,
129
+ * focused only on rotate detection.
130
+ *
131
+ * @param config - Configuration object containing rotate settings
132
+ * @returns Function that enhances a component with rotate capabilities
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * // Add rotate gesture recognition to a component
137
+ * const component = pipe(
138
+ * createBase,
139
+ * withElement(...),
140
+ * withRotateGesture({
141
+ * onRotate: (e) => updateRotation(e.rotation)
142
+ * })
143
+ * )(config);
144
+ * ```
145
+ */
146
+ export const withRotateGesture =
147
+ (config: RotateGestureConfig = {}) =>
148
+ <C extends ElementComponent>(component: C): C & RotateGestureComponent => {
149
+ if (!component.element) {
150
+ console.warn("Cannot add rotate gesture recognition: missing element");
151
+ return component as C & RotateGestureComponent;
152
+ }
153
+
154
+ // Default configuration
155
+ const {
156
+ preventDefault = true,
157
+ onRotate,
158
+ onRotateStart,
159
+ onRotateEnd,
160
+ enabled = true,
161
+ } = config;
162
+
163
+ // Event handlers storage
164
+ const handlers = {
165
+ rotate: new Set<(event: RotateEvent) => void>(),
166
+ rotatestart: new Set<(event: RotateEvent) => void>(),
167
+ rotateend: new Set<(event: RotateEvent) => void>(),
168
+ };
169
+
170
+ // Add initial handlers if provided
171
+ if (onRotate) handlers.rotate.add(onRotate as (event: RotateEvent) => void);
172
+ if (onRotateStart)
173
+ handlers.rotatestart.add(onRotateStart as (event: RotateEvent) => void);
174
+ if (onRotateEnd)
175
+ handlers.rotateend.add(onRotateEnd as (event: RotateEvent) => void);
176
+
177
+ // Check if device supports touch (required for rotate)
178
+ const isTouchSupported =
179
+ "ontouchstart" in window || navigator.maxTouchPoints > 0;
180
+
181
+ // Gesture state for tracking
182
+ let startAngle = 0;
183
+ let lastRotation = 0;
184
+ let isRotating = false;
185
+ let startTime = 0;
186
+ let isEnabled = enabled;
187
+
188
+ /**
189
+ * Create a rotate event
190
+ */
191
+ const createRotateEvent = (
192
+ e: TouchEvent,
193
+ type: "rotate" | "rotatestart" | "rotateend",
194
+ rotation: number,
195
+ centerX: number,
196
+ centerY: number
197
+ ): ExtendedRotateEvent => {
198
+ const event: ExtendedRotateEvent = {
199
+ type,
200
+ originalEvent: e,
201
+ target: e.target!,
202
+ startTime,
203
+ endTime: Date.now(),
204
+ duration: Date.now() - startTime,
205
+ defaultPrevented: false,
206
+ preventDefault: () => {
207
+ event.defaultPrevented = true;
208
+ if (e.cancelable) {
209
+ e.preventDefault();
210
+ }
211
+ },
212
+ stopPropagation: () => {
213
+ e.stopPropagation();
214
+ },
215
+ rotation,
216
+ centerX,
217
+ centerY,
218
+ };
219
+ return event;
220
+ };
221
+
222
+ /**
223
+ * Dispatch a rotate event to registered handlers
224
+ */
225
+ const dispatchRotateEvent = (
226
+ e: TouchEvent,
227
+ type: "rotate" | "rotatestart" | "rotateend",
228
+ rotation: number,
229
+ centerX: number,
230
+ centerY: number
231
+ ): void => {
232
+ const extendedRotateEvent = createRotateEvent(
233
+ e,
234
+ type,
235
+ rotation,
236
+ centerX,
237
+ centerY
238
+ );
239
+
240
+ // Call each handler for this type
241
+ handlers[type].forEach((handler) => {
242
+ try {
243
+ // Type assertion to handle the extended type
244
+ handler(extendedRotateEvent as unknown as RotateEvent);
245
+ } catch (error) {
246
+ console.error(`Error in ${type} handler:`, error);
247
+ }
248
+ });
249
+
250
+ // Forward to component's event system if available
251
+ if (hasEmit(component)) {
252
+ component.emit(type, extendedRotateEvent);
253
+ }
254
+
255
+ // Apply preventDefault if configured
256
+ if (preventDefault && !extendedRotateEvent.defaultPrevented) {
257
+ extendedRotateEvent.preventDefault();
258
+ }
259
+ };
260
+
261
+ /**
262
+ * Handle touch start
263
+ */
264
+ const handleTouchStart = (e: TouchEvent): void => {
265
+ if (!isEnabled || e.touches.length !== 2) return;
266
+
267
+ // Calculate initial angle between touch points
268
+ const touch1 = e.touches[0];
269
+ const touch2 = e.touches[1];
270
+
271
+ startAngle = getAngle(
272
+ touch1.clientX,
273
+ touch1.clientY,
274
+ touch2.clientX,
275
+ touch2.clientY
276
+ );
277
+
278
+ startTime = Date.now();
279
+ lastRotation = 0;
280
+ isRotating = false;
281
+ };
282
+
283
+ /**
284
+ * Handle touch move
285
+ */
286
+ const handleTouchMove = (e: TouchEvent): void => {
287
+ if (!isEnabled || e.touches.length !== 2) return;
288
+
289
+ const touch1 = e.touches[0];
290
+ const touch2 = e.touches[1];
291
+
292
+ // Calculate current angle and rotation
293
+ const currentAngle = getAngle(
294
+ touch1.clientX,
295
+ touch1.clientY,
296
+ touch2.clientX,
297
+ touch2.clientY
298
+ );
299
+
300
+ // Calculate rotation in degrees (normalized to -180 to 180)
301
+ let rotation = currentAngle - startAngle;
302
+
303
+ // Normalize rotation to the -180 to 180 range
304
+ if (rotation > 180) rotation -= 360;
305
+ if (rotation < -180) rotation += 360;
306
+
307
+ const centerX = (touch1.clientX + touch2.clientX) / 2;
308
+ const centerY = (touch1.clientY + touch2.clientY) / 2;
309
+
310
+ // Check if rotation change is significant enough
311
+ const rotationDiff = Math.abs(rotation - lastRotation);
312
+ const ROTATION_THRESHOLD = 2; // degrees
313
+
314
+ if (rotationDiff > ROTATION_THRESHOLD) {
315
+ // Start rotate if this is the first significant rotation
316
+ if (!isRotating) {
317
+ isRotating = true;
318
+ dispatchRotateEvent(e, "rotatestart", rotation, centerX, centerY);
319
+ }
320
+
321
+ // Dispatch continuous rotate event
322
+ dispatchRotateEvent(e, "rotate", rotation, centerX, centerY);
323
+ lastRotation = rotation;
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Handle touch end
329
+ */
330
+ const handleTouchEnd = (e: TouchEvent): void => {
331
+ if (!isEnabled || !isRotating) return;
332
+
333
+ // Calculate final center point if we still have one finger on screen
334
+ let centerX = 0;
335
+ let centerY = 0;
336
+
337
+ if (e.touches.length === 1) {
338
+ centerX = e.touches[0].clientX;
339
+ centerY = e.touches[0].clientY;
340
+ } else if (e.changedTouches.length > 0) {
341
+ centerX = e.changedTouches[0].clientX;
342
+ centerY = e.changedTouches[0].clientY;
343
+ }
344
+
345
+ // Dispatch rotate end event
346
+ dispatchRotateEvent(e, "rotateend", lastRotation, centerX, centerY);
347
+
348
+ // Reset state
349
+ isRotating = false;
350
+ };
351
+
352
+ /**
353
+ * Handle touch cancel
354
+ */
355
+ const handleTouchCancel = (e: TouchEvent): void => {
356
+ if (!isEnabled || !isRotating) return;
357
+
358
+ // Calculate final center point
359
+ let centerX = 0;
360
+ let centerY = 0;
361
+
362
+ if (e.touches.length > 0) {
363
+ centerX = e.touches[0].clientX;
364
+ centerY = e.touches[0].clientY;
365
+ }
366
+
367
+ // Dispatch rotate end event
368
+ dispatchRotateEvent(e, "rotateend", lastRotation, centerX, centerY);
369
+
370
+ // Reset state
371
+ isRotating = false;
372
+ };
373
+
374
+ // Event listeners dictionary
375
+ const eventListeners: Record<string, EventListener> = {
376
+ touchstart: handleTouchStart as EventListener,
377
+ touchmove: handleTouchMove as EventListener,
378
+ touchend: handleTouchEnd as EventListener,
379
+ touchcancel: handleTouchCancel as EventListener,
380
+ };
381
+
382
+ /**
383
+ * Add event listeners to element
384
+ */
385
+ const setupEventListeners = (): void => {
386
+ if (!isTouchSupported) return;
387
+
388
+ Object.entries(eventListeners).forEach(([event, listener]) => {
389
+ component.element.addEventListener(event, listener, {
390
+ passive: !preventDefault,
391
+ });
392
+ });
393
+ };
394
+
395
+ /**
396
+ * Remove event listeners from element
397
+ */
398
+ const removeEventListeners = (): void => {
399
+ if (!isTouchSupported) return;
400
+
401
+ Object.entries(eventListeners).forEach(([event, listener]) => {
402
+ component.element.removeEventListener(event, listener);
403
+ });
404
+ };
405
+
406
+ // Setup listeners if initially enabled
407
+ if (isEnabled && isTouchSupported) {
408
+ setupEventListeners();
409
+ }
410
+
411
+ // Handle lifecycle integration
412
+ if (hasLifecycle(component)) {
413
+ const originalDestroy = component.lifecycle.destroy;
414
+
415
+ component.lifecycle.destroy = () => {
416
+ // Clean up event listeners
417
+ removeEventListeners();
418
+
419
+ // Clear handlers
420
+ Object.values(handlers).forEach((handlerSet) => handlerSet.clear());
421
+
422
+ // Call original destroy method
423
+ originalDestroy.call(component.lifecycle);
424
+ };
425
+ }
426
+
427
+ // Create enhanced component
428
+ return {
429
+ ...component,
430
+
431
+ // Add handler methods
432
+ onRotate(handler: (event: RotateEvent) => void) {
433
+ handlers.rotate.add(handler);
434
+ return this;
435
+ },
436
+
437
+ onRotateStart(handler: (event: RotateEvent) => void) {
438
+ handlers.rotatestart.add(handler);
439
+ return this;
440
+ },
441
+
442
+ onRotateEnd(handler: (event: RotateEvent) => void) {
443
+ handlers.rotateend.add(handler);
444
+ return this;
445
+ },
446
+
447
+ // Remove handler methods
448
+ offRotate(handler: (event: RotateEvent) => void) {
449
+ handlers.rotate.delete(handler);
450
+ return this;
451
+ },
452
+
453
+ offRotateStart(handler: (event: RotateEvent) => void) {
454
+ handlers.rotatestart.delete(handler);
455
+ return this;
456
+ },
457
+
458
+ offRotateEnd(handler: (event: RotateEvent) => void) {
459
+ handlers.rotateend.delete(handler);
460
+ return this;
461
+ },
462
+
463
+ // Enable/disable methods
464
+ enableRotate() {
465
+ if (!isEnabled) {
466
+ isEnabled = true;
467
+ setupEventListeners();
468
+ }
469
+ return this;
470
+ },
471
+
472
+ disableRotate() {
473
+ if (isEnabled) {
474
+ isEnabled = false;
475
+ removeEventListeners();
476
+ }
477
+ return this;
478
+ },
479
+
480
+ // Support check method
481
+ isRotateSupported() {
482
+ return isTouchSupported;
483
+ },
484
+ };
485
+ };