mtrl 0.2.2 → 0.2.4
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/.typedocignore +11 -0
- package/DOCS.md +153 -0
- package/index.ts +18 -3
- package/package.json +7 -2
- package/src/components/badge/_styles.scss +174 -0
- package/src/components/badge/api.ts +292 -0
- package/src/components/badge/badge.ts +52 -0
- package/src/components/badge/config.ts +68 -0
- package/src/components/badge/constants.ts +30 -0
- package/src/components/badge/features.ts +185 -0
- package/src/components/badge/index.ts +4 -0
- package/src/components/badge/types.ts +105 -0
- package/src/components/button/types.ts +174 -29
- package/src/components/carousel/_styles.scss +645 -0
- package/src/components/carousel/api.ts +147 -0
- package/src/components/carousel/carousel.ts +178 -0
- package/src/components/carousel/config.ts +91 -0
- package/src/components/carousel/constants.ts +95 -0
- package/src/components/carousel/features/drag.ts +388 -0
- package/src/components/carousel/features/index.ts +8 -0
- package/src/components/carousel/features/slides.ts +682 -0
- package/src/components/carousel/index.ts +38 -0
- package/src/components/carousel/types.ts +327 -0
- package/src/components/dialog/_styles.scss +213 -0
- package/src/components/dialog/api.ts +283 -0
- package/src/components/dialog/config.ts +113 -0
- package/src/components/dialog/constants.ts +32 -0
- package/src/components/dialog/dialog.ts +56 -0
- package/src/components/dialog/features.ts +713 -0
- package/src/components/dialog/index.ts +15 -0
- package/src/components/dialog/types.ts +221 -0
- package/src/components/progress/_styles.scss +13 -1
- package/src/components/progress/api.ts +2 -2
- package/src/components/progress/progress.ts +2 -2
- package/src/components/progress/types.ts +3 -0
- package/src/components/radios/_styles.scss +232 -0
- package/src/components/radios/api.ts +100 -0
- package/src/components/radios/config.ts +60 -0
- package/src/components/radios/constants.ts +28 -0
- package/src/components/radios/index.ts +4 -0
- package/src/components/radios/radio.ts +269 -0
- package/src/components/radios/radios.ts +42 -0
- package/src/components/radios/types.ts +232 -0
- package/src/components/sheet/_styles.scss +236 -0
- package/src/components/sheet/api.ts +96 -0
- package/src/components/sheet/config.ts +66 -0
- package/src/components/sheet/constants.ts +20 -0
- package/src/components/sheet/features/content.ts +51 -0
- package/src/components/sheet/features/gestures.ts +177 -0
- package/src/components/sheet/features/index.ts +6 -0
- package/src/components/sheet/features/position.ts +42 -0
- package/src/components/sheet/features/state.ts +116 -0
- package/src/components/sheet/features/title.ts +86 -0
- package/src/components/sheet/index.ts +4 -0
- package/src/components/sheet/sheet.ts +57 -0
- package/src/components/sheet/types.ts +266 -0
- package/src/components/slider/_styles.scss +518 -0
- package/src/components/slider/api.ts +336 -0
- package/src/components/slider/config.ts +145 -0
- package/src/components/slider/constants.ts +28 -0
- package/src/components/slider/features/appearance.ts +140 -0
- package/src/components/slider/features/disabled.ts +43 -0
- package/src/components/slider/features/events.ts +164 -0
- package/src/components/slider/features/index.ts +5 -0
- package/src/components/slider/features/interactions.ts +256 -0
- package/src/components/slider/features/keyboard.ts +114 -0
- package/src/components/slider/features/slider.ts +336 -0
- package/src/components/slider/features/structure.ts +264 -0
- package/src/components/slider/features/ui.ts +518 -0
- package/src/components/slider/index.ts +9 -0
- package/src/components/slider/slider.ts +58 -0
- package/src/components/slider/types.ts +166 -0
- package/src/components/tabs/_styles.scss +224 -0
- package/src/components/tabs/api.ts +443 -0
- package/src/components/tabs/config.ts +80 -0
- package/src/components/tabs/constants.ts +12 -0
- package/src/components/tabs/index.ts +4 -0
- package/src/components/tabs/tabs.ts +52 -0
- package/src/components/tabs/types.ts +247 -0
- package/src/components/textfield/_styles.scss +97 -4
- package/src/components/tooltip/_styles.scss +241 -0
- package/src/components/tooltip/api.ts +411 -0
- package/src/components/tooltip/config.ts +78 -0
- package/src/components/tooltip/constants.ts +27 -0
- package/src/components/tooltip/index.ts +4 -0
- package/src/components/tooltip/tooltip.ts +60 -0
- package/src/components/tooltip/types.ts +178 -0
- package/src/core/build/_ripple.scss +79 -0
- package/src/core/build/constants.ts +48 -0
- package/src/core/build/icon.ts +137 -0
- package/src/core/build/ripple.ts +216 -0
- package/src/core/build/text.ts +91 -0
- package/src/index.ts +9 -1
- package/src/styles/abstract/_variables.scss +24 -12
- package/tsconfig.json +22 -0
- package/typedoc.json +28 -0
- package/typedoc.simple.json +14 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
// src/components/dialog/features.ts
|
|
2
|
+
import { getOverlayConfig } from './config';
|
|
3
|
+
import { DIALOG_SIZES, DIALOG_ANIMATIONS, DIALOG_FOOTER_ALIGNMENTS, DIALOG_EVENTS } from './constants';
|
|
4
|
+
import { DialogConfig, DialogButton, DialogEvent } from './types';
|
|
5
|
+
import createButton from '../button';
|
|
6
|
+
import { BUTTON_VARIANTS } from '../button/constants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates the dialog DOM structure
|
|
10
|
+
* @param config Dialog configuration
|
|
11
|
+
* @returns Component enhancer with DOM structure
|
|
12
|
+
*/
|
|
13
|
+
export const withStructure = (config: DialogConfig) => component => {
|
|
14
|
+
// Create the overlay element
|
|
15
|
+
const overlayConfig = getOverlayConfig(config);
|
|
16
|
+
const overlay = document.createElement(overlayConfig.tag);
|
|
17
|
+
|
|
18
|
+
// Add overlay classes
|
|
19
|
+
overlay.classList.add(component.getClass('dialog-overlay'));
|
|
20
|
+
|
|
21
|
+
// Set overlay attributes
|
|
22
|
+
Object.entries(overlayConfig.attrs || {}).forEach(([key, value]) => {
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
overlay.setAttribute(key, String(value));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Set custom z-index if provided
|
|
29
|
+
if (config.zIndex) {
|
|
30
|
+
overlay.style.zIndex = String(config.zIndex);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create internal structure
|
|
34
|
+
const createHeader = () => {
|
|
35
|
+
const header = document.createElement('div');
|
|
36
|
+
header.classList.add(component.getClass('dialog-header'));
|
|
37
|
+
|
|
38
|
+
const headerContent = document.createElement('div');
|
|
39
|
+
headerContent.classList.add(component.getClass('dialog-header-content'));
|
|
40
|
+
header.appendChild(headerContent);
|
|
41
|
+
|
|
42
|
+
if (config.title) {
|
|
43
|
+
const title = document.createElement('h2');
|
|
44
|
+
title.classList.add(component.getClass('dialog-header-title'));
|
|
45
|
+
title.textContent = config.title;
|
|
46
|
+
headerContent.appendChild(title);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.subtitle) {
|
|
50
|
+
const subtitle = document.createElement('p');
|
|
51
|
+
subtitle.classList.add(component.getClass('dialog-header-subtitle'));
|
|
52
|
+
subtitle.textContent = config.subtitle;
|
|
53
|
+
headerContent.appendChild(subtitle);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.closeButton !== false) {
|
|
57
|
+
const closeButton = document.createElement('button');
|
|
58
|
+
closeButton.classList.add(component.getClass('dialog-header-close'));
|
|
59
|
+
closeButton.setAttribute('aria-label', 'Close dialog');
|
|
60
|
+
closeButton.innerHTML = `
|
|
61
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
62
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
63
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
64
|
+
</svg>
|
|
65
|
+
`;
|
|
66
|
+
closeButton.addEventListener('click', () => {
|
|
67
|
+
component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
|
|
68
|
+
});
|
|
69
|
+
header.appendChild(closeButton);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return header;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const createContent = () => {
|
|
76
|
+
const content = document.createElement('div');
|
|
77
|
+
content.classList.add(component.getClass('dialog-content'));
|
|
78
|
+
|
|
79
|
+
if (config.content) {
|
|
80
|
+
content.innerHTML = config.content;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return content;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const createFooter = () => {
|
|
87
|
+
const footer = document.createElement('div');
|
|
88
|
+
footer.classList.add(component.getClass('dialog-footer'));
|
|
89
|
+
|
|
90
|
+
// Apply footer alignment
|
|
91
|
+
const alignment = config.footerAlignment || DIALOG_FOOTER_ALIGNMENTS.RIGHT;
|
|
92
|
+
if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
|
|
93
|
+
footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add buttons if provided
|
|
97
|
+
if (Array.isArray(config.buttons) && config.buttons.length > 0) {
|
|
98
|
+
config.buttons.forEach(buttonConfig => addButton(footer, buttonConfig, component));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return footer;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create the dialog structure
|
|
105
|
+
const header = createHeader();
|
|
106
|
+
const content = createContent();
|
|
107
|
+
const footer = Array.isArray(config.buttons) && config.buttons.length > 0 ? createFooter() : null;
|
|
108
|
+
|
|
109
|
+
// Add header, content, and footer to dialog
|
|
110
|
+
component.element.appendChild(header);
|
|
111
|
+
component.element.appendChild(content);
|
|
112
|
+
if (footer) {
|
|
113
|
+
component.element.appendChild(footer);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add dialog to overlay
|
|
117
|
+
overlay.appendChild(component.element);
|
|
118
|
+
|
|
119
|
+
// Add dialog classes
|
|
120
|
+
component.element.classList.add(component.getClass('dialog'));
|
|
121
|
+
|
|
122
|
+
// Apply size class
|
|
123
|
+
const size = config.size || DIALOG_SIZES.MEDIUM;
|
|
124
|
+
if (size !== DIALOG_SIZES.MEDIUM) {
|
|
125
|
+
component.element.classList.add(`${component.getClass('dialog')}--${size}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply animation class
|
|
129
|
+
const animation = config.animation || DIALOG_ANIMATIONS.SCALE;
|
|
130
|
+
if (animation !== DIALOG_ANIMATIONS.SCALE) {
|
|
131
|
+
component.element.classList.add(`${component.getClass('dialog')}--${animation}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add overlay to container or document.body
|
|
135
|
+
const container = config.container || document.body;
|
|
136
|
+
container.appendChild(overlay);
|
|
137
|
+
|
|
138
|
+
// Store elements in component
|
|
139
|
+
return {
|
|
140
|
+
...component,
|
|
141
|
+
overlay,
|
|
142
|
+
structure: {
|
|
143
|
+
header,
|
|
144
|
+
content,
|
|
145
|
+
footer,
|
|
146
|
+
container
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adds button to dialog footer
|
|
153
|
+
* @param footer Footer element
|
|
154
|
+
* @param buttonConfig Button configuration
|
|
155
|
+
* @param component Dialog component
|
|
156
|
+
*/
|
|
157
|
+
const addButton = (footer: HTMLElement, buttonConfig: DialogButton, component: any) => {
|
|
158
|
+
const {
|
|
159
|
+
text,
|
|
160
|
+
variant = BUTTON_VARIANTS.TEXT,
|
|
161
|
+
onClick,
|
|
162
|
+
closeDialog = true,
|
|
163
|
+
autofocus = false,
|
|
164
|
+
attrs = {}
|
|
165
|
+
} = buttonConfig;
|
|
166
|
+
|
|
167
|
+
const button = createButton({
|
|
168
|
+
text,
|
|
169
|
+
variant,
|
|
170
|
+
...attrs
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Add click handler
|
|
174
|
+
button.on('click', event => {
|
|
175
|
+
let shouldClose = closeDialog;
|
|
176
|
+
|
|
177
|
+
// Call onClick handler if provided
|
|
178
|
+
if (typeof onClick === 'function') {
|
|
179
|
+
const result = onClick(event, component);
|
|
180
|
+
// If onClick returns false, don't close the dialog
|
|
181
|
+
if (result === false) {
|
|
182
|
+
shouldClose = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Close dialog if needed
|
|
187
|
+
if (shouldClose) {
|
|
188
|
+
component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Set autofocus attribute if needed
|
|
193
|
+
if (autofocus) {
|
|
194
|
+
button.element.setAttribute('autofocus', 'true');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
footer.appendChild(button.element);
|
|
198
|
+
|
|
199
|
+
// Store button instance
|
|
200
|
+
if (!component._buttons) {
|
|
201
|
+
component._buttons = [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
component._buttons.push({
|
|
205
|
+
config: buttonConfig,
|
|
206
|
+
instance: button
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Add visibility control to dialog
|
|
212
|
+
* @returns Component enhancer with visibility features
|
|
213
|
+
*/
|
|
214
|
+
export const withVisibility = () => component => {
|
|
215
|
+
// Initial state
|
|
216
|
+
const isOpen = component.config.open === true;
|
|
217
|
+
|
|
218
|
+
// Setup animation duration
|
|
219
|
+
const animationDuration = component.config.animationDuration || 150;
|
|
220
|
+
|
|
221
|
+
// Helper functions to handle focus trap
|
|
222
|
+
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
223
|
+
let previouslyFocusedElement: HTMLElement | null = null;
|
|
224
|
+
|
|
225
|
+
const trapFocus = () => {
|
|
226
|
+
if (!component.config.trapFocus) return;
|
|
227
|
+
|
|
228
|
+
const focusableContent = component.element.querySelectorAll(focusableElements);
|
|
229
|
+
if (focusableContent.length === 0) return;
|
|
230
|
+
|
|
231
|
+
const firstFocusableElement = focusableContent[0] as HTMLElement;
|
|
232
|
+
const lastFocusableElement = focusableContent[focusableContent.length - 1] as HTMLElement;
|
|
233
|
+
|
|
234
|
+
// Focus the first element if autofocus is enabled
|
|
235
|
+
if (component.config.autofocus) {
|
|
236
|
+
// Check for a button with autofocus attribute
|
|
237
|
+
const autofocusElement = component.element.querySelector('[autofocus]') as HTMLElement;
|
|
238
|
+
if (autofocusElement) {
|
|
239
|
+
autofocusElement.focus();
|
|
240
|
+
} else {
|
|
241
|
+
firstFocusableElement.focus();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Set up the keyboard trap
|
|
246
|
+
component.element.addEventListener('keydown', handleKeyDown);
|
|
247
|
+
|
|
248
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
249
|
+
const isTabPressed = e.key === 'Tab';
|
|
250
|
+
|
|
251
|
+
if (!isTabPressed) return;
|
|
252
|
+
|
|
253
|
+
if (e.shiftKey) {
|
|
254
|
+
// If shift + tab pressed and focus is on first element, move to last
|
|
255
|
+
if (document.activeElement === firstFocusableElement) {
|
|
256
|
+
lastFocusableElement.focus();
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// If tab pressed and focus is on last element, move to first
|
|
261
|
+
if (document.activeElement === lastFocusableElement) {
|
|
262
|
+
firstFocusableElement.focus();
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const releaseFocus = () => {
|
|
270
|
+
if (!component.config.trapFocus) return;
|
|
271
|
+
|
|
272
|
+
component.element.removeEventListener('keydown', handleKeyDown);
|
|
273
|
+
|
|
274
|
+
// Restore focus to previously focused element
|
|
275
|
+
if (previouslyFocusedElement) {
|
|
276
|
+
previouslyFocusedElement.focus();
|
|
277
|
+
previouslyFocusedElement = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function handleKeyDown() {}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const setupEvents = () => {
|
|
284
|
+
// Handle overlay click
|
|
285
|
+
if (component.config.closeOnOverlayClick !== false) {
|
|
286
|
+
component.overlay.addEventListener('click', handleOverlayClick);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle Escape key
|
|
290
|
+
if (component.config.closeOnEscape !== false) {
|
|
291
|
+
document.addEventListener('keydown', handleEscKey);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const cleanupEvents = () => {
|
|
296
|
+
component.overlay.removeEventListener('click', handleOverlayClick);
|
|
297
|
+
document.removeEventListener('keydown', handleEscKey);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
function handleOverlayClick(e: MouseEvent) {
|
|
301
|
+
// Only close if the click was directly on the overlay
|
|
302
|
+
if (e.target === component.overlay) {
|
|
303
|
+
component.events.trigger(DIALOG_EVENTS.CLOSE, {
|
|
304
|
+
dialog: component,
|
|
305
|
+
originalEvent: e
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function handleEscKey(e: KeyboardEvent) {
|
|
311
|
+
if (e.key === 'Escape' && component.visibility.isOpen()) {
|
|
312
|
+
component.events.trigger(DIALOG_EVENTS.CLOSE, {
|
|
313
|
+
dialog: component,
|
|
314
|
+
originalEvent: e
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Setup initial state
|
|
320
|
+
if (isOpen) {
|
|
321
|
+
component.overlay.classList.add(`${component.getClass('dialog-overlay')}--visible`);
|
|
322
|
+
component.element.classList.add(`${component.getClass('dialog')}--visible`);
|
|
323
|
+
|
|
324
|
+
// Setup focus trap and events
|
|
325
|
+
trapFocus();
|
|
326
|
+
setupEvents();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
...component,
|
|
331
|
+
visibility: {
|
|
332
|
+
open() {
|
|
333
|
+
// Don't do anything if already open
|
|
334
|
+
if (this.isOpen()) return;
|
|
335
|
+
|
|
336
|
+
// Store the currently focused element
|
|
337
|
+
previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
338
|
+
|
|
339
|
+
// Trigger before open event
|
|
340
|
+
const beforeOpenEvent = { dialog: component, defaultPrevented: false, preventDefault: () => { beforeOpenEvent.defaultPrevented = true; } };
|
|
341
|
+
component.events.trigger(DIALOG_EVENTS.BEFORE_OPEN, beforeOpenEvent);
|
|
342
|
+
|
|
343
|
+
// If event was prevented, don't open
|
|
344
|
+
if (beforeOpenEvent.defaultPrevented) return;
|
|
345
|
+
|
|
346
|
+
// Show the overlay
|
|
347
|
+
component.overlay.classList.add(`${component.getClass('dialog-overlay')}--visible`);
|
|
348
|
+
|
|
349
|
+
// Show the dialog after a small delay to allow the overlay animation to start
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
component.element.classList.add(`${component.getClass('dialog')}--visible`);
|
|
352
|
+
|
|
353
|
+
// Setup focus trap and events
|
|
354
|
+
trapFocus();
|
|
355
|
+
setupEvents();
|
|
356
|
+
|
|
357
|
+
// Trigger after open event after animation completes
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
component.events.trigger(DIALOG_EVENTS.AFTER_OPEN, { dialog: component });
|
|
360
|
+
}, animationDuration);
|
|
361
|
+
|
|
362
|
+
// Trigger open event
|
|
363
|
+
component.events.trigger(DIALOG_EVENTS.OPEN, { dialog: component });
|
|
364
|
+
}, 10);
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
close() {
|
|
368
|
+
// Don't do anything if already closed
|
|
369
|
+
if (!this.isOpen()) return;
|
|
370
|
+
|
|
371
|
+
// Trigger before close event
|
|
372
|
+
const beforeCloseEvent = { dialog: component, defaultPrevented: false, preventDefault: () => { beforeCloseEvent.defaultPrevented = true; } };
|
|
373
|
+
component.events.trigger(DIALOG_EVENTS.BEFORE_CLOSE, beforeCloseEvent);
|
|
374
|
+
|
|
375
|
+
// If event was prevented, don't close
|
|
376
|
+
if (beforeCloseEvent.defaultPrevented) return;
|
|
377
|
+
|
|
378
|
+
// Hide the dialog
|
|
379
|
+
component.element.classList.remove(`${component.getClass('dialog')}--visible`);
|
|
380
|
+
|
|
381
|
+
// Release focus trap and cleanup events
|
|
382
|
+
releaseFocus();
|
|
383
|
+
cleanupEvents();
|
|
384
|
+
|
|
385
|
+
// Trigger close event
|
|
386
|
+
component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
|
|
387
|
+
|
|
388
|
+
// Hide the overlay after animation completes
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
component.overlay.classList.remove(`${component.getClass('dialog-overlay')}--visible`);
|
|
391
|
+
|
|
392
|
+
// Trigger after close event
|
|
393
|
+
component.events.trigger(DIALOG_EVENTS.AFTER_CLOSE, { dialog: component });
|
|
394
|
+
}, animationDuration);
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
toggle(open?: boolean) {
|
|
398
|
+
if (open === undefined) {
|
|
399
|
+
// Toggle based on current state
|
|
400
|
+
if (this.isOpen()) {
|
|
401
|
+
this.close();
|
|
402
|
+
} else {
|
|
403
|
+
this.open();
|
|
404
|
+
}
|
|
405
|
+
} else if (open) {
|
|
406
|
+
this.open();
|
|
407
|
+
} else {
|
|
408
|
+
this.close();
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
isOpen() {
|
|
413
|
+
return component.element.classList.contains(`${component.getClass('dialog')}--visible`);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
focus: {
|
|
418
|
+
trapFocus,
|
|
419
|
+
releaseFocus
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Adds content management features to dialog
|
|
426
|
+
* @returns Component enhancer with content features
|
|
427
|
+
*/
|
|
428
|
+
export const withContent = () => component => {
|
|
429
|
+
const headerElement = component.structure.header;
|
|
430
|
+
const contentElement = component.structure.content;
|
|
431
|
+
const footerElement = component.structure.footer;
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
...component,
|
|
435
|
+
content: {
|
|
436
|
+
/**
|
|
437
|
+
* Sets dialog title
|
|
438
|
+
* @param title Title text
|
|
439
|
+
*/
|
|
440
|
+
setTitle(title: string) {
|
|
441
|
+
let titleElement = headerElement.querySelector(`.${component.getClass('dialog-header-title')}`);
|
|
442
|
+
|
|
443
|
+
if (!titleElement && title) {
|
|
444
|
+
// Create title element if it doesn't exist
|
|
445
|
+
titleElement = document.createElement('h2');
|
|
446
|
+
titleElement.classList.add(component.getClass('dialog-header-title'));
|
|
447
|
+
headerElement.querySelector(`.${component.getClass('dialog-header-content')}`).appendChild(titleElement);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (titleElement) {
|
|
451
|
+
titleElement.textContent = title;
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Gets dialog title
|
|
457
|
+
* @returns Title text
|
|
458
|
+
*/
|
|
459
|
+
getTitle() {
|
|
460
|
+
const titleElement = headerElement.querySelector(`.${component.getClass('dialog-header-title')}`);
|
|
461
|
+
return titleElement ? titleElement.textContent || '' : '';
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Sets dialog subtitle
|
|
466
|
+
* @param subtitle Subtitle text
|
|
467
|
+
*/
|
|
468
|
+
setSubtitle(subtitle: string) {
|
|
469
|
+
let subtitleElement = headerElement.querySelector(`.${component.getClass('dialog-header-subtitle')}`);
|
|
470
|
+
|
|
471
|
+
if (!subtitleElement && subtitle) {
|
|
472
|
+
// Create subtitle element if it doesn't exist
|
|
473
|
+
subtitleElement = document.createElement('p');
|
|
474
|
+
subtitleElement.classList.add(component.getClass('dialog-header-subtitle'));
|
|
475
|
+
headerElement.querySelector(`.${component.getClass('dialog-header-content')}`).appendChild(subtitleElement);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (subtitleElement) {
|
|
479
|
+
subtitleElement.textContent = subtitle;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Gets dialog subtitle
|
|
485
|
+
* @returns Subtitle text
|
|
486
|
+
*/
|
|
487
|
+
getSubtitle() {
|
|
488
|
+
const subtitleElement = headerElement.querySelector(`.${component.getClass('dialog-header-subtitle')}`);
|
|
489
|
+
return subtitleElement ? subtitleElement.textContent || '' : '';
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Sets dialog content
|
|
494
|
+
* @param content Content HTML
|
|
495
|
+
*/
|
|
496
|
+
setContent(content: string) {
|
|
497
|
+
contentElement.innerHTML = content;
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Gets dialog content
|
|
502
|
+
* @returns Content HTML
|
|
503
|
+
*/
|
|
504
|
+
getContent() {
|
|
505
|
+
return contentElement.innerHTML;
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Gets dialog header element
|
|
510
|
+
* @returns Header element
|
|
511
|
+
*/
|
|
512
|
+
getHeaderElement() {
|
|
513
|
+
return headerElement;
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Gets dialog content element
|
|
518
|
+
* @returns Content element
|
|
519
|
+
*/
|
|
520
|
+
getContentElement() {
|
|
521
|
+
return contentElement;
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Gets dialog footer element
|
|
526
|
+
* @returns Footer element
|
|
527
|
+
*/
|
|
528
|
+
getFooterElement() {
|
|
529
|
+
return footerElement;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Adds button management features to dialog
|
|
537
|
+
* @returns Component enhancer with button features
|
|
538
|
+
*/
|
|
539
|
+
export const withButtons = () => component => {
|
|
540
|
+
// Initialize buttons array if not already done
|
|
541
|
+
if (!component._buttons) {
|
|
542
|
+
component._buttons = [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
...component,
|
|
547
|
+
buttons: {
|
|
548
|
+
/**
|
|
549
|
+
* Adds a button to the dialog footer
|
|
550
|
+
* @param button Button configuration
|
|
551
|
+
*/
|
|
552
|
+
addButton(button: DialogButton) {
|
|
553
|
+
// Create footer if it doesn't exist
|
|
554
|
+
let footer = component.structure.footer;
|
|
555
|
+
|
|
556
|
+
if (!footer) {
|
|
557
|
+
footer = document.createElement('div');
|
|
558
|
+
footer.classList.add(component.getClass('dialog-footer'));
|
|
559
|
+
|
|
560
|
+
// Apply footer alignment
|
|
561
|
+
const alignment = component.config.footerAlignment || DIALOG_FOOTER_ALIGNMENTS.RIGHT;
|
|
562
|
+
if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
|
|
563
|
+
footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
component.element.appendChild(footer);
|
|
567
|
+
component.structure.footer = footer;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Add the button
|
|
571
|
+
addButton(footer, button, component);
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Removes a button by index or text
|
|
576
|
+
* @param indexOrText Button index or text
|
|
577
|
+
*/
|
|
578
|
+
removeButton(indexOrText: number | string) {
|
|
579
|
+
if (typeof indexOrText === 'number') {
|
|
580
|
+
// Remove by index
|
|
581
|
+
if (indexOrText >= 0 && indexOrText < component._buttons.length) {
|
|
582
|
+
const button = component._buttons[indexOrText];
|
|
583
|
+
button.instance.destroy();
|
|
584
|
+
component._buttons.splice(indexOrText, 1);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// Remove by text
|
|
588
|
+
const index = component._buttons.findIndex(button =>
|
|
589
|
+
button.config.text === indexOrText);
|
|
590
|
+
|
|
591
|
+
if (index !== -1) {
|
|
592
|
+
const button = component._buttons[index];
|
|
593
|
+
button.instance.destroy();
|
|
594
|
+
component._buttons.splice(index, 1);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// If no buttons left, remove footer
|
|
599
|
+
if (component._buttons.length === 0 && component.structure.footer) {
|
|
600
|
+
component.element.removeChild(component.structure.footer);
|
|
601
|
+
component.structure.footer = null;
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Gets all footer buttons
|
|
607
|
+
* @returns Array of button configurations
|
|
608
|
+
*/
|
|
609
|
+
getButtons() {
|
|
610
|
+
return component._buttons.map(button => button.config);
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Sets footer alignment
|
|
615
|
+
* @param alignment Footer alignment
|
|
616
|
+
*/
|
|
617
|
+
setFooterAlignment(alignment: keyof typeof DIALOG_FOOTER_ALIGNMENTS | DIALOG_FOOTER_ALIGNMENTS) {
|
|
618
|
+
if (!component.structure.footer) return;
|
|
619
|
+
|
|
620
|
+
// Remove existing alignment classes
|
|
621
|
+
Object.values(DIALOG_FOOTER_ALIGNMENTS).forEach(align => {
|
|
622
|
+
if (align !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
|
|
623
|
+
component.structure.footer.classList.remove(`${component.getClass('dialog-footer')}--${align}`);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Add new alignment class if not right (default)
|
|
628
|
+
if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
|
|
629
|
+
component.structure.footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Adds size management features to dialog
|
|
638
|
+
* @returns Component enhancer with size features
|
|
639
|
+
*/
|
|
640
|
+
export const withSize = () => component => {
|
|
641
|
+
return {
|
|
642
|
+
...component,
|
|
643
|
+
size: {
|
|
644
|
+
/**
|
|
645
|
+
* Sets dialog size
|
|
646
|
+
* @param size Size variant
|
|
647
|
+
*/
|
|
648
|
+
setSize(size: keyof typeof DIALOG_SIZES | DIALOG_SIZES) {
|
|
649
|
+
// Remove existing size classes
|
|
650
|
+
Object.values(DIALOG_SIZES).forEach(sizeValue => {
|
|
651
|
+
component.element.classList.remove(`${component.getClass('dialog')}--${sizeValue}`);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Add new size class if not medium (default)
|
|
655
|
+
if (size !== DIALOG_SIZES.MEDIUM) {
|
|
656
|
+
component.element.classList.add(`${component.getClass('dialog')}--${size}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Adds confirmation dialog features
|
|
665
|
+
* @returns Component enhancer with confirm feature
|
|
666
|
+
*/
|
|
667
|
+
export const withConfirm = () => component => {
|
|
668
|
+
return {
|
|
669
|
+
...component,
|
|
670
|
+
confirm(options) {
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
const {
|
|
673
|
+
title = 'Confirm',
|
|
674
|
+
message,
|
|
675
|
+
confirmText = 'Yes',
|
|
676
|
+
cancelText = 'No',
|
|
677
|
+
confirmVariant = BUTTON_VARIANTS.FILLED,
|
|
678
|
+
cancelVariant = BUTTON_VARIANTS.TEXT,
|
|
679
|
+
size = DIALOG_SIZES.SMALL
|
|
680
|
+
} = options;
|
|
681
|
+
|
|
682
|
+
// Set dialog properties
|
|
683
|
+
component.content.setTitle(title);
|
|
684
|
+
component.content.setContent(`<p>${message}</p>`);
|
|
685
|
+
component.size.setSize(size);
|
|
686
|
+
|
|
687
|
+
// Clear existing buttons
|
|
688
|
+
component._buttons.forEach(button => button.instance.destroy());
|
|
689
|
+
component._buttons = [];
|
|
690
|
+
|
|
691
|
+
// Add confirm and cancel buttons
|
|
692
|
+
component.buttons.addButton({
|
|
693
|
+
text: confirmText,
|
|
694
|
+
variant: confirmVariant,
|
|
695
|
+
onClick: () => {
|
|
696
|
+
resolve(true);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
component.buttons.addButton({
|
|
701
|
+
text: cancelText,
|
|
702
|
+
variant: cancelVariant,
|
|
703
|
+
onClick: () => {
|
|
704
|
+
resolve(false);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Open the dialog
|
|
709
|
+
component.visibility.open();
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
};
|