mtrl 0.2.0 → 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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtrl",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A functional JavaScript component library with composable architecture based on Material Design 3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"author": "floor",
|
|
25
25
|
"license": "MIT License",
|
|
26
|
-
"
|
|
26
|
+
"devDependencies": {
|
|
27
27
|
"typedoc": "^0.27.9"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/components/checkbox/constants.
|
|
1
|
+
// src/components/checkbox/constants.ts
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Visual variants for checkbox
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export const CHECKBOX_VARIANTS = {
|
|
7
7
|
FILLED: 'filled',
|
|
8
8
|
OUTLINED: 'outlined'
|
|
9
|
-
}
|
|
9
|
+
} as const;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Label position options
|
|
@@ -14,58 +14,7 @@ export const CHECKBOX_VARIANTS = {
|
|
|
14
14
|
export const CHECKBOX_LABEL_POSITION = {
|
|
15
15
|
START: 'start',
|
|
16
16
|
END: 'end'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validation schema for checkbox configuration
|
|
21
|
-
*/
|
|
22
|
-
export const CHECKBOX_SCHEMA = {
|
|
23
|
-
type: 'object',
|
|
24
|
-
properties: {
|
|
25
|
-
name: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
optional: true
|
|
28
|
-
},
|
|
29
|
-
checked: {
|
|
30
|
-
type: 'boolean',
|
|
31
|
-
optional: true
|
|
32
|
-
},
|
|
33
|
-
indeterminate: {
|
|
34
|
-
type: 'boolean',
|
|
35
|
-
optional: true
|
|
36
|
-
},
|
|
37
|
-
required: {
|
|
38
|
-
type: 'boolean',
|
|
39
|
-
optional: true
|
|
40
|
-
},
|
|
41
|
-
disabled: {
|
|
42
|
-
type: 'boolean',
|
|
43
|
-
optional: true
|
|
44
|
-
},
|
|
45
|
-
value: {
|
|
46
|
-
type: 'string',
|
|
47
|
-
optional: true
|
|
48
|
-
},
|
|
49
|
-
label: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
optional: true
|
|
52
|
-
},
|
|
53
|
-
labelPosition: {
|
|
54
|
-
type: 'string',
|
|
55
|
-
enum: Object.values(CHECKBOX_LABEL_POSITION),
|
|
56
|
-
optional: true
|
|
57
|
-
},
|
|
58
|
-
variant: {
|
|
59
|
-
type: 'string',
|
|
60
|
-
enum: Object.values(CHECKBOX_VARIANTS),
|
|
61
|
-
optional: true
|
|
62
|
-
},
|
|
63
|
-
class: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
optional: true
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
17
|
+
} as const;
|
|
69
18
|
|
|
70
19
|
/**
|
|
71
20
|
* Checkbox state classes
|
|
@@ -75,7 +24,7 @@ export const CHECKBOX_STATES = {
|
|
|
75
24
|
INDETERMINATE: 'indeterminate',
|
|
76
25
|
DISABLED: 'disabled',
|
|
77
26
|
FOCUSED: 'focused'
|
|
78
|
-
}
|
|
27
|
+
} as const;
|
|
79
28
|
|
|
80
29
|
/**
|
|
81
30
|
* Checkbox element classes
|
|
@@ -85,4 +34,4 @@ export const CHECKBOX_CLASSES = {
|
|
|
85
34
|
INPUT: 'checkbox-input',
|
|
86
35
|
ICON: 'checkbox-icon',
|
|
87
36
|
LABEL: 'checkbox-label'
|
|
88
|
-
}
|
|
37
|
+
} as const;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/components/switch/constants.
|
|
1
|
+
// src/components/switch/constants.ts
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Label position options
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export const SWITCH_LABEL_POSITION = {
|
|
7
7
|
START: 'start',
|
|
8
8
|
END: 'end'
|
|
9
|
-
}
|
|
9
|
+
} as const;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Validation schema for switch configuration
|
|
@@ -56,7 +56,7 @@ export const SWITCH_SCHEMA = {
|
|
|
56
56
|
optional: true
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
} as const;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Switch state classes
|
|
@@ -65,7 +65,7 @@ export const SWITCH_STATES = {
|
|
|
65
65
|
CHECKED: 'checked',
|
|
66
66
|
DISABLED: 'disabled',
|
|
67
67
|
FOCUSED: 'focused'
|
|
68
|
-
}
|
|
68
|
+
} as const;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Switch element classes
|
|
@@ -77,4 +77,4 @@ export const SWITCH_CLASSES = {
|
|
|
77
77
|
THUMB: 'thumb',
|
|
78
78
|
THUMB_ICON: 'thumb-icon',
|
|
79
79
|
LABEL: 'switch-label'
|
|
80
|
-
}
|
|
80
|
+
} as const;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @description Core utilities for component composition and creation with built-in mobile support
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createElement, CreateElementOptions } from '../dom/create';
|
|
7
|
+
import { createElement, CreateElementOptions, removeEventHandlers } from '../dom/create';
|
|
8
8
|
import {
|
|
9
9
|
normalizeEvent,
|
|
10
10
|
hasTouchSupport,
|
|
@@ -138,7 +138,7 @@ export const createBase = (config: Record<string, any> = {}): BaseComponent => (
|
|
|
138
138
|
* @returns {Function} Component enhancer
|
|
139
139
|
*/
|
|
140
140
|
export const withElement = (options: WithElementOptions = {}) =>
|
|
141
|
-
(
|
|
141
|
+
<T extends BaseComponent>(component: T): T & ElementComponent => {
|
|
142
142
|
/**
|
|
143
143
|
* Handles the start of a touch interaction.
|
|
144
144
|
*/
|
|
@@ -146,8 +146,8 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
146
146
|
base.updateTouchState(event, 'start');
|
|
147
147
|
element.classList.add(`${base.getClass('touch-active')}`);
|
|
148
148
|
|
|
149
|
-
if (options.forwardEvents?.touchstart &&
|
|
150
|
-
|
|
149
|
+
if (options.forwardEvents?.touchstart && 'emit' in component) {
|
|
150
|
+
(component as any).emit('touchstart', normalizeEvent(event));
|
|
151
151
|
}
|
|
152
152
|
};
|
|
153
153
|
|
|
@@ -162,12 +162,12 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
162
162
|
base.updateTouchState(event, 'end');
|
|
163
163
|
|
|
164
164
|
// Emit tap event for short touches
|
|
165
|
-
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD &&
|
|
166
|
-
|
|
165
|
+
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && 'emit' in component) {
|
|
166
|
+
(component as any).emit('tap', normalizeEvent(event));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
if (options.forwardEvents?.touchend &&
|
|
170
|
-
|
|
169
|
+
if (options.forwardEvents?.touchend && 'emit' in component) {
|
|
170
|
+
(component as any).emit('touchend', normalizeEvent(event));
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
173
|
|
|
@@ -182,19 +182,22 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
182
182
|
const deltaY = normalized.clientY - base.touchState.startPosition.y;
|
|
183
183
|
|
|
184
184
|
// Detect and emit swipe gestures
|
|
185
|
-
if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD &&
|
|
186
|
-
|
|
185
|
+
if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD && 'emit' in component) {
|
|
186
|
+
(component as any).emit('swipe', {
|
|
187
187
|
direction: deltaX > 0 ? 'right' : 'left',
|
|
188
188
|
deltaX,
|
|
189
189
|
deltaY
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
if (options.forwardEvents?.touchmove &&
|
|
194
|
-
|
|
193
|
+
if (options.forwardEvents?.touchmove && 'emit' in component) {
|
|
194
|
+
(component as any).emit('touchmove', { ...normalized, deltaX, deltaY });
|
|
195
195
|
}
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
+
// Get the base component for reference
|
|
199
|
+
const base = component;
|
|
200
|
+
|
|
198
201
|
// Create element options from component options
|
|
199
202
|
const elementOptions: CreateElementOptions = {
|
|
200
203
|
tag: options.tag || 'div',
|
|
@@ -204,7 +207,8 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
204
207
|
options.className
|
|
205
208
|
].filter(Boolean),
|
|
206
209
|
attrs: options.attrs || {},
|
|
207
|
-
|
|
210
|
+
forwardEvents: options.forwardEvents || {},
|
|
211
|
+
context: component // Pass component as context for events
|
|
208
212
|
};
|
|
209
213
|
|
|
210
214
|
// Create the element with appropriate classes
|
|
@@ -218,7 +222,7 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
return {
|
|
221
|
-
...
|
|
225
|
+
...component,
|
|
222
226
|
element,
|
|
223
227
|
|
|
224
228
|
/**
|
|
@@ -241,6 +245,10 @@ export const withElement = (options: WithElementOptions = {}) =>
|
|
|
241
245
|
element.removeEventListener('touchend', handleTouchEnd);
|
|
242
246
|
element.removeEventListener('touchmove', handleTouchMove);
|
|
243
247
|
}
|
|
248
|
+
|
|
249
|
+
// Clean up any registered event handlers using our new utility
|
|
250
|
+
removeEventHandlers(element);
|
|
251
|
+
|
|
244
252
|
element.remove();
|
|
245
253
|
}
|
|
246
254
|
};
|
package/src/core/dom/create.ts
CHANGED
|
@@ -72,6 +72,13 @@ export interface CreateElementOptions {
|
|
|
72
72
|
[key: string]: any;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Event handler storage to facilitate cleanup
|
|
77
|
+
*/
|
|
78
|
+
export interface EventHandlerStorage {
|
|
79
|
+
[eventName: string]: EventListener;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Creates a DOM element with the specified options
|
|
77
84
|
*
|
|
@@ -117,21 +124,57 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
|
|
|
117
124
|
// Handle all other attributes
|
|
118
125
|
const allAttrs = { ...attrs, ...rest };
|
|
119
126
|
Object.entries(allAttrs).forEach(([key, value]) => {
|
|
120
|
-
if (value != null) element.setAttribute(key, value);
|
|
127
|
+
if (value != null) element.setAttribute(key, String(value));
|
|
121
128
|
});
|
|
122
129
|
|
|
123
|
-
//
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
? eventConfig
|
|
128
|
-
: () => true;
|
|
130
|
+
// Initialize event handler storage if not present
|
|
131
|
+
if (!element.__eventHandlers) {
|
|
132
|
+
element.__eventHandlers = {};
|
|
133
|
+
}
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
// Handle event forwarding if context has emit method or is a component with on method
|
|
136
|
+
if (forwardEvents && (context?.emit || context?.on)) {
|
|
137
|
+
Object.entries(forwardEvents).forEach(([nativeEvent, eventConfig]) => {
|
|
138
|
+
// Create a wrapper handler function to evaluate condition and forward event
|
|
139
|
+
const handler = (event: Event) => {
|
|
140
|
+
// Determine if the event should be forwarded
|
|
141
|
+
let shouldForward = true;
|
|
142
|
+
|
|
143
|
+
if (typeof eventConfig === 'function') {
|
|
144
|
+
try {
|
|
145
|
+
// If it's a function, call with component context and event
|
|
146
|
+
shouldForward = eventConfig({ ...context, element }, event);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.warn(`Error in event condition for ${nativeEvent}:`, error);
|
|
149
|
+
shouldForward = false;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// If it's a boolean, use directly
|
|
153
|
+
shouldForward = Boolean(eventConfig);
|
|
133
154
|
}
|
|
134
|
-
|
|
155
|
+
|
|
156
|
+
// Forward the event if condition passes
|
|
157
|
+
if (shouldForward) {
|
|
158
|
+
if (context.emit) {
|
|
159
|
+
context.emit(nativeEvent, { event, element, originalEvent: event });
|
|
160
|
+
} else if (context.on) {
|
|
161
|
+
// This is a component with on method but no emit method
|
|
162
|
+
// Dispatch a custom event that can be listened to
|
|
163
|
+
const customEvent = new CustomEvent(nativeEvent, {
|
|
164
|
+
detail: { event, element, originalEvent: event },
|
|
165
|
+
bubbles: true,
|
|
166
|
+
cancelable: true
|
|
167
|
+
});
|
|
168
|
+
element.dispatchEvent(customEvent);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Store the handler for future removal
|
|
174
|
+
element.__eventHandlers[nativeEvent] = handler;
|
|
175
|
+
|
|
176
|
+
// Add the actual event listener
|
|
177
|
+
element.addEventListener(nativeEvent, handler);
|
|
135
178
|
});
|
|
136
179
|
}
|
|
137
180
|
|
|
@@ -147,6 +190,19 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
|
|
|
147
190
|
return element;
|
|
148
191
|
};
|
|
149
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Removes event handlers from an element
|
|
195
|
+
* @param element - Element to cleanup
|
|
196
|
+
*/
|
|
197
|
+
export const removeEventHandlers = (element: HTMLElement): void => {
|
|
198
|
+
if (element.__eventHandlers) {
|
|
199
|
+
Object.entries(element.__eventHandlers).forEach(([eventName, handler]) => {
|
|
200
|
+
element.removeEventListener(eventName, handler);
|
|
201
|
+
});
|
|
202
|
+
delete element.__eventHandlers;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
150
206
|
/**
|
|
151
207
|
* Higher-order function to add attributes to an element
|
|
152
208
|
* @param {Record<string, any>} attrs - Attributes to add
|
|
@@ -185,4 +241,11 @@ export const withContent = (content: Node | string) =>
|
|
|
185
241
|
element.textContent = content;
|
|
186
242
|
}
|
|
187
243
|
return element;
|
|
188
|
-
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Extend HTMLElement interface to add eventHandlers property
|
|
247
|
+
declare global {
|
|
248
|
+
interface HTMLElement {
|
|
249
|
+
__eventHandlers?: EventHandlerStorage;
|
|
250
|
+
}
|
|
251
|
+
}
|