mtrl 0.3.6 → 0.3.8
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 +2 -2
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +61 -22
- package/src/components/menu/config.ts +10 -8
- package/src/components/menu/features/anchor.ts +254 -19
- package/src/components/menu/features/controller.ts +724 -271
- package/src/components/menu/features/index.ts +11 -2
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +5 -5
- package/src/components/menu/menu.ts +21 -61
- package/src/components/menu/types.ts +30 -16
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +331 -0
- package/src/components/select/index.ts +38 -0
- package/src/components/select/select.ts +73 -0
- package/src/components/select/types.ts +355 -0
- package/src/components/textfield/api.ts +78 -6
- package/src/components/textfield/features/index.ts +17 -0
- package/src/components/textfield/features/leading-icon.ts +127 -0
- package/src/components/textfield/features/placement.ts +149 -0
- package/src/components/textfield/features/prefix-text.ts +107 -0
- package/src/components/textfield/features/suffix-text.ts +100 -0
- package/src/components/textfield/features/supporting-text.ts +113 -0
- package/src/components/textfield/features/trailing-icon.ts +108 -0
- package/src/components/textfield/textfield.ts +51 -15
- package/src/components/textfield/types.ts +70 -0
- package/src/core/collection/adapters/base.ts +62 -0
- package/src/core/collection/collection.ts +300 -0
- package/src/core/collection/index.ts +57 -0
- package/src/core/collection/list-manager.ts +333 -0
- package/src/index.ts +4 -45
- package/src/styles/abstract/_variables.scss +18 -0
- package/src/styles/components/_button.scss +21 -5
- package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
- package/src/styles/components/_menu.scss +97 -24
- package/src/styles/components/_select.scss +272 -0
- package/src/styles/components/_textfield.scss +233 -42
- package/src/styles/main.scss +2 -1
- package/src/components/textfield/features.ts +0 -322
- package/src/core/collection/adapters/base.js +0 -26
- package/src/core/collection/collection.js +0 -259
- package/src/core/collection/list-manager.js +0 -157
- /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// src/components/select/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Available Select variants
|
|
5
|
+
*/
|
|
6
|
+
export type SelectVariant = 'filled' | 'outlined';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Select variant constants
|
|
10
|
+
*/
|
|
11
|
+
export const SELECT_VARIANTS = {
|
|
12
|
+
FILLED: 'filled',
|
|
13
|
+
OUTLINED: 'outlined'
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Select option interface
|
|
18
|
+
*/
|
|
19
|
+
export interface SelectOption {
|
|
20
|
+
/**
|
|
21
|
+
* Unique identifier for the option
|
|
22
|
+
*/
|
|
23
|
+
id: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Display text for the option
|
|
27
|
+
*/
|
|
28
|
+
text: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether the option is disabled
|
|
32
|
+
*/
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optional icon to display with the option
|
|
37
|
+
*/
|
|
38
|
+
icon?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether this option has a submenu
|
|
42
|
+
*/
|
|
43
|
+
hasSubmenu?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Optional array of submenu options
|
|
47
|
+
* Only used when hasSubmenu is true
|
|
48
|
+
*/
|
|
49
|
+
submenu?: SelectOption[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Additional data associated with the option
|
|
53
|
+
*/
|
|
54
|
+
data?: any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Configuration interface for the Select component
|
|
59
|
+
*/
|
|
60
|
+
export interface SelectConfig {
|
|
61
|
+
/**
|
|
62
|
+
* Array of options to display in the select menu
|
|
63
|
+
*/
|
|
64
|
+
options: SelectOption[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Currently selected value (option id)
|
|
68
|
+
*/
|
|
69
|
+
value?: string;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Visual variant (filled, outlined)
|
|
73
|
+
*/
|
|
74
|
+
variant?: SelectVariant | string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Label text
|
|
78
|
+
*/
|
|
79
|
+
label?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Input name attribute
|
|
83
|
+
*/
|
|
84
|
+
name?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether select is required
|
|
88
|
+
*/
|
|
89
|
+
required?: boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether select is disabled
|
|
93
|
+
*/
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Supporting text content
|
|
98
|
+
*/
|
|
99
|
+
supportingText?: string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether supporting text indicates an error
|
|
103
|
+
*/
|
|
104
|
+
error?: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Menu placement relative to the textfield
|
|
108
|
+
*/
|
|
109
|
+
placement?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Additional CSS classes
|
|
113
|
+
*/
|
|
114
|
+
class?: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Prefix for class names
|
|
118
|
+
*/
|
|
119
|
+
prefix?: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Component name
|
|
123
|
+
*/
|
|
124
|
+
componentName?: string;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Event callbacks
|
|
128
|
+
*/
|
|
129
|
+
on?: {
|
|
130
|
+
/**
|
|
131
|
+
* Called when the select value changes
|
|
132
|
+
*/
|
|
133
|
+
change?: (event: SelectChangeEvent) => void;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Called when the select menu opens
|
|
137
|
+
*/
|
|
138
|
+
open?: (event: SelectEvent) => void;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Called when the select menu closes
|
|
142
|
+
*/
|
|
143
|
+
close?: (event: SelectEvent) => void;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Select component interface
|
|
149
|
+
*/
|
|
150
|
+
export interface SelectComponent {
|
|
151
|
+
/**
|
|
152
|
+
* The root element of the select
|
|
153
|
+
*/
|
|
154
|
+
element: HTMLElement;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The textfield component
|
|
158
|
+
*/
|
|
159
|
+
textfield: any;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The menu component
|
|
163
|
+
*/
|
|
164
|
+
menu: any;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Gets the select's current value (selected option id)
|
|
168
|
+
*/
|
|
169
|
+
getValue: () => string | null;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sets the select's value (by option id)
|
|
173
|
+
* @param value - Option id to select
|
|
174
|
+
* @returns Select component for chaining
|
|
175
|
+
*/
|
|
176
|
+
setValue: (value: string) => SelectComponent;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Gets the select's current displayed text
|
|
180
|
+
*/
|
|
181
|
+
getText: () => string;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the selected option object
|
|
185
|
+
*/
|
|
186
|
+
getSelectedOption: () => SelectOption | null;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets all available options
|
|
190
|
+
*/
|
|
191
|
+
getOptions: () => SelectOption[];
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Sets new options
|
|
195
|
+
* @param options - New options array
|
|
196
|
+
* @returns Select component for chaining
|
|
197
|
+
*/
|
|
198
|
+
setOptions: (options: SelectOption[]) => SelectComponent;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Opens the select menu
|
|
202
|
+
* @param interactionType - The type of interaction ('mouse' or 'keyboard')
|
|
203
|
+
* @returns Select component for chaining
|
|
204
|
+
*/
|
|
205
|
+
open: (interactionType?: 'mouse' | 'keyboard') => SelectComponent;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Closes the select menu
|
|
209
|
+
* @returns Select component for chaining
|
|
210
|
+
*/
|
|
211
|
+
close: () => SelectComponent;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks if the menu is open
|
|
215
|
+
*/
|
|
216
|
+
isOpen: () => boolean;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Adds an event listener
|
|
220
|
+
* @param event - Event name
|
|
221
|
+
* @param handler - Event handler
|
|
222
|
+
* @returns Select component for chaining
|
|
223
|
+
*/
|
|
224
|
+
on: <T extends keyof SelectEvents>(event: T, handler: SelectEvents[T]) => SelectComponent;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Removes an event listener
|
|
228
|
+
* @param event - Event name
|
|
229
|
+
* @param handler - Event handler
|
|
230
|
+
* @returns Select component for chaining
|
|
231
|
+
*/
|
|
232
|
+
off: <T extends keyof SelectEvents>(event: T, handler: SelectEvents[T]) => SelectComponent;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Enables the select
|
|
236
|
+
* @returns Select component for chaining
|
|
237
|
+
*/
|
|
238
|
+
enable: () => SelectComponent;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Disables the select
|
|
242
|
+
* @returns Select component for chaining
|
|
243
|
+
*/
|
|
244
|
+
disable: () => SelectComponent;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Destroys the select component
|
|
248
|
+
*/
|
|
249
|
+
destroy: () => void;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Select event interface
|
|
254
|
+
*/
|
|
255
|
+
export interface SelectEvent {
|
|
256
|
+
/**
|
|
257
|
+
* The select component
|
|
258
|
+
*/
|
|
259
|
+
select: SelectComponent;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Original DOM event if available
|
|
263
|
+
*/
|
|
264
|
+
originalEvent?: Event;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Function to prevent default behavior
|
|
268
|
+
*/
|
|
269
|
+
preventDefault: () => void;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Whether default behavior was prevented
|
|
273
|
+
*/
|
|
274
|
+
defaultPrevented: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Select change event interface
|
|
279
|
+
*/
|
|
280
|
+
export interface SelectChangeEvent extends SelectEvent {
|
|
281
|
+
/**
|
|
282
|
+
* The selected option id
|
|
283
|
+
*/
|
|
284
|
+
value: string;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The selected option text
|
|
288
|
+
*/
|
|
289
|
+
text: string;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The complete selected option object
|
|
293
|
+
*/
|
|
294
|
+
option: SelectOption;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Select events interface for type-checking
|
|
299
|
+
* @internal
|
|
300
|
+
*/
|
|
301
|
+
export interface SelectEvents {
|
|
302
|
+
'change': (event: SelectChangeEvent) => void;
|
|
303
|
+
'open': (event: SelectEvent) => void;
|
|
304
|
+
'close': (event: SelectEvent) => void;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* API options interface
|
|
309
|
+
* @internal
|
|
310
|
+
*/
|
|
311
|
+
export interface ApiOptions {
|
|
312
|
+
select: {
|
|
313
|
+
getValue: () => string | null;
|
|
314
|
+
setValue: (value: string) => any;
|
|
315
|
+
getText: () => string;
|
|
316
|
+
getSelectedOption: () => SelectOption | null;
|
|
317
|
+
getOptions: () => SelectOption[];
|
|
318
|
+
setOptions: (options: SelectOption[]) => any;
|
|
319
|
+
open: () => any;
|
|
320
|
+
close: () => any;
|
|
321
|
+
isOpen: () => boolean;
|
|
322
|
+
};
|
|
323
|
+
events?: {
|
|
324
|
+
on: <T extends string>(event: T, handler: (event: any) => void) => any;
|
|
325
|
+
off: <T extends string>(event: T, handler: (event: any) => void) => any;
|
|
326
|
+
};
|
|
327
|
+
disabled: {
|
|
328
|
+
enable: () => any;
|
|
329
|
+
disable: () => any;
|
|
330
|
+
};
|
|
331
|
+
lifecycle: {
|
|
332
|
+
destroy: () => void;
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Base component interface
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
340
|
+
export interface BaseComponent {
|
|
341
|
+
element: HTMLElement;
|
|
342
|
+
textfield?: any;
|
|
343
|
+
menu?: any;
|
|
344
|
+
on?: (event: string, handler: Function) => any;
|
|
345
|
+
off?: (event: string, handler: Function) => any;
|
|
346
|
+
emit?: (event: string, data: any) => void;
|
|
347
|
+
disabled?: {
|
|
348
|
+
enable: () => any;
|
|
349
|
+
disable: () => any;
|
|
350
|
+
};
|
|
351
|
+
lifecycle?: {
|
|
352
|
+
destroy: () => void;
|
|
353
|
+
};
|
|
354
|
+
[key: string]: any;
|
|
355
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
// src/components/textfield/api.ts
|
|
2
2
|
import { BaseComponent, TextfieldComponent, ApiOptions } from './types';
|
|
3
|
+
import { PlacementComponent } from './features/placement';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Enhances textfield component with API methods
|
|
6
|
-
* @param {ApiOptions} options - API configuration
|
|
7
|
-
* @returns {Function} Higher-order function that adds API methods to component
|
|
8
|
-
*/
|
|
9
5
|
/**
|
|
10
6
|
* Enhances textfield component with API methods
|
|
11
7
|
* @param {ApiOptions} options - API configuration
|
|
12
8
|
* @returns {Function} Higher-order function that adds API methods to component
|
|
13
9
|
*/
|
|
14
10
|
export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
15
|
-
(component: BaseComponent): TextfieldComponent => ({
|
|
11
|
+
(component: BaseComponent & Partial<PlacementComponent>): TextfieldComponent => ({
|
|
16
12
|
...component as any,
|
|
17
13
|
element: component.element,
|
|
18
14
|
input: component.input as HTMLInputElement | HTMLTextAreaElement,
|
|
@@ -42,6 +38,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
42
38
|
// Label management
|
|
43
39
|
setLabel(text: string): TextfieldComponent {
|
|
44
40
|
component.label?.setText(text);
|
|
41
|
+
// Update positions after changing label
|
|
42
|
+
if (component.updateElementPositions) {
|
|
43
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
44
|
+
}
|
|
45
45
|
return this;
|
|
46
46
|
},
|
|
47
47
|
|
|
@@ -54,6 +54,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
54
54
|
setLeadingIcon(html: string): TextfieldComponent {
|
|
55
55
|
if (component.setLeadingIcon) {
|
|
56
56
|
component.setLeadingIcon(html);
|
|
57
|
+
// Update positions after changing icon
|
|
58
|
+
if (component.updateElementPositions) {
|
|
59
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
60
|
+
}
|
|
57
61
|
}
|
|
58
62
|
return this;
|
|
59
63
|
},
|
|
@@ -61,6 +65,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
61
65
|
removeLeadingIcon(): TextfieldComponent {
|
|
62
66
|
if (component.removeLeadingIcon) {
|
|
63
67
|
component.removeLeadingIcon();
|
|
68
|
+
// Update positions after removing icon
|
|
69
|
+
if (component.updateElementPositions) {
|
|
70
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
return this;
|
|
66
74
|
},
|
|
@@ -70,6 +78,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
70
78
|
setTrailingIcon(html: string): TextfieldComponent {
|
|
71
79
|
if (component.setTrailingIcon) {
|
|
72
80
|
component.setTrailingIcon(html);
|
|
81
|
+
// Update positions after changing icon
|
|
82
|
+
if (component.updateElementPositions) {
|
|
83
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
return this;
|
|
75
87
|
},
|
|
@@ -77,6 +89,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
77
89
|
removeTrailingIcon(): TextfieldComponent {
|
|
78
90
|
if (component.removeTrailingIcon) {
|
|
79
91
|
component.removeTrailingIcon();
|
|
92
|
+
// Update positions after removing icon
|
|
93
|
+
if (component.updateElementPositions) {
|
|
94
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
return this;
|
|
82
98
|
},
|
|
@@ -96,6 +112,62 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
|
96
112
|
}
|
|
97
113
|
return this;
|
|
98
114
|
},
|
|
115
|
+
|
|
116
|
+
// Prefix text management (if present)
|
|
117
|
+
prefixTextElement: component.prefixTextElement || null,
|
|
118
|
+
setPrefixText(text: string): TextfieldComponent {
|
|
119
|
+
if (component.setPrefixText) {
|
|
120
|
+
component.setPrefixText(text);
|
|
121
|
+
// Update positions after changing prefix
|
|
122
|
+
if (component.updateElementPositions) {
|
|
123
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
removePrefixText(): TextfieldComponent {
|
|
130
|
+
if (component.removePrefixText) {
|
|
131
|
+
component.removePrefixText();
|
|
132
|
+
// Update positions after removing prefix
|
|
133
|
+
if (component.updateElementPositions) {
|
|
134
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return this;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Suffix text management (if present)
|
|
141
|
+
suffixTextElement: component.suffixTextElement || null,
|
|
142
|
+
setSuffixText(text: string): TextfieldComponent {
|
|
143
|
+
if (component.setSuffixText) {
|
|
144
|
+
component.setSuffixText(text);
|
|
145
|
+
// Update positions after changing suffix
|
|
146
|
+
if (component.updateElementPositions) {
|
|
147
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return this;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
removeSuffixText(): TextfieldComponent {
|
|
154
|
+
if (component.removeSuffixText) {
|
|
155
|
+
component.removeSuffixText();
|
|
156
|
+
// Update positions after removing suffix
|
|
157
|
+
if (component.updateElementPositions) {
|
|
158
|
+
setTimeout(() => component.updateElementPositions(), 10);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Update positioning manually (useful after DOM updates)
|
|
165
|
+
updatePositions(): TextfieldComponent {
|
|
166
|
+
if (component.updateElementPositions) {
|
|
167
|
+
component.updateElementPositions();
|
|
168
|
+
}
|
|
169
|
+
return this;
|
|
170
|
+
},
|
|
99
171
|
|
|
100
172
|
// Event handling
|
|
101
173
|
on(event: string, handler: Function): TextfieldComponent {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/components/textfield/features/index.ts
|
|
2
|
+
|
|
3
|
+
// Export features
|
|
4
|
+
export { withLeadingIcon } from './leading-icon';
|
|
5
|
+
export { withTrailingIcon } from './trailing-icon';
|
|
6
|
+
export { withPrefixText } from './prefix-text';
|
|
7
|
+
export { withSuffixText } from './suffix-text';
|
|
8
|
+
export { withSupportingText } from './supporting-text';
|
|
9
|
+
export { withPlacement } from './placement';
|
|
10
|
+
|
|
11
|
+
// Export interfaces
|
|
12
|
+
export type { LeadingIconComponent, LeadingIconConfig } from './leading-icon';
|
|
13
|
+
export type { TrailingIconComponent, TrailingIconConfig } from './trailing-icon';
|
|
14
|
+
export type { PrefixTextComponent, PrefixTextConfig } from './prefix-text';
|
|
15
|
+
export type { SuffixTextComponent, SuffixTextConfig } from './suffix-text';
|
|
16
|
+
export type { SupportingTextComponent, SupportingTextConfig } from './supporting-text';
|
|
17
|
+
export type { PlacementComponent } from './placement';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/components/textfield/features/leading-icon.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for leading icon feature
|
|
7
|
+
*/
|
|
8
|
+
export interface LeadingIconConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Leading icon HTML content
|
|
11
|
+
*/
|
|
12
|
+
leadingIcon?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CSS class prefix
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Component name
|
|
21
|
+
*/
|
|
22
|
+
componentName?: string;
|
|
23
|
+
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component with leading icon capabilities
|
|
29
|
+
*/
|
|
30
|
+
export interface LeadingIconComponent extends BaseComponent {
|
|
31
|
+
/**
|
|
32
|
+
* Leading icon element
|
|
33
|
+
*/
|
|
34
|
+
leadingIcon: HTMLElement | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets leading icon content
|
|
38
|
+
* @param html - HTML content for the icon
|
|
39
|
+
* @returns Component instance for chaining
|
|
40
|
+
*/
|
|
41
|
+
setLeadingIcon: (html: string) => LeadingIconComponent;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Removes leading icon
|
|
45
|
+
* @returns Component instance for chaining
|
|
46
|
+
*/
|
|
47
|
+
removeLeadingIcon: () => LeadingIconComponent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds leading icon to a textfield component
|
|
52
|
+
* @param config - Configuration with leading icon settings
|
|
53
|
+
* @returns Function that enhances a component with leading icon
|
|
54
|
+
*/
|
|
55
|
+
export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
|
|
56
|
+
<C extends ElementComponent>(component: C): C & LeadingIconComponent => {
|
|
57
|
+
if (!config.leadingIcon) {
|
|
58
|
+
return component as any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create icon element
|
|
62
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
63
|
+
const iconElement = document.createElement('span');
|
|
64
|
+
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
|
|
65
|
+
iconElement.innerHTML = config.leadingIcon;
|
|
66
|
+
|
|
67
|
+
// Add leading icon to the component
|
|
68
|
+
component.element.appendChild(iconElement);
|
|
69
|
+
|
|
70
|
+
// Add leading-icon class to the component
|
|
71
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
72
|
+
|
|
73
|
+
// When there's a leading icon, adjust input padding
|
|
74
|
+
if (component.input) {
|
|
75
|
+
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add lifecycle integration if available
|
|
79
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
80
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
81
|
+
component.lifecycle.destroy = () => {
|
|
82
|
+
iconElement.remove();
|
|
83
|
+
originalDestroy.call(component.lifecycle);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update label position based on icon
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
90
|
+
if (labelEl) {
|
|
91
|
+
if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
|
|
92
|
+
(labelEl as HTMLElement).style.left = '44px';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, 10);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...component,
|
|
99
|
+
leadingIcon: iconElement,
|
|
100
|
+
|
|
101
|
+
setLeadingIcon(html: string) {
|
|
102
|
+
iconElement.innerHTML = html;
|
|
103
|
+
return this;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
removeLeadingIcon() {
|
|
107
|
+
if (iconElement.parentNode) {
|
|
108
|
+
iconElement.remove();
|
|
109
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
110
|
+
if (component.input) {
|
|
111
|
+
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Reset label position if no prefix
|
|
115
|
+
if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
|
|
116
|
+
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
117
|
+
if (labelEl) {
|
|
118
|
+
(labelEl as HTMLElement).style.left = '';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.leadingIcon = null;
|
|
123
|
+
}
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
};
|