mtrl 0.2.8 → 0.2.9
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 +2 -0
- package/package.json +1 -1
- 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-types.ts +124 -0
- package/src/components/navigation/system.ts +776 -0
- package/src/components/slider/config.ts +20 -2
- package/src/components/slider/features/controller.ts +761 -0
- package/src/components/slider/features/handlers.ts +18 -15
- package/src/components/slider/features/index.ts +3 -2
- package/src/components/slider/features/range.ts +104 -0
- package/src/components/slider/slider.ts +34 -14
- package/src/components/slider/structure.ts +152 -0
- 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 +33 -0
- package/src/core/composition/features/icon.ts +131 -0
- package/src/core/composition/features/index.ts +11 -0
- package/src/core/composition/features/label.ts +156 -0
- package/src/core/composition/features/structure.ts +22 -0
- package/src/core/composition/index.ts +26 -0
- package/src/core/index.ts +1 -1
- package/src/core/structure.ts +288 -0
- package/src/index.ts +1 -0
- package/src/styles/components/_navigation-mobile.scss +244 -0
- package/src/styles/components/_navigation-system.scss +151 -0
- package/src/styles/components/_textfield.scss +250 -11
- 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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// src/components/textfield/features.ts
|
|
2
|
+
import { BaseComponent, ElementComponent } from '../../core/compose/component';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for leading icon feature
|
|
6
|
+
*/
|
|
7
|
+
export interface LeadingIconConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Leading icon HTML content
|
|
10
|
+
*/
|
|
11
|
+
leadingIcon?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CSS class prefix
|
|
15
|
+
*/
|
|
16
|
+
prefix?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Component name
|
|
20
|
+
*/
|
|
21
|
+
componentName?: string;
|
|
22
|
+
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for trailing icon feature
|
|
28
|
+
*/
|
|
29
|
+
export interface TrailingIconConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Trailing icon HTML content
|
|
32
|
+
*/
|
|
33
|
+
trailingIcon?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CSS class prefix
|
|
37
|
+
*/
|
|
38
|
+
prefix?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Component name
|
|
42
|
+
*/
|
|
43
|
+
componentName?: string;
|
|
44
|
+
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configuration for supporting text feature
|
|
50
|
+
*/
|
|
51
|
+
export interface SupportingTextConfig {
|
|
52
|
+
/**
|
|
53
|
+
* Supporting text content
|
|
54
|
+
*/
|
|
55
|
+
supportingText?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether supporting text indicates an error
|
|
59
|
+
*/
|
|
60
|
+
error?: boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* CSS class prefix
|
|
64
|
+
*/
|
|
65
|
+
prefix?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Component name
|
|
69
|
+
*/
|
|
70
|
+
componentName?: string;
|
|
71
|
+
|
|
72
|
+
[key: string]: any;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Component with leading icon capabilities
|
|
77
|
+
*/
|
|
78
|
+
export interface LeadingIconComponent extends BaseComponent {
|
|
79
|
+
/**
|
|
80
|
+
* Leading icon element
|
|
81
|
+
*/
|
|
82
|
+
leadingIcon: HTMLElement | null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets leading icon content
|
|
86
|
+
* @param html - HTML content for the icon
|
|
87
|
+
* @returns Component instance for chaining
|
|
88
|
+
*/
|
|
89
|
+
setLeadingIcon: (html: string) => LeadingIconComponent;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Removes leading icon
|
|
93
|
+
* @returns Component instance for chaining
|
|
94
|
+
*/
|
|
95
|
+
removeLeadingIcon: () => LeadingIconComponent;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Component with trailing icon capabilities
|
|
100
|
+
*/
|
|
101
|
+
export interface TrailingIconComponent extends BaseComponent {
|
|
102
|
+
/**
|
|
103
|
+
* Trailing icon element
|
|
104
|
+
*/
|
|
105
|
+
trailingIcon: HTMLElement | null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sets trailing icon content
|
|
109
|
+
* @param html - HTML content for the icon
|
|
110
|
+
* @returns Component instance for chaining
|
|
111
|
+
*/
|
|
112
|
+
setTrailingIcon: (html: string) => TrailingIconComponent;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Removes trailing icon
|
|
116
|
+
* @returns Component instance for chaining
|
|
117
|
+
*/
|
|
118
|
+
removeTrailingIcon: () => TrailingIconComponent;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Component with supporting text capabilities
|
|
123
|
+
*/
|
|
124
|
+
export interface SupportingTextComponent extends BaseComponent {
|
|
125
|
+
/**
|
|
126
|
+
* Supporting text element
|
|
127
|
+
*/
|
|
128
|
+
supportingTextElement: HTMLElement | null;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sets supporting text content
|
|
132
|
+
* @param text - Text content
|
|
133
|
+
* @param isError - Whether text represents an error
|
|
134
|
+
* @returns Component instance for chaining
|
|
135
|
+
*/
|
|
136
|
+
setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Removes supporting text
|
|
140
|
+
* @returns Component instance for chaining
|
|
141
|
+
*/
|
|
142
|
+
removeSupportingText: () => SupportingTextComponent;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates and manages a leading icon for a component
|
|
147
|
+
* @param config - Configuration object with leading icon settings
|
|
148
|
+
* @returns Function that enhances a component with leading icon functionality
|
|
149
|
+
*/
|
|
150
|
+
export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
|
|
151
|
+
<C extends ElementComponent>(component: C): C & LeadingIconComponent => {
|
|
152
|
+
if (!config.leadingIcon) {
|
|
153
|
+
return component as C & LeadingIconComponent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create icon element
|
|
157
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
158
|
+
const iconElement = document.createElement('span');
|
|
159
|
+
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
|
|
160
|
+
iconElement.innerHTML = config.leadingIcon;
|
|
161
|
+
|
|
162
|
+
// Add leading icon to the component
|
|
163
|
+
component.element.appendChild(iconElement);
|
|
164
|
+
|
|
165
|
+
// Add leading-icon class to the component
|
|
166
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
167
|
+
|
|
168
|
+
// When there's a leading icon, adjust input padding
|
|
169
|
+
if (component.input) {
|
|
170
|
+
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add lifecycle integration if available
|
|
174
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
175
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
176
|
+
component.lifecycle.destroy = () => {
|
|
177
|
+
iconElement.remove();
|
|
178
|
+
originalDestroy.call(component.lifecycle);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...component,
|
|
184
|
+
leadingIcon: iconElement,
|
|
185
|
+
|
|
186
|
+
setLeadingIcon(html: string) {
|
|
187
|
+
iconElement.innerHTML = html;
|
|
188
|
+
return this;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
removeLeadingIcon() {
|
|
192
|
+
if (iconElement.parentNode) {
|
|
193
|
+
iconElement.remove();
|
|
194
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
195
|
+
if (component.input) {
|
|
196
|
+
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
197
|
+
}
|
|
198
|
+
this.leadingIcon = null;
|
|
199
|
+
}
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates and manages a trailing icon for a component
|
|
207
|
+
* @param config - Configuration object with trailing icon settings
|
|
208
|
+
* @returns Function that enhances a component with trailing icon functionality
|
|
209
|
+
*/
|
|
210
|
+
export const withTrailingIcon = <T extends TrailingIconConfig>(config: T) =>
|
|
211
|
+
<C extends ElementComponent>(component: C): C & TrailingIconComponent => {
|
|
212
|
+
if (!config.trailingIcon) {
|
|
213
|
+
return component as C & TrailingIconComponent;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create icon element
|
|
217
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
218
|
+
const iconElement = document.createElement('span');
|
|
219
|
+
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
|
|
220
|
+
iconElement.innerHTML = config.trailingIcon;
|
|
221
|
+
|
|
222
|
+
// Add trailing icon to the component
|
|
223
|
+
component.element.appendChild(iconElement);
|
|
224
|
+
|
|
225
|
+
// Add trailing-icon class to the component
|
|
226
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
227
|
+
|
|
228
|
+
// When there's a trailing icon, adjust input padding
|
|
229
|
+
if (component.input) {
|
|
230
|
+
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add lifecycle integration if available
|
|
234
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
235
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
236
|
+
component.lifecycle.destroy = () => {
|
|
237
|
+
iconElement.remove();
|
|
238
|
+
originalDestroy.call(component.lifecycle);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
...component,
|
|
244
|
+
trailingIcon: iconElement,
|
|
245
|
+
|
|
246
|
+
setTrailingIcon(html: string) {
|
|
247
|
+
iconElement.innerHTML = html;
|
|
248
|
+
return this;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
removeTrailingIcon() {
|
|
252
|
+
if (iconElement.parentNode) {
|
|
253
|
+
iconElement.remove();
|
|
254
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
255
|
+
if (component.input) {
|
|
256
|
+
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
257
|
+
}
|
|
258
|
+
this.trailingIcon = null;
|
|
259
|
+
}
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Creates and manages supporting text for a component
|
|
267
|
+
* @param config - Configuration object with supporting text settings
|
|
268
|
+
* @returns Function that enhances a component with supporting text functionality
|
|
269
|
+
*/
|
|
270
|
+
export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
|
|
271
|
+
<C extends ElementComponent>(component: C): C & SupportingTextComponent => {
|
|
272
|
+
if (!config.supportingText) {
|
|
273
|
+
return component as C & SupportingTextComponent;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create supporting text element
|
|
277
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
278
|
+
const supportingElement = document.createElement('div');
|
|
279
|
+
supportingElement.className = `${PREFIX}-${config.componentName || 'textfield'}-helper`;
|
|
280
|
+
supportingElement.textContent = config.supportingText;
|
|
281
|
+
|
|
282
|
+
if (config.error) {
|
|
283
|
+
supportingElement.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`);
|
|
284
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Add supporting text to the component
|
|
288
|
+
component.element.appendChild(supportingElement);
|
|
289
|
+
|
|
290
|
+
// Add lifecycle integration if available
|
|
291
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
292
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
293
|
+
component.lifecycle.destroy = () => {
|
|
294
|
+
supportingElement.remove();
|
|
295
|
+
originalDestroy.call(component.lifecycle);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
...component,
|
|
301
|
+
supportingTextElement: supportingElement,
|
|
302
|
+
|
|
303
|
+
setSupportingText(text: string, isError = false) {
|
|
304
|
+
supportingElement.textContent = text;
|
|
305
|
+
|
|
306
|
+
// Handle error state
|
|
307
|
+
supportingElement.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`, isError);
|
|
308
|
+
component.element.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}--error`, isError);
|
|
309
|
+
|
|
310
|
+
return this;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
removeSupportingText() {
|
|
314
|
+
if (supportingElement.parentNode) {
|
|
315
|
+
supportingElement.remove();
|
|
316
|
+
this.supportingTextElement = null;
|
|
317
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
318
|
+
}
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
};
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
withTextLabel
|
|
11
11
|
} from '../../core/compose/features';
|
|
12
12
|
import { withAPI } from './api';
|
|
13
|
+
import {
|
|
14
|
+
withLeadingIcon,
|
|
15
|
+
withTrailingIcon,
|
|
16
|
+
withSupportingText
|
|
17
|
+
} from './features';
|
|
13
18
|
import { TextfieldConfig, TextfieldComponent } from './types';
|
|
14
19
|
import {
|
|
15
20
|
createBaseConfig,
|
|
@@ -33,6 +38,9 @@ const createTextfield = (config: TextfieldConfig = {}): TextfieldComponent => {
|
|
|
33
38
|
withVariant(baseConfig),
|
|
34
39
|
withTextInput(baseConfig),
|
|
35
40
|
withTextLabel(baseConfig),
|
|
41
|
+
withLeadingIcon(baseConfig),
|
|
42
|
+
withTrailingIcon(baseConfig),
|
|
43
|
+
withSupportingText(baseConfig),
|
|
36
44
|
withDisabled(baseConfig),
|
|
37
45
|
withLifecycle(),
|
|
38
46
|
comp => withAPI(getApiConfig(comp))(comp)
|
|
@@ -31,9 +31,6 @@ export interface TextfieldConfig {
|
|
|
31
31
|
/** Label text */
|
|
32
32
|
label?: string;
|
|
33
33
|
|
|
34
|
-
/** Placeholder text */
|
|
35
|
-
placeholder?: string;
|
|
36
|
-
|
|
37
34
|
/** Initial value */
|
|
38
35
|
value?: string;
|
|
39
36
|
|
|
@@ -52,6 +49,18 @@ export interface TextfieldConfig {
|
|
|
52
49
|
/** Autocomplete attribute */
|
|
53
50
|
autocomplete?: string;
|
|
54
51
|
|
|
52
|
+
/** Leading icon HTML content */
|
|
53
|
+
leadingIcon?: string;
|
|
54
|
+
|
|
55
|
+
/** Trailing icon HTML content */
|
|
56
|
+
trailingIcon?: string;
|
|
57
|
+
|
|
58
|
+
/** Supporting text content */
|
|
59
|
+
supportingText?: string;
|
|
60
|
+
|
|
61
|
+
/** Whether supporting text indicates an error */
|
|
62
|
+
error?: boolean;
|
|
63
|
+
|
|
55
64
|
/** Additional CSS classes */
|
|
56
65
|
class?: string;
|
|
57
66
|
|
|
@@ -48,12 +48,9 @@ const CLOCK_CONSTANTS = {
|
|
|
48
48
|
function getThemeColors(prefix: string): ThemeColors {
|
|
49
49
|
const root = document.documentElement;
|
|
50
50
|
const styles = getComputedStyle(root);
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
// Extract primary color
|
|
53
53
|
const primaryColor = styles.getPropertyValue(`--${prefix}-sys-color-primary`).trim() || '#6750A4';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('primaryColor', primaryColor)
|
|
57
54
|
|
|
58
55
|
// Extract on-primary color
|
|
59
56
|
const onPrimaryColor = styles.getPropertyValue(`--${prefix}-sys-color-on-primary`).trim() || '#FFFFFF';
|
|
@@ -112,14 +112,14 @@ export interface TextInputComponent extends ElementComponent {
|
|
|
112
112
|
*/
|
|
113
113
|
export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
|
|
114
114
|
<C extends ElementComponent>(component: C): C & TextInputComponent => {
|
|
115
|
-
const
|
|
115
|
+
const isMultiline = config.multiline || config.type === 'multiline';
|
|
116
|
+
const input = document.createElement(isMultiline ? 'textarea' : 'input') as
|
|
116
117
|
HTMLInputElement | HTMLTextAreaElement;
|
|
117
118
|
|
|
118
119
|
input.className = `${component.getClass('textfield')}-input`;
|
|
119
120
|
|
|
120
121
|
// Set input attributes
|
|
121
122
|
const attributes: Record<string, string | number | boolean | undefined> = {
|
|
122
|
-
type: config.multiline ? undefined : (config.type || 'text'),
|
|
123
123
|
name: config.name,
|
|
124
124
|
required: config.required,
|
|
125
125
|
disabled: config.disabled,
|
|
@@ -129,6 +129,14 @@ export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
|
|
|
129
129
|
value: config.value || ''
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
// Only set type attribute for input elements, not for textarea
|
|
133
|
+
if (!isMultiline) {
|
|
134
|
+
attributes.type = config.type || 'text';
|
|
135
|
+
} else {
|
|
136
|
+
// For textarea, add a data attribute to identify it as multiline
|
|
137
|
+
attributes['data-type'] = 'multiline';
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
133
141
|
if (value !== null && value !== undefined) {
|
|
134
142
|
if (typeof value === 'boolean') {
|
|
@@ -188,6 +196,11 @@ export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
|
|
|
188
196
|
// Initial state
|
|
189
197
|
updateInputState();
|
|
190
198
|
|
|
199
|
+
// Add multiline class to the component if it's a textarea
|
|
200
|
+
if (isMultiline) {
|
|
201
|
+
component.element.classList.add(`${component.getClass('textfield')}--multiline`);
|
|
202
|
+
}
|
|
203
|
+
|
|
191
204
|
component.element.appendChild(input);
|
|
192
205
|
|
|
193
206
|
// Cleanup
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/core/composition/features/dom.ts
|
|
2
|
+
import { createStructure, flattenStructure } from '../../structure';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates DOM elements from structure definition using the core createStructure utility
|
|
6
|
+
* This is a key feature that bridges the gap between declarative structure and actual DOM
|
|
7
|
+
*
|
|
8
|
+
* @returns Component enhancer that creates DOM structure
|
|
9
|
+
*/
|
|
10
|
+
export const withDom = () => component => {
|
|
11
|
+
// Return unmodified component if no structure definition
|
|
12
|
+
if (!component.structureDefinition) {
|
|
13
|
+
return component;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Use the existing createStructure function to build the DOM
|
|
18
|
+
const structure = createStructure(component.structureDefinition);
|
|
19
|
+
|
|
20
|
+
// Use the existing flattenStructure function to create a flat reference map
|
|
21
|
+
const components = structure.getAll();
|
|
22
|
+
|
|
23
|
+
// Return enhanced component with DOM structure
|
|
24
|
+
return {
|
|
25
|
+
...component,
|
|
26
|
+
element: structure.element, // Root element
|
|
27
|
+
components // All component elements
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to create DOM structure:', error);
|
|
31
|
+
throw new Error(`Failed to create slider DOM: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/core/composition/features/icon.ts
|
|
2
|
+
import { createElement } from '../../dom/create';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for icon feature
|
|
6
|
+
*/
|
|
7
|
+
export interface IconConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Icon HTML content
|
|
10
|
+
*/
|
|
11
|
+
icon?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Position of the icon ('start' or 'end')
|
|
15
|
+
*/
|
|
16
|
+
iconPosition?: 'start' | 'end';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Size variant for the icon
|
|
20
|
+
*/
|
|
21
|
+
iconSize?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CSS class prefix
|
|
25
|
+
*/
|
|
26
|
+
prefix?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Component name for class generation
|
|
30
|
+
*/
|
|
31
|
+
componentName?: string;
|
|
32
|
+
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enhances structure definition with an icon element
|
|
38
|
+
* Unlike the traditional withIcon, this modifies the structure definition
|
|
39
|
+
* without creating actual DOM elements.
|
|
40
|
+
*
|
|
41
|
+
* @param config Configuration containing icon information
|
|
42
|
+
* @returns Component enhancer that adds icon to structure definition
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // Add icon to a structure definition
|
|
47
|
+
* const component = pipe(
|
|
48
|
+
* createBase,
|
|
49
|
+
* withStructure(config),
|
|
50
|
+
* withIcon(config)
|
|
51
|
+
* )(config);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const withIcon = (config: IconConfig) => component => {
|
|
55
|
+
// If no icon or missing structure definition, return unmodified
|
|
56
|
+
if (!config.icon || !component.structureDefinition) {
|
|
57
|
+
return component;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Get component details for class names
|
|
62
|
+
const prefix = config.prefix || component.config?.prefix || 'mtrl';
|
|
63
|
+
const componentName = config.componentName || component.componentName || 'component';
|
|
64
|
+
|
|
65
|
+
// Clone the structure definition
|
|
66
|
+
const structureDefinition = JSON.parse(JSON.stringify(component.structureDefinition));
|
|
67
|
+
|
|
68
|
+
// Determine icon position
|
|
69
|
+
const position = config.iconPosition || 'start';
|
|
70
|
+
|
|
71
|
+
// Add the --icon modifier class to the main element
|
|
72
|
+
const elementClasses = structureDefinition.element.options.className || [];
|
|
73
|
+
const iconModifierClass = `${prefix}-${componentName}--icon`;
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(elementClasses)) {
|
|
76
|
+
if (!elementClasses.includes(iconModifierClass)) {
|
|
77
|
+
elementClasses.push(iconModifierClass);
|
|
78
|
+
}
|
|
79
|
+
} else if (typeof elementClasses === 'string') {
|
|
80
|
+
if (!elementClasses.includes(iconModifierClass)) {
|
|
81
|
+
structureDefinition.element.options.className = `${elementClasses} ${iconModifierClass}`.trim();
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
structureDefinition.element.options.className = [iconModifierClass];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create icon element definition with component-specific class
|
|
88
|
+
const iconDef = {
|
|
89
|
+
name: 'icon',
|
|
90
|
+
creator: createElement,
|
|
91
|
+
options: {
|
|
92
|
+
tag: 'span',
|
|
93
|
+
className: [
|
|
94
|
+
`${prefix}-${componentName}-icon`,
|
|
95
|
+
`${prefix}-${componentName}-icon--${position}`
|
|
96
|
+
],
|
|
97
|
+
html: config.icon
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Add size class if specified
|
|
102
|
+
if (config.iconSize) {
|
|
103
|
+
const classes = iconDef.options.className;
|
|
104
|
+
if (Array.isArray(classes)) {
|
|
105
|
+
classes.push(`${prefix}-${componentName}-icon--${config.iconSize}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add icon directly to the main element's children
|
|
110
|
+
if (position === 'start') {
|
|
111
|
+
// Create new children object with icon first
|
|
112
|
+
const existingChildren = { ...structureDefinition.element.children };
|
|
113
|
+
structureDefinition.element.children = {
|
|
114
|
+
icon: iconDef,
|
|
115
|
+
...existingChildren
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
// Add icon after existing children
|
|
119
|
+
structureDefinition.element.children.icon = iconDef;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Return component with updated structure definition
|
|
123
|
+
return {
|
|
124
|
+
...component,
|
|
125
|
+
structureDefinition
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn('Error enhancing structure with icon:', error);
|
|
129
|
+
return component;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/core/composition/features/index.ts
|
|
2
|
+
|
|
3
|
+
// Export composition features
|
|
4
|
+
export { withIcon } from './icon';
|
|
5
|
+
export { withLabel } from './label';
|
|
6
|
+
export { withDom } from './dom';
|
|
7
|
+
export { withStructure } from './structure';
|
|
8
|
+
|
|
9
|
+
// Re-export interface types for better developer experience
|
|
10
|
+
export type { IconConfig } from './icon';
|
|
11
|
+
export type { LabelConfig } from './label';
|