mtrl 0.3.5 → 0.3.7
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 +1 -1
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +144 -267
- package/src/components/menu/config.ts +84 -40
- package/src/components/menu/features/anchor.ts +243 -0
- package/src/components/menu/features/controller.ts +1167 -0
- package/src/components/menu/features/index.ts +5 -0
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +31 -63
- package/src/components/menu/menu.ts +72 -104
- package/src/components/menu/types.ts +264 -447
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +317 -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/core/dom/classes.ts +81 -9
- package/src/core/dom/create.ts +30 -19
- package/src/core/layout/README.md +531 -166
- package/src/core/layout/array.ts +3 -4
- package/src/core/layout/config.ts +193 -0
- package/src/core/layout/create.ts +1 -2
- package/src/core/layout/index.ts +12 -2
- package/src/core/layout/object.ts +2 -3
- package/src/core/layout/processor.ts +60 -12
- package/src/core/layout/result.ts +1 -2
- package/src/core/layout/types.ts +105 -50
- package/src/core/layout/utils.ts +69 -61
- package/src/index.ts +6 -2
- 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 +109 -18
- package/src/styles/components/_select.scss +265 -0
- package/src/styles/components/_textfield.scss +233 -42
- package/src/styles/main.scss +24 -23
- package/src/styles/utilities/_layout.scss +665 -0
- package/src/components/menu/features/items-manager.ts +0 -457
- package/src/components/menu/features/keyboard-navigation.ts +0 -133
- package/src/components/menu/features/positioning.ts +0 -127
- package/src/components/menu/features/visibility.ts +0 -230
- package/src/components/menu/menu-item.ts +0 -86
- package/src/components/menu/utils.ts +0 -67
- 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
- /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
// src/components/menu/features/visibility.ts
|
|
2
|
-
import { BaseComponent, MenuConfig } from '../types';
|
|
3
|
-
import { MENU_EVENT, MENU_CLASSES } from '../utils';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Adds visibility management functionality to a menu component
|
|
7
|
-
*
|
|
8
|
-
* This feature adds the ability to show and hide the menu with smooth transitions,
|
|
9
|
-
* along with proper event handling for clicks outside the menu and keyboard shortcuts.
|
|
10
|
-
* It implements the following functionality:
|
|
11
|
-
*
|
|
12
|
-
* - Tracking visibility state
|
|
13
|
-
* - Showing the menu with animation
|
|
14
|
-
* - Hiding the menu with animation
|
|
15
|
-
* - Automatic handling of clicks outside the menu
|
|
16
|
-
* - Keyboard shortcut (Escape) for dismissing the menu
|
|
17
|
-
* - Proper ARIA attributes for accessibility
|
|
18
|
-
* - Event emission for component state changes
|
|
19
|
-
*
|
|
20
|
-
* @param {MenuConfig} config - Menu configuration options
|
|
21
|
-
* @returns {Function} Component enhancer function that adds visibility features
|
|
22
|
-
*
|
|
23
|
-
* @internal
|
|
24
|
-
* @category Components
|
|
25
|
-
*/
|
|
26
|
-
export const withVisibility = (config: MenuConfig) => (component: BaseComponent): BaseComponent => {
|
|
27
|
-
let isVisible = false;
|
|
28
|
-
let outsideClickHandler: ((event: MouseEvent) => void) | null = null;
|
|
29
|
-
let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
|
|
30
|
-
const prefix = config.prefix || 'mtrl';
|
|
31
|
-
|
|
32
|
-
// Create the component interface with hide/show methods first
|
|
33
|
-
const enhancedComponent: BaseComponent = {
|
|
34
|
-
...component,
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Shows the menu
|
|
38
|
-
*
|
|
39
|
-
* Makes the menu visible with a smooth transition animation.
|
|
40
|
-
* Handles DOM insertion, event binding, and ARIA attribute updates.
|
|
41
|
-
* Emits an 'open' event when the menu becomes visible.
|
|
42
|
-
*
|
|
43
|
-
* @returns {BaseComponent} The component instance for method chaining
|
|
44
|
-
* @internal
|
|
45
|
-
*/
|
|
46
|
-
show() {
|
|
47
|
-
if (isVisible) return this;
|
|
48
|
-
|
|
49
|
-
// First set visibility to true to prevent multiple calls
|
|
50
|
-
isVisible = true;
|
|
51
|
-
|
|
52
|
-
// Make sure the element is in the DOM
|
|
53
|
-
if (!component.element.parentNode) {
|
|
54
|
-
document.body.appendChild(component.element);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Always clean up previous handlers before adding new ones
|
|
58
|
-
if (outsideClickHandler) {
|
|
59
|
-
document.removeEventListener('mousedown', outsideClickHandler);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Setup outside click handler for closing
|
|
63
|
-
outsideClickHandler = handleOutsideClick;
|
|
64
|
-
|
|
65
|
-
// Use setTimeout to ensure the handler is not triggered immediately
|
|
66
|
-
setTimeout(() => {
|
|
67
|
-
document.addEventListener('mousedown', outsideClickHandler!);
|
|
68
|
-
}, 0);
|
|
69
|
-
|
|
70
|
-
// Setup keyboard navigation
|
|
71
|
-
if (!keydownHandler) {
|
|
72
|
-
keydownHandler = handleKeydown;
|
|
73
|
-
document.addEventListener('keydown', keydownHandler);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Add display block first for transition to work
|
|
77
|
-
component.element.style.display = 'block';
|
|
78
|
-
|
|
79
|
-
// Force a reflow before adding the visible class for animation
|
|
80
|
-
// eslint-disable-next-line no-void
|
|
81
|
-
void component.element.offsetHeight;
|
|
82
|
-
component.element.classList.add(`${prefix}-${MENU_CLASSES.VISIBLE}`);
|
|
83
|
-
component.element.setAttribute('aria-hidden', 'false');
|
|
84
|
-
|
|
85
|
-
// Emit open event
|
|
86
|
-
component.emit?.(MENU_EVENT.OPEN, {});
|
|
87
|
-
|
|
88
|
-
return this;
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Hides the menu
|
|
93
|
-
*
|
|
94
|
-
* Makes the menu invisible with a smooth transition animation.
|
|
95
|
-
* Handles event cleanup, ARIA attribute updates, and DOM removal.
|
|
96
|
-
* Emits a 'close' event when the menu becomes hidden.
|
|
97
|
-
* Also ensures any open submenus are closed first.
|
|
98
|
-
*
|
|
99
|
-
* @returns {BaseComponent} The component instance for method chaining
|
|
100
|
-
* @internal
|
|
101
|
-
*/
|
|
102
|
-
hide() {
|
|
103
|
-
// Return early if already hidden
|
|
104
|
-
if (!isVisible) return this;
|
|
105
|
-
|
|
106
|
-
// First set the visibility flag to false
|
|
107
|
-
isVisible = false;
|
|
108
|
-
|
|
109
|
-
// Close any open submenus first
|
|
110
|
-
if (component.closeSubmenus) {
|
|
111
|
-
component.closeSubmenus();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Remove ALL event listeners
|
|
115
|
-
if (outsideClickHandler) {
|
|
116
|
-
document.removeEventListener('mousedown', outsideClickHandler);
|
|
117
|
-
outsideClickHandler = null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (keydownHandler) {
|
|
121
|
-
document.removeEventListener('keydown', keydownHandler);
|
|
122
|
-
keydownHandler = null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Hide the menu with visual indication first
|
|
126
|
-
component.element.classList.remove(`${prefix}-${MENU_CLASSES.VISIBLE}`);
|
|
127
|
-
component.element.setAttribute('aria-hidden', 'true');
|
|
128
|
-
|
|
129
|
-
// Define a reliable cleanup function
|
|
130
|
-
const cleanupElement = () => {
|
|
131
|
-
// Safety check to prevent errors
|
|
132
|
-
if (component.element) {
|
|
133
|
-
component.element.style.display = 'none';
|
|
134
|
-
|
|
135
|
-
// Remove from DOM if still attached
|
|
136
|
-
if (component.element.parentNode) {
|
|
137
|
-
component.element.remove();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Try to use transition end for smooth animation
|
|
143
|
-
const handleTransitionEnd = (e: TransitionEvent) => {
|
|
144
|
-
if (e.propertyName === 'opacity' || e.propertyName === 'transform') {
|
|
145
|
-
component.element.removeEventListener('transitionend', handleTransitionEnd);
|
|
146
|
-
cleanupElement();
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
component.element.addEventListener('transitionend', handleTransitionEnd);
|
|
151
|
-
|
|
152
|
-
// Fallback timeout in case transition events don't fire
|
|
153
|
-
// This ensures the menu always gets removed
|
|
154
|
-
setTimeout(cleanupElement, 300);
|
|
155
|
-
|
|
156
|
-
// Emit close event
|
|
157
|
-
component.emit?.(MENU_EVENT.CLOSE, {});
|
|
158
|
-
|
|
159
|
-
return this;
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Returns whether the menu is currently visible
|
|
164
|
-
*
|
|
165
|
-
* Provides the current visibility state of the menu component.
|
|
166
|
-
* This method is used internally and exposed via the public API.
|
|
167
|
-
*
|
|
168
|
-
* @returns {boolean} True if the menu is visible, false otherwise
|
|
169
|
-
* @internal
|
|
170
|
-
*/
|
|
171
|
-
isVisible() {
|
|
172
|
-
return isVisible;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Handles clicks outside the menu
|
|
178
|
-
*
|
|
179
|
-
* Event handler for detecting and responding to mouse clicks outside the menu.
|
|
180
|
-
* Checks if the click occurred outside both the menu and its origin element,
|
|
181
|
-
* and if so, hides the menu. This provides the auto-dismiss behavior expected
|
|
182
|
-
* of temporary surfaces like menus.
|
|
183
|
-
*
|
|
184
|
-
* @param {MouseEvent} event - Mouse event containing target information
|
|
185
|
-
* @internal
|
|
186
|
-
*/
|
|
187
|
-
const handleOutsideClick = (event: MouseEvent) => {
|
|
188
|
-
if (!isVisible) return;
|
|
189
|
-
|
|
190
|
-
// Store the opening button if available
|
|
191
|
-
const origin = config.origin?.element;
|
|
192
|
-
|
|
193
|
-
// Check if click is outside the menu but not on the opening button
|
|
194
|
-
const clickedElement = event.target as Node;
|
|
195
|
-
|
|
196
|
-
// Don't close if the click is inside the menu
|
|
197
|
-
if (component.element.contains(clickedElement)) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Don't close if the click is on the opening button (it will handle opening/closing)
|
|
202
|
-
if (origin && (origin === clickedElement || origin.contains(clickedElement))) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// If we got here, close the menu
|
|
207
|
-
enhancedComponent.hide?.();
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Handles keyboard events for the menu
|
|
212
|
-
*
|
|
213
|
-
* Event handler for keyboard interactions with the menu.
|
|
214
|
-
* Currently implements the Escape key to dismiss the menu,
|
|
215
|
-
* following standard interaction patterns for temporary UI surfaces.
|
|
216
|
-
*
|
|
217
|
-
* @param {KeyboardEvent} event - Keyboard event containing key information
|
|
218
|
-
* @internal
|
|
219
|
-
*/
|
|
220
|
-
const handleKeydown = (event: KeyboardEvent) => {
|
|
221
|
-
if (!isVisible) return;
|
|
222
|
-
|
|
223
|
-
if (event.key === 'Escape') {
|
|
224
|
-
event.preventDefault();
|
|
225
|
-
enhancedComponent.hide?.();
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
return enhancedComponent;
|
|
230
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// src/components/menu/menu-item.ts
|
|
2
|
-
import { MenuItemConfig } from './types';
|
|
3
|
-
import { MENU_ITEM_TYPE, getMenuClass } from './utils';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a DOM element for a menu item
|
|
7
|
-
*
|
|
8
|
-
* Generates an HTMLElement (li) based on the provided configuration.
|
|
9
|
-
* Handles different types of menu items (standard, divider, submenu),
|
|
10
|
-
* applies proper CSS classes, and sets appropriate ARIA attributes
|
|
11
|
-
* for accessibility.
|
|
12
|
-
*
|
|
13
|
-
* @param {MenuItemConfig} itemConfig - Item configuration
|
|
14
|
-
* @param {string} prefix - CSS class prefix (default: 'mtrl')
|
|
15
|
-
* @returns {HTMLElement} Menu item DOM element
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* // Create a standard menu item
|
|
20
|
-
* const itemElement = createMenuItem(
|
|
21
|
-
* { name: 'edit', text: 'Edit' },
|
|
22
|
-
* 'mtrl'
|
|
23
|
-
* );
|
|
24
|
-
*
|
|
25
|
-
* // Create a disabled menu item
|
|
26
|
-
* const disabledItem = createMenuItem(
|
|
27
|
-
* { name: 'print', text: 'Print', disabled: true },
|
|
28
|
-
* 'mtrl'
|
|
29
|
-
* );
|
|
30
|
-
*
|
|
31
|
-
* // Create a divider
|
|
32
|
-
* const divider = createMenuItem(
|
|
33
|
-
* { type: 'divider' },
|
|
34
|
-
* 'mtrl'
|
|
35
|
-
* );
|
|
36
|
-
*
|
|
37
|
-
* // Create an item with submenu indicator
|
|
38
|
-
* const submenuItem = createMenuItem(
|
|
39
|
-
* {
|
|
40
|
-
* name: 'share',
|
|
41
|
-
* text: 'Share',
|
|
42
|
-
* items: [
|
|
43
|
-
* { name: 'email', text: 'Email' },
|
|
44
|
-
* { name: 'link', text: 'Copy Link' }
|
|
45
|
-
* ]
|
|
46
|
-
* },
|
|
47
|
-
* 'mtrl'
|
|
48
|
-
* );
|
|
49
|
-
* ```
|
|
50
|
-
*
|
|
51
|
-
* @internal
|
|
52
|
-
* @category Components
|
|
53
|
-
*/
|
|
54
|
-
export const createMenuItem = (itemConfig: MenuItemConfig, prefix: string): HTMLElement => {
|
|
55
|
-
const item = document.createElement('li');
|
|
56
|
-
item.className = `${prefix}-${getMenuClass('ITEM')}`;
|
|
57
|
-
|
|
58
|
-
if (itemConfig.type === MENU_ITEM_TYPE.DIVIDER) {
|
|
59
|
-
item.className = `${prefix}-${getMenuClass('DIVIDER')}`;
|
|
60
|
-
return item;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (itemConfig.class) {
|
|
64
|
-
item.className += ` ${itemConfig.class}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (itemConfig.disabled) {
|
|
68
|
-
item.setAttribute('aria-disabled', 'true');
|
|
69
|
-
item.className += ` ${prefix}-${getMenuClass('ITEM')}--disabled`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (itemConfig.name) {
|
|
73
|
-
item.setAttribute('data-name', itemConfig.name);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
item.textContent = itemConfig.text || '';
|
|
77
|
-
|
|
78
|
-
if (itemConfig.items?.length) {
|
|
79
|
-
item.className += ` ${prefix}-${getMenuClass('ITEM')}--submenu`;
|
|
80
|
-
item.setAttribute('aria-haspopup', 'true');
|
|
81
|
-
item.setAttribute('aria-expanded', 'false');
|
|
82
|
-
// We don't need to add a submenu indicator as it's handled by CSS ::after
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return item;
|
|
86
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// src/components/menu/utils.ts
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Menu alignment constants for internal use
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
export const MENU_ALIGNMENT = {
|
|
8
|
-
LEFT: 'left',
|
|
9
|
-
RIGHT: 'right',
|
|
10
|
-
CENTER: 'center'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Menu vertical alignment constants for internal use
|
|
15
|
-
* @internal
|
|
16
|
-
*/
|
|
17
|
-
export const MENU_VERTICAL_ALIGNMENT = {
|
|
18
|
-
TOP: 'top',
|
|
19
|
-
BOTTOM: 'bottom',
|
|
20
|
-
MIDDLE: 'middle'
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Menu item types for internal use
|
|
25
|
-
* @internal
|
|
26
|
-
*/
|
|
27
|
-
export const MENU_ITEM_TYPE = {
|
|
28
|
-
ITEM: 'item',
|
|
29
|
-
DIVIDER: 'divider'
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Menu events for internal use
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
export const MENU_EVENT = {
|
|
37
|
-
SELECT: 'select',
|
|
38
|
-
OPEN: 'open',
|
|
39
|
-
CLOSE: 'close',
|
|
40
|
-
SUBMENU_OPEN: 'submenuOpen',
|
|
41
|
-
SUBMENU_CLOSE: 'submenuClose'
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Menu CSS classes for internal use
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
export const MENU_CLASSES = {
|
|
49
|
-
ROOT: 'menu',
|
|
50
|
-
ITEM: 'menu-item',
|
|
51
|
-
ITEM_CONTAINER: 'menu-item-container',
|
|
52
|
-
LIST: 'menu-list',
|
|
53
|
-
DIVIDER: 'menu-divider',
|
|
54
|
-
SUBMENU: 'menu--submenu',
|
|
55
|
-
VISIBLE: 'menu--visible',
|
|
56
|
-
DISABLED: 'menu--disabled'
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Gets a class name for menu elements
|
|
61
|
-
* @param {string} element - Element name from MENU_CLASSES
|
|
62
|
-
* @returns {string} The class name
|
|
63
|
-
* @internal
|
|
64
|
-
*/
|
|
65
|
-
export const getMenuClass = (element: keyof typeof MENU_CLASSES): string => {
|
|
66
|
-
return MENU_CLASSES[element];
|
|
67
|
-
};
|
|
@@ -1,322 +0,0 @@
|
|
|
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
|
-
};
|