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