mtrl 0.2.8 → 0.3.0
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/index.ts +4 -0
- package/package.json +1 -1
- package/src/components/button/button.ts +34 -5
- package/src/components/navigation/api.ts +131 -96
- package/src/components/navigation/features/controller.ts +273 -0
- package/src/components/navigation/features/items.ts +133 -64
- package/src/components/navigation/navigation.ts +17 -2
- package/src/components/navigation/system/core.ts +302 -0
- package/src/components/navigation/system/events.ts +240 -0
- package/src/components/navigation/system/index.ts +184 -0
- package/src/components/navigation/system/mobile.ts +278 -0
- package/src/components/navigation/system/state.ts +77 -0
- package/src/components/navigation/system/types.ts +364 -0
- package/src/components/slider/config.ts +20 -2
- package/src/components/slider/features/controller.ts +737 -0
- package/src/components/slider/features/handlers.ts +18 -16
- package/src/components/slider/features/index.ts +3 -2
- package/src/components/slider/features/range.ts +104 -0
- package/src/components/slider/schema.ts +141 -0
- package/src/components/slider/slider.ts +34 -13
- package/src/components/switch/api.ts +16 -0
- package/src/components/switch/config.ts +1 -18
- package/src/components/switch/features.ts +198 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.ts +3 -3
- package/src/components/switch/types.ts +14 -2
- package/src/components/textfield/api.ts +53 -0
- package/src/components/textfield/features.ts +322 -0
- package/src/components/textfield/textfield.ts +8 -0
- package/src/components/textfield/types.ts +12 -3
- package/src/components/timepicker/clockdial.ts +1 -4
- package/src/core/compose/features/textinput.ts +15 -2
- package/src/core/composition/features/dom.ts +45 -0
- package/src/core/composition/features/icon.ts +131 -0
- package/src/core/composition/features/index.ts +12 -0
- package/src/core/composition/features/label.ts +155 -0
- package/src/core/composition/features/layout.ts +47 -0
- package/src/core/composition/index.ts +26 -0
- package/src/core/index.ts +1 -1
- package/src/core/layout/README.md +350 -0
- package/src/core/layout/array.ts +181 -0
- package/src/core/layout/create.ts +55 -0
- package/src/core/layout/index.ts +26 -0
- package/src/core/layout/object.ts +124 -0
- package/src/core/layout/processor.ts +58 -0
- package/src/core/layout/result.ts +85 -0
- package/src/core/layout/types.ts +125 -0
- package/src/core/layout/utils.ts +136 -0
- package/src/index.ts +1 -0
- package/src/styles/abstract/_variables.scss +28 -0
- package/src/styles/components/_navigation-mobile.scss +244 -0
- package/src/styles/components/_navigation-system.scss +151 -0
- package/src/styles/components/_switch.scss +133 -69
- package/src/styles/components/_textfield.scss +259 -27
- package/demo/build.ts +0 -349
- package/demo/index.html +0 -110
- package/demo/main.js +0 -448
- package/demo/styles.css +0 -239
- package/server.ts +0 -86
- package/src/components/slider/features/slider.ts +0 -318
- package/src/components/slider/features/structure.ts +0 -181
- package/src/components/slider/features/ui.ts +0 -388
- package/src/components/textfield/constants.ts +0 -100
- package/src/core/layout/index.js +0 -95
|
@@ -7,20 +7,23 @@ import { SliderConfig, SliderEvent } from '../types';
|
|
|
7
7
|
*
|
|
8
8
|
* @param config Slider configuration
|
|
9
9
|
* @param state Slider state object
|
|
10
|
-
* @param
|
|
10
|
+
* @param uiRenderer UI renderer interface from controller
|
|
11
11
|
* @param eventHelpers Event helper methods
|
|
12
12
|
* @returns Event handlers for all slider interactions
|
|
13
13
|
*/
|
|
14
|
-
export const createHandlers = (config: SliderConfig, state,
|
|
14
|
+
export const createHandlers = (config: SliderConfig, state, uiRenderer, eventHelpers) => {
|
|
15
15
|
// Get required elements from structure (with fallbacks)
|
|
16
|
+
const components = state.component?.components || {};
|
|
17
|
+
|
|
18
|
+
// Extract needed components
|
|
16
19
|
const {
|
|
17
20
|
container = null,
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
handle = null,
|
|
22
|
+
track = null,
|
|
20
23
|
valueBubble = null,
|
|
21
24
|
secondHandle = null,
|
|
22
25
|
secondValueBubble = null
|
|
23
|
-
} =
|
|
26
|
+
} = components;
|
|
24
27
|
|
|
25
28
|
// Get required helper methods (with fallbacks)
|
|
26
29
|
const {
|
|
@@ -28,8 +31,8 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
28
31
|
roundToStep = value => value,
|
|
29
32
|
clamp = (value, min, max) => value,
|
|
30
33
|
showValueBubble = () => {},
|
|
31
|
-
|
|
32
|
-
} =
|
|
34
|
+
render = () => {}
|
|
35
|
+
} = uiRenderer;
|
|
33
36
|
|
|
34
37
|
const { triggerEvent = () => ({ defaultPrevented: false }) } = eventHelpers;
|
|
35
38
|
|
|
@@ -127,7 +130,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
127
130
|
document.addEventListener('touchmove', handleMouseMove, { passive: false });
|
|
128
131
|
document.addEventListener('touchend', handleMouseUp);
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
render();
|
|
131
134
|
triggerEvent(SLIDER_EVENTS.START, e);
|
|
132
135
|
};
|
|
133
136
|
|
|
@@ -169,9 +172,8 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
// Update UI and trigger events
|
|
172
|
-
|
|
175
|
+
render();
|
|
173
176
|
triggerEvent(SLIDER_EVENTS.INPUT, e);
|
|
174
|
-
triggerEvent(SLIDER_EVENTS.CHANGE, e);
|
|
175
177
|
} catch (error) {
|
|
176
178
|
console.warn('Error handling track click:', error);
|
|
177
179
|
}
|
|
@@ -244,7 +246,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
244
246
|
state.value = newValue;
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
|
|
249
|
+
render();
|
|
248
250
|
triggerEvent(SLIDER_EVENTS.INPUT, e);
|
|
249
251
|
} catch (error) {
|
|
250
252
|
console.warn('Error during slider drag:', error);
|
|
@@ -274,7 +276,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
274
276
|
|
|
275
277
|
// Reset active handle and update UI
|
|
276
278
|
state.activeHandle = null;
|
|
277
|
-
|
|
279
|
+
render();
|
|
278
280
|
|
|
279
281
|
// Trigger events
|
|
280
282
|
triggerEvent(SLIDER_EVENTS.CHANGE, e);
|
|
@@ -355,7 +357,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
// Update UI and trigger events
|
|
358
|
-
|
|
360
|
+
render();
|
|
359
361
|
triggerEvent(SLIDER_EVENTS.INPUT, e);
|
|
360
362
|
triggerEvent(SLIDER_EVENTS.CHANGE, e);
|
|
361
363
|
};
|
|
@@ -406,7 +408,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
406
408
|
* Set up all event listeners
|
|
407
409
|
*/
|
|
408
410
|
const setupEventListeners = () => {
|
|
409
|
-
if (!state.component || !state.component.
|
|
411
|
+
if (!state.component || !state.component.components) {
|
|
410
412
|
console.warn('Cannot set up event listeners: missing component structure');
|
|
411
413
|
return;
|
|
412
414
|
}
|
|
@@ -436,7 +438,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
436
438
|
* Clean up all event listeners
|
|
437
439
|
*/
|
|
438
440
|
const cleanupEventListeners = () => {
|
|
439
|
-
if (!state.component || !state.component.
|
|
441
|
+
if (!state.component || !state.component.components) return;
|
|
440
442
|
|
|
441
443
|
// Clean up container listeners
|
|
442
444
|
if (container) {
|
|
@@ -492,4 +494,4 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
|
|
|
492
494
|
hideAllBubbles,
|
|
493
495
|
clearKeyboardFocus
|
|
494
496
|
};
|
|
495
|
-
}
|
|
497
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/components/slider/features/index.ts
|
|
2
|
-
|
|
2
|
+
// Export slider-specific features
|
|
3
|
+
export { withRange } from './range';
|
|
3
4
|
export { withStates } from './states';
|
|
4
|
-
export {
|
|
5
|
+
export { withController } from './controller';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/components/slider/features/range.ts
|
|
2
|
+
import { SliderConfig } from '../types';
|
|
3
|
+
import { createElement } from '../../../core/dom/create';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enhances structure definition with range slider elements
|
|
7
|
+
*
|
|
8
|
+
* @param config Slider configuration
|
|
9
|
+
* @returns Component enhancer that adds range slider to structure
|
|
10
|
+
*/
|
|
11
|
+
export const withRange = (config: SliderConfig) => component => {
|
|
12
|
+
// If not a range slider or missing structure definition, return unmodified
|
|
13
|
+
if (!config.range || !config.secondValue || !component.schema) {
|
|
14
|
+
return component;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Calculate values for second handle
|
|
19
|
+
const min = config.min || 0;
|
|
20
|
+
const max = config.max || 100;
|
|
21
|
+
const secondValue = config.secondValue;
|
|
22
|
+
const secondValuePercent = ((secondValue - min) / (max - min)) * 100;
|
|
23
|
+
const formatter = config.valueFormatter || (val => val.toString());
|
|
24
|
+
const isDisabled = config.disabled === true;
|
|
25
|
+
const getClass = component.getClass;
|
|
26
|
+
|
|
27
|
+
// Clone the structure definition (deep copy)
|
|
28
|
+
const schema = JSON.parse(JSON.stringify(component.schema));
|
|
29
|
+
|
|
30
|
+
// Add range class to root element
|
|
31
|
+
const rootClasses = schema.element.options.className || [];
|
|
32
|
+
if (Array.isArray(rootClasses)) {
|
|
33
|
+
rootClasses.push(getClass('slider--range'));
|
|
34
|
+
} else {
|
|
35
|
+
schema.element.options.className = `${rootClasses} ${getClass('slider--range')}`.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add start track segment to track children
|
|
39
|
+
const trackChildren = schema.element.children.container.children.track.children;
|
|
40
|
+
trackChildren.startTrack = {
|
|
41
|
+
name: 'startTrack',
|
|
42
|
+
creator: createElement,
|
|
43
|
+
options: {
|
|
44
|
+
tag: 'div',
|
|
45
|
+
className: getClass('slider-start-track'),
|
|
46
|
+
style: {
|
|
47
|
+
width: '0%'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Add second handle to container children
|
|
53
|
+
const containerChildren = schema.element.children.container.children;
|
|
54
|
+
containerChildren.secondHandle = {
|
|
55
|
+
name: 'secondHandle',
|
|
56
|
+
creator: createElement,
|
|
57
|
+
options: {
|
|
58
|
+
tag: 'div',
|
|
59
|
+
className: getClass('slider-handle'),
|
|
60
|
+
attrs: {
|
|
61
|
+
role: 'slider',
|
|
62
|
+
'aria-valuemin': String(min),
|
|
63
|
+
'aria-valuemax': String(max),
|
|
64
|
+
'aria-valuenow': String(secondValue),
|
|
65
|
+
'aria-orientation': 'horizontal',
|
|
66
|
+
tabindex: isDisabled ? '-1' : '0',
|
|
67
|
+
'aria-disabled': isDisabled ? 'true' : 'false',
|
|
68
|
+
'data-value': String(secondValue),
|
|
69
|
+
'data-handle-index': '1'
|
|
70
|
+
},
|
|
71
|
+
style: {
|
|
72
|
+
left: `${secondValuePercent}%`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Add second value bubble to container children
|
|
78
|
+
containerChildren.secondValueBubble = {
|
|
79
|
+
name: 'secondValueBubble',
|
|
80
|
+
creator: createElement,
|
|
81
|
+
options: {
|
|
82
|
+
tag: 'div',
|
|
83
|
+
className: getClass('slider-value'),
|
|
84
|
+
attrs: {
|
|
85
|
+
'aria-hidden': 'true',
|
|
86
|
+
'data-handle-index': '1'
|
|
87
|
+
},
|
|
88
|
+
text: formatter(secondValue),
|
|
89
|
+
style: {
|
|
90
|
+
left: `${secondValuePercent}%`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Return component with updated structure definition
|
|
96
|
+
return {
|
|
97
|
+
...component,
|
|
98
|
+
schema
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn('Error enhancing structure with range functionality:', error);
|
|
102
|
+
return component;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/components/slider/structure.ts
|
|
2
|
+
import { SliderConfig } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates the base slider structure definition
|
|
6
|
+
*
|
|
7
|
+
* @param component Component for class name generation
|
|
8
|
+
* @param config Slider configuration
|
|
9
|
+
* @returns Structure schema object
|
|
10
|
+
*/
|
|
11
|
+
export function createSliderSchema(component, config: SliderConfig) {
|
|
12
|
+
// Get prefixed class names
|
|
13
|
+
const getClass = (className) => component.getClass(className);
|
|
14
|
+
|
|
15
|
+
// Set default values
|
|
16
|
+
const min = config.min || 0;
|
|
17
|
+
const max = config.max || 100;
|
|
18
|
+
const value = config.value !== undefined ? config.value : min;
|
|
19
|
+
const isDisabled = config.disabled === true;
|
|
20
|
+
const formatter = config.valueFormatter || (val => val.toString());
|
|
21
|
+
|
|
22
|
+
// Calculate initial position
|
|
23
|
+
const valuePercent = ((value - min) / (max - min)) * 100;
|
|
24
|
+
|
|
25
|
+
// Return base structure definition formatted for createStructure
|
|
26
|
+
return {
|
|
27
|
+
element: {
|
|
28
|
+
options: {
|
|
29
|
+
className: [getClass('slider'), config.class].filter(Boolean),
|
|
30
|
+
attrs: {
|
|
31
|
+
tabindex: '-1',
|
|
32
|
+
role: 'none',
|
|
33
|
+
'aria-disabled': isDisabled ? 'true' : 'false'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
children: {
|
|
37
|
+
// Container with all slider elements
|
|
38
|
+
container: {
|
|
39
|
+
options: {
|
|
40
|
+
className: getClass('slider-container')
|
|
41
|
+
},
|
|
42
|
+
children: {
|
|
43
|
+
// Track with segments
|
|
44
|
+
track: {
|
|
45
|
+
options: {
|
|
46
|
+
className: getClass('slider-track')
|
|
47
|
+
},
|
|
48
|
+
children: {
|
|
49
|
+
activeTrack: {
|
|
50
|
+
options: {
|
|
51
|
+
className: getClass('slider-active-track'),
|
|
52
|
+
style: {
|
|
53
|
+
width: `${valuePercent}%`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
startTrack: {
|
|
58
|
+
options: {
|
|
59
|
+
className: getClass('slider-start-track'),
|
|
60
|
+
style: {
|
|
61
|
+
display: 'none', // Initially hidden for single slider
|
|
62
|
+
width: '0%'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
remainingTrack: {
|
|
67
|
+
options: {
|
|
68
|
+
className: getClass('slider-remaining-track'),
|
|
69
|
+
style: {
|
|
70
|
+
width: `${100 - valuePercent}%`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Ticks container
|
|
78
|
+
ticksContainer: {
|
|
79
|
+
options: {
|
|
80
|
+
className: getClass('slider-ticks-container')
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Dots for ends
|
|
85
|
+
startDot: {
|
|
86
|
+
options: {
|
|
87
|
+
className: [
|
|
88
|
+
getClass('slider-dot'),
|
|
89
|
+
getClass('slider-dot--start')
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
endDot: {
|
|
94
|
+
options: {
|
|
95
|
+
className: [
|
|
96
|
+
getClass('slider-dot'),
|
|
97
|
+
getClass('slider-dot--end')
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Main handle
|
|
103
|
+
handle: {
|
|
104
|
+
options: {
|
|
105
|
+
className: getClass('slider-handle'),
|
|
106
|
+
attrs: {
|
|
107
|
+
role: 'slider',
|
|
108
|
+
'aria-valuemin': String(min),
|
|
109
|
+
'aria-valuemax': String(max),
|
|
110
|
+
'aria-valuenow': String(value),
|
|
111
|
+
'aria-orientation': 'horizontal',
|
|
112
|
+
tabindex: isDisabled ? '-1' : '0',
|
|
113
|
+
'aria-disabled': isDisabled ? 'true' : 'false',
|
|
114
|
+
'data-value': String(value),
|
|
115
|
+
'data-handle-index': '0'
|
|
116
|
+
},
|
|
117
|
+
style: {
|
|
118
|
+
left: `${valuePercent}%`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
// Main value bubble
|
|
123
|
+
valueBubble: {
|
|
124
|
+
options: {
|
|
125
|
+
className: getClass('slider-value'),
|
|
126
|
+
attrs: {
|
|
127
|
+
'aria-hidden': 'true',
|
|
128
|
+
'data-handle-index': '0'
|
|
129
|
+
},
|
|
130
|
+
text: formatter(value),
|
|
131
|
+
style: {
|
|
132
|
+
left: `${valuePercent}%`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,42 +1,63 @@
|
|
|
1
1
|
// src/components/slider/slider.ts
|
|
2
2
|
import { pipe } from '../../core/compose/pipe';
|
|
3
|
-
import { createBase
|
|
4
|
-
import { withEvents, withLifecycle
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createBase } from '../../core/compose/component';
|
|
4
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
|
+
import { withLayout, withIcon, withLabel, withDom } from '../../core/composition/features';
|
|
6
|
+
import {
|
|
7
|
+
withRange,
|
|
8
|
+
withStates,
|
|
9
|
+
withController
|
|
10
|
+
} from './features';
|
|
7
11
|
import { withAPI } from './api';
|
|
8
12
|
import { SliderConfig, SliderComponent } from './types';
|
|
9
|
-
import { createBaseConfig,
|
|
13
|
+
import { createBaseConfig, getApiConfig } from './config';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Creates a new Slider component
|
|
17
|
+
*
|
|
18
|
+
* Slider follows a clear architectural pattern:
|
|
19
|
+
* 1. Structure definition - Describes the DOM structure declaratively
|
|
20
|
+
* 2. Feature enhancement - Adds specific capabilities (range, icons, labels)
|
|
21
|
+
* 3. DOM creation - Turns the structure into actual DOM elements
|
|
22
|
+
* 4. State management - Handles visual states and appearance
|
|
23
|
+
* 5. Controller - Manages behavior, events, and UI rendering
|
|
24
|
+
* 6. Lifecycle - Handles component lifecycle events
|
|
25
|
+
* 7. Public API - Exposes a clean, consistent API
|
|
26
|
+
*
|
|
13
27
|
* @param {SliderConfig} config - Slider configuration object
|
|
14
28
|
* @returns {SliderComponent} Slider component instance
|
|
15
29
|
*/
|
|
16
30
|
const createSlider = (config: SliderConfig = {}): SliderComponent => {
|
|
31
|
+
// Process configuration with defaults
|
|
17
32
|
const baseConfig = createBaseConfig(config);
|
|
18
33
|
|
|
19
34
|
try {
|
|
20
|
-
// Create the component
|
|
35
|
+
// Create the component by composing features in a specific order
|
|
21
36
|
const component = pipe(
|
|
37
|
+
// Base component with event system
|
|
22
38
|
createBase,
|
|
23
39
|
withEvents(),
|
|
24
|
-
|
|
40
|
+
withLayout(baseConfig),
|
|
25
41
|
withIcon(baseConfig),
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
withLabel(baseConfig),
|
|
43
|
+
withRange(baseConfig),
|
|
44
|
+
|
|
45
|
+
// Now create the actual DOM elements from the complete structure
|
|
46
|
+
withDom(),
|
|
47
|
+
|
|
48
|
+
// Add state management and behavior
|
|
28
49
|
withStates(baseConfig),
|
|
29
|
-
|
|
50
|
+
withController(baseConfig),
|
|
30
51
|
withLifecycle()
|
|
31
52
|
)(baseConfig);
|
|
32
53
|
|
|
33
|
-
// Generate the API configuration
|
|
54
|
+
// Generate the API configuration based on the enhanced component
|
|
34
55
|
const apiOptions = getApiConfig(component);
|
|
35
56
|
|
|
36
|
-
// Apply the API layer
|
|
57
|
+
// Apply the public API layer
|
|
37
58
|
const slider = withAPI(apiOptions)(component);
|
|
38
59
|
|
|
39
|
-
// Register event handlers from config
|
|
60
|
+
// Register event handlers from config for convenience
|
|
40
61
|
if (baseConfig.on && typeof slider.on === 'function') {
|
|
41
62
|
Object.entries(baseConfig.on).forEach(([event, handler]) => {
|
|
42
63
|
if (typeof handler === 'function') {
|
|
@@ -48,6 +48,22 @@ export const withAPI = ({ disabled, lifecycle, checkable }: ApiOptions) =>
|
|
|
48
48
|
getLabel(): string {
|
|
49
49
|
return component.text?.getText() || '';
|
|
50
50
|
},
|
|
51
|
+
|
|
52
|
+
// Supporting text management (if present)
|
|
53
|
+
supportingTextElement: component.supportingTextElement || null,
|
|
54
|
+
setSupportingText(text: string, isError?: boolean): SwitchComponent {
|
|
55
|
+
if (component.setSupportingText) {
|
|
56
|
+
component.setSupportingText(text, isError);
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
removeSupportingText(): SwitchComponent {
|
|
62
|
+
if (component.removeSupportingText) {
|
|
63
|
+
component.removeSupportingText();
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
},
|
|
51
67
|
|
|
52
68
|
// Event handling
|
|
53
69
|
on(event: string, handler: Function): SwitchComponent {
|
|
@@ -9,9 +9,7 @@ import { SwitchConfig, BaseComponent, ApiOptions } from './types';
|
|
|
9
9
|
/**
|
|
10
10
|
* Default configuration for the Switch component
|
|
11
11
|
*/
|
|
12
|
-
export const defaultConfig: SwitchConfig = {
|
|
13
|
-
labelPosition: 'end'
|
|
14
|
-
};
|
|
12
|
+
export const defaultConfig: SwitchConfig = {};
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* Creates the base configuration for Switch component
|
|
@@ -34,21 +32,6 @@ export const getElementConfig = (config: SwitchConfig) =>
|
|
|
34
32
|
interactive: true
|
|
35
33
|
});
|
|
36
34
|
|
|
37
|
-
/**
|
|
38
|
-
* Applies label position class to the component
|
|
39
|
-
* @param {SwitchConfig} config - Component configuration
|
|
40
|
-
*/
|
|
41
|
-
export const withLabelPosition = (config: SwitchConfig) => (component: BaseComponent): BaseComponent => {
|
|
42
|
-
if (!config.label) return component;
|
|
43
|
-
|
|
44
|
-
const position = config.labelPosition || 'end';
|
|
45
|
-
const positionClass = `${config.prefix}-switch--label-${position}`;
|
|
46
|
-
|
|
47
|
-
component.element.classList.add(positionClass);
|
|
48
|
-
|
|
49
|
-
return component;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
35
|
/**
|
|
53
36
|
* Creates API configuration for the Switch component
|
|
54
37
|
* @param {BaseComponent} comp - Component with disabled, lifecycle, and checkable features
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/components/switch/features.ts
|
|
2
|
+
import { BaseComponent, ElementComponent } from '../../core/compose/component';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for supporting text feature
|
|
6
|
+
*/
|
|
7
|
+
export interface SupportingTextConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Supporting text content
|
|
10
|
+
*/
|
|
11
|
+
supportingText?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether supporting text indicates an error
|
|
15
|
+
*/
|
|
16
|
+
error?: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CSS class prefix
|
|
20
|
+
*/
|
|
21
|
+
prefix?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Component name
|
|
25
|
+
*/
|
|
26
|
+
componentName?: string;
|
|
27
|
+
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Component with supporting text capabilities
|
|
33
|
+
*/
|
|
34
|
+
export interface SupportingTextComponent extends BaseComponent {
|
|
35
|
+
/**
|
|
36
|
+
* Supporting text element
|
|
37
|
+
*/
|
|
38
|
+
supportingTextElement: HTMLElement | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sets supporting text content
|
|
42
|
+
* @param text - Text content
|
|
43
|
+
* @param isError - Whether text represents an error
|
|
44
|
+
* @returns Component instance for chaining
|
|
45
|
+
*/
|
|
46
|
+
setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Removes supporting text
|
|
50
|
+
* @returns Component instance for chaining
|
|
51
|
+
*/
|
|
52
|
+
removeSupportingText: () => SupportingTextComponent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Helper to ensure the switch has the proper container/content structure
|
|
57
|
+
* @param component - The component to enhance
|
|
58
|
+
* @param prefix - CSS class prefix
|
|
59
|
+
* @param componentName - Component name
|
|
60
|
+
* @returns The container and content elements
|
|
61
|
+
*/
|
|
62
|
+
const ensureSwitchStructure = (
|
|
63
|
+
component: ElementComponent,
|
|
64
|
+
prefix: string,
|
|
65
|
+
componentName: string
|
|
66
|
+
) => {
|
|
67
|
+
const PREFIX = prefix || 'mtrl';
|
|
68
|
+
const COMPONENT = componentName || 'switch';
|
|
69
|
+
|
|
70
|
+
// Create or find container
|
|
71
|
+
let container = component.element.querySelector(`.${PREFIX}-${COMPONENT}-container`);
|
|
72
|
+
if (!container) {
|
|
73
|
+
container = document.createElement('div');
|
|
74
|
+
container.className = `${PREFIX}-${COMPONENT}-container`;
|
|
75
|
+
|
|
76
|
+
// Find input and track to move them to container
|
|
77
|
+
const input = component.element.querySelector(`.${PREFIX}-${COMPONENT}-input`);
|
|
78
|
+
const track = component.element.querySelector(`.${PREFIX}-${COMPONENT}-track`);
|
|
79
|
+
|
|
80
|
+
// Gather all elements except container
|
|
81
|
+
const elementsToMove = [];
|
|
82
|
+
if (input) elementsToMove.push(input);
|
|
83
|
+
if (track) elementsToMove.push(track);
|
|
84
|
+
|
|
85
|
+
// Create content wrapper
|
|
86
|
+
const contentWrapper = document.createElement('div');
|
|
87
|
+
contentWrapper.className = `${PREFIX}-${COMPONENT}-content`;
|
|
88
|
+
|
|
89
|
+
// Find label and move to content
|
|
90
|
+
const label = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
|
|
91
|
+
if (label) {
|
|
92
|
+
contentWrapper.appendChild(label);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add content wrapper to container first
|
|
96
|
+
container.appendChild(contentWrapper);
|
|
97
|
+
|
|
98
|
+
// Add other elements to container
|
|
99
|
+
elementsToMove.forEach(el => container.appendChild(el));
|
|
100
|
+
|
|
101
|
+
// Add container to component
|
|
102
|
+
component.element.appendChild(container);
|
|
103
|
+
|
|
104
|
+
return { container, contentWrapper };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Container exists, find or create content wrapper
|
|
108
|
+
let contentWrapper = component.element.querySelector(`.${PREFIX}-${COMPONENT}-content`);
|
|
109
|
+
if (!contentWrapper) {
|
|
110
|
+
contentWrapper = document.createElement('div');
|
|
111
|
+
contentWrapper.className = `${PREFIX}-${COMPONENT}-content`;
|
|
112
|
+
|
|
113
|
+
// Find label to move to content
|
|
114
|
+
const label = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
|
|
115
|
+
if (label) {
|
|
116
|
+
contentWrapper.appendChild(label);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Insert content wrapper at beginning of container
|
|
120
|
+
container.insertBefore(contentWrapper, container.firstChild);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { container, contentWrapper };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates and manages supporting text for a component
|
|
128
|
+
* @param config - Configuration object with supporting text settings
|
|
129
|
+
* @returns Function that enhances a component with supporting text functionality
|
|
130
|
+
*/
|
|
131
|
+
export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
|
|
132
|
+
<C extends ElementComponent>(component: C): C & SupportingTextComponent => {
|
|
133
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
134
|
+
const COMPONENT = config.componentName || 'switch';
|
|
135
|
+
|
|
136
|
+
// Ensure we have the proper container/content structure
|
|
137
|
+
const { contentWrapper } = ensureSwitchStructure(component, PREFIX, COMPONENT);
|
|
138
|
+
|
|
139
|
+
// Create supporting text element if needed
|
|
140
|
+
let supportingElement = null;
|
|
141
|
+
if (config.supportingText) {
|
|
142
|
+
supportingElement = document.createElement('div');
|
|
143
|
+
supportingElement.className = `${PREFIX}-${COMPONENT}-helper`;
|
|
144
|
+
supportingElement.textContent = config.supportingText;
|
|
145
|
+
|
|
146
|
+
if (config.error) {
|
|
147
|
+
supportingElement.classList.add(`${PREFIX}-${COMPONENT}-helper--error`);
|
|
148
|
+
component.element.classList.add(`${PREFIX}-${COMPONENT}--error`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add supporting text to the content wrapper
|
|
152
|
+
contentWrapper.appendChild(supportingElement);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add lifecycle integration if available
|
|
156
|
+
if ('lifecycle' in component && component.lifecycle?.destroy && supportingElement) {
|
|
157
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
158
|
+
component.lifecycle.destroy = () => {
|
|
159
|
+
if (supportingElement) supportingElement.remove();
|
|
160
|
+
originalDestroy.call(component.lifecycle);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...component,
|
|
166
|
+
supportingTextElement: supportingElement,
|
|
167
|
+
|
|
168
|
+
setSupportingText(text: string, isError = false) {
|
|
169
|
+
const { contentWrapper } = ensureSwitchStructure(component, PREFIX, COMPONENT);
|
|
170
|
+
let supportingElement = this.supportingTextElement;
|
|
171
|
+
|
|
172
|
+
if (!supportingElement) {
|
|
173
|
+
// Create if it doesn't exist
|
|
174
|
+
supportingElement = document.createElement('div');
|
|
175
|
+
supportingElement.className = `${PREFIX}-${COMPONENT}-helper`;
|
|
176
|
+
contentWrapper.appendChild(supportingElement);
|
|
177
|
+
this.supportingTextElement = supportingElement;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
supportingElement.textContent = text;
|
|
181
|
+
|
|
182
|
+
// Handle error state
|
|
183
|
+
supportingElement.classList.toggle(`${PREFIX}-${COMPONENT}-helper--error`, isError);
|
|
184
|
+
component.element.classList.toggle(`${PREFIX}-${COMPONENT}--error`, isError);
|
|
185
|
+
|
|
186
|
+
return this;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
removeSupportingText() {
|
|
190
|
+
if (this.supportingTextElement && this.supportingTextElement.parentNode) {
|
|
191
|
+
this.supportingTextElement.remove();
|
|
192
|
+
this.supportingTextElement = null;
|
|
193
|
+
component.element.classList.remove(`${PREFIX}-${COMPONENT}--error`);
|
|
194
|
+
}
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
};
|