juxscript 1.1.60 → 1.1.63
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/lib/components/modal.d.ts +65 -21
- package/lib/components/modal.d.ts.map +1 -1
- package/lib/components/modal.js +291 -79
- package/lib/components/modal.ts +350 -88
- package/package.json +1 -1
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
export type EmojiIconType = {
|
|
3
|
+
icon: string;
|
|
4
|
+
collection?: 'lucide' | 'heroicons' | 'material-symbols' | 'material' | 'icon-park' | string;
|
|
5
|
+
};
|
|
2
6
|
export interface ModalOptions {
|
|
3
7
|
title?: string;
|
|
4
|
-
content?: string
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
content?: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
|
|
9
|
+
icon?: EmojiIconType | undefined;
|
|
10
|
+
close?: boolean;
|
|
11
|
+
backdropClose?: boolean;
|
|
12
|
+
position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
|
|
7
13
|
size?: 'small' | 'medium' | 'large';
|
|
14
|
+
actions?: Array<{
|
|
15
|
+
label: string;
|
|
16
|
+
variant?: string;
|
|
17
|
+
click?: () => void;
|
|
18
|
+
} | Array<BaseComponent<any>>>;
|
|
8
19
|
style?: string;
|
|
9
20
|
class?: string;
|
|
10
21
|
}
|
|
11
22
|
type ModalState = {
|
|
12
23
|
title: string;
|
|
13
|
-
content: string
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
content: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
|
|
25
|
+
icon: EmojiIconType | undefined;
|
|
26
|
+
close: boolean;
|
|
27
|
+
backdropClose: boolean;
|
|
28
|
+
position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
|
|
16
29
|
size: string;
|
|
17
30
|
open: boolean;
|
|
31
|
+
actions: Array<{
|
|
32
|
+
label: string;
|
|
33
|
+
variant?: string;
|
|
34
|
+
click?: () => void;
|
|
35
|
+
} | Array<BaseComponent<any>>>;
|
|
18
36
|
style: string;
|
|
19
37
|
class: string;
|
|
20
38
|
};
|
|
@@ -23,29 +41,55 @@ export declare class Modal extends BaseComponent<ModalState> {
|
|
|
23
41
|
constructor(id: string, options?: ModalOptions);
|
|
24
42
|
protected getTriggerEvents(): readonly string[];
|
|
25
43
|
protected getCallbackEvents(): readonly string[];
|
|
44
|
+
update(prop: string, value: any): void;
|
|
45
|
+
private _updateTitle;
|
|
46
|
+
private _updateIcon;
|
|
47
|
+
private _updateContent;
|
|
48
|
+
private _rebuildActions;
|
|
26
49
|
title(value: string): this;
|
|
27
|
-
content(value: string): this;
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
|
|
51
|
+
icon(value: EmojiIconType): this;
|
|
52
|
+
close(value: boolean): this;
|
|
53
|
+
backdropClose(value: boolean): this;
|
|
30
54
|
size(value: 'small' | 'medium' | 'large'): this;
|
|
55
|
+
actions(value: Array<{
|
|
56
|
+
label: string;
|
|
57
|
+
variant?: string;
|
|
58
|
+
click?: () => void;
|
|
59
|
+
}>): this;
|
|
60
|
+
addAction(action: {
|
|
61
|
+
label: string;
|
|
62
|
+
variant?: string;
|
|
63
|
+
click?: () => void;
|
|
64
|
+
}): this;
|
|
31
65
|
open(): this;
|
|
32
|
-
|
|
66
|
+
closeModal(): this;
|
|
67
|
+
toggle(): this;
|
|
68
|
+
/**
|
|
69
|
+
* Check if modal is currently open
|
|
70
|
+
*/
|
|
71
|
+
isOpen(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Check if modal is currently closed
|
|
74
|
+
*/
|
|
75
|
+
isClosed(): boolean;
|
|
33
76
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @returns The ID of the modal body element (e.g., 'mymodal-body')
|
|
36
|
-
*
|
|
37
|
-
* Usage:
|
|
38
|
-
* const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
|
|
39
|
-
* jux.table('files-table', {...}).render(modal.bodyId());
|
|
77
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
40
78
|
*/
|
|
41
|
-
|
|
79
|
+
addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
|
|
42
80
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @returns The modal body element or null if not rendered
|
|
81
|
+
* Clear all content from modal
|
|
45
82
|
*/
|
|
46
|
-
|
|
83
|
+
clearContent(): this;
|
|
84
|
+
/**
|
|
85
|
+
* Get the modal content element ID for rendering child components
|
|
86
|
+
*/
|
|
87
|
+
contentId(): string;
|
|
88
|
+
/**
|
|
89
|
+
* Get the modal content element (only available after render)
|
|
90
|
+
*/
|
|
91
|
+
getContentElement(): HTMLElement | null;
|
|
47
92
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
48
|
-
update(prop: string, value: any): void;
|
|
49
93
|
}
|
|
50
94
|
export declare function modal(id: string, options?: ModalOptions): Modal;
|
|
51
95
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.ts"],"names":[],"mappings":"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"}
|
package/lib/components/modal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
2
3
|
// Event definitions
|
|
3
4
|
const TRIGGER_EVENTS = [];
|
|
4
5
|
const CALLBACK_EVENTS = ['open', 'close'];
|
|
@@ -7,10 +8,13 @@ export class Modal extends BaseComponent {
|
|
|
7
8
|
super(id, {
|
|
8
9
|
title: options.title ?? '',
|
|
9
10
|
content: options.content ?? '',
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
icon: options.icon ?? undefined,
|
|
12
|
+
close: options.close ?? true,
|
|
13
|
+
backdropClose: options.backdropClose ?? true,
|
|
14
|
+
position: options.position,
|
|
12
15
|
size: options.size ?? 'medium',
|
|
13
16
|
open: false,
|
|
17
|
+
actions: options.actions ?? [],
|
|
14
18
|
style: options.style ?? '',
|
|
15
19
|
class: options.class ?? ''
|
|
16
20
|
});
|
|
@@ -23,95 +27,255 @@ export class Modal extends BaseComponent {
|
|
|
23
27
|
return CALLBACK_EVENTS;
|
|
24
28
|
}
|
|
25
29
|
/* ═════════════════════════════════════════════════════════════════
|
|
26
|
-
*
|
|
30
|
+
* REACTIVE UPDATE (Precision DOM edits)
|
|
27
31
|
* ═════════════════════════════════════════════════════════════════ */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
update(prop, value) {
|
|
33
|
+
super.update(prop, value);
|
|
34
|
+
if (!this._overlay)
|
|
35
|
+
return;
|
|
36
|
+
const modal = this._overlay.querySelector('.jux-modal');
|
|
37
|
+
if (!modal)
|
|
38
|
+
return;
|
|
39
|
+
switch (prop) {
|
|
40
|
+
case 'open':
|
|
41
|
+
this._overlay.style.display = value ? 'flex' : 'none';
|
|
42
|
+
if (value) {
|
|
43
|
+
this._triggerCallback('open');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this._triggerCallback('close');
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case 'title':
|
|
50
|
+
this._updateTitle(value);
|
|
51
|
+
break;
|
|
52
|
+
case 'content':
|
|
53
|
+
this._updateContent(value);
|
|
54
|
+
break;
|
|
55
|
+
case 'icon':
|
|
56
|
+
this._updateIcon(value);
|
|
57
|
+
break;
|
|
58
|
+
case 'size':
|
|
59
|
+
modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
|
|
60
|
+
break;
|
|
61
|
+
case 'actions':
|
|
62
|
+
this._rebuildActions();
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_updateTitle(value) {
|
|
67
|
+
let headerEl = this._overlay.querySelector('.jux-modal-header');
|
|
68
|
+
if (!headerEl && value) {
|
|
69
|
+
// Create header if it doesn't exist
|
|
70
|
+
headerEl = document.createElement('div');
|
|
71
|
+
headerEl.className = 'jux-modal-header';
|
|
37
72
|
const modal = this._overlay.querySelector('.jux-modal');
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
const body = modal.querySelector('.jux-modal-body');
|
|
74
|
+
modal.insertBefore(headerEl, body);
|
|
75
|
+
}
|
|
76
|
+
if (headerEl) {
|
|
77
|
+
// Preserve icon if exists
|
|
78
|
+
const iconEl = headerEl.querySelector('.jux-modal-header-icon');
|
|
79
|
+
headerEl.innerHTML = '';
|
|
80
|
+
if (iconEl) {
|
|
81
|
+
headerEl.appendChild(iconEl);
|
|
82
|
+
}
|
|
83
|
+
const titleEl = document.createElement('span');
|
|
84
|
+
titleEl.className = 'jux-modal-header-title';
|
|
85
|
+
titleEl.textContent = value;
|
|
86
|
+
headerEl.appendChild(titleEl);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
_updateIcon(value) {
|
|
90
|
+
const headerEl = this._overlay.querySelector('.jux-modal-header');
|
|
91
|
+
if (!headerEl)
|
|
92
|
+
return;
|
|
93
|
+
let iconEl = headerEl.querySelector('.jux-modal-header-icon');
|
|
94
|
+
if (value) {
|
|
95
|
+
if (!iconEl) {
|
|
96
|
+
iconEl = document.createElement('span');
|
|
97
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
98
|
+
headerEl.insertBefore(iconEl, headerEl.firstChild);
|
|
41
99
|
}
|
|
100
|
+
iconEl.innerHTML = '';
|
|
101
|
+
iconEl.appendChild(renderIcon(value.icon, value.collection));
|
|
102
|
+
}
|
|
103
|
+
else if (iconEl) {
|
|
104
|
+
iconEl.remove();
|
|
42
105
|
}
|
|
106
|
+
}
|
|
107
|
+
_updateContent(value) {
|
|
108
|
+
const contentEl = this._overlay.querySelector('.jux-modal-content');
|
|
109
|
+
if (!contentEl)
|
|
110
|
+
return;
|
|
111
|
+
contentEl.innerHTML = '';
|
|
112
|
+
const items = Array.isArray(value) ? value : [value];
|
|
113
|
+
items.forEach(item => {
|
|
114
|
+
if (typeof item === 'string') {
|
|
115
|
+
const tempDiv = document.createElement('div');
|
|
116
|
+
tempDiv.innerHTML = item;
|
|
117
|
+
while (tempDiv.firstChild) {
|
|
118
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (item instanceof BaseComponent) {
|
|
122
|
+
item.render(contentEl);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
_rebuildActions() {
|
|
127
|
+
const modal = this._overlay.querySelector('.jux-modal');
|
|
128
|
+
if (!modal)
|
|
129
|
+
return;
|
|
130
|
+
let footerEl = modal.querySelector('.jux-modal-footer');
|
|
131
|
+
if (this.state.actions.length === 0) {
|
|
132
|
+
if (footerEl)
|
|
133
|
+
footerEl.remove();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!footerEl) {
|
|
137
|
+
footerEl = document.createElement('div');
|
|
138
|
+
footerEl.className = 'jux-modal-footer';
|
|
139
|
+
modal.appendChild(footerEl);
|
|
140
|
+
}
|
|
141
|
+
footerEl.innerHTML = '';
|
|
142
|
+
this.state.actions.forEach(action => {
|
|
143
|
+
// Check if it's an array of BaseComponents
|
|
144
|
+
if (Array.isArray(action)) {
|
|
145
|
+
action.forEach(component => {
|
|
146
|
+
if (component instanceof BaseComponent) {
|
|
147
|
+
component.render(footerEl);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Otherwise it's an action object with label property
|
|
152
|
+
else if ('label' in action) {
|
|
153
|
+
const btn = document.createElement('button');
|
|
154
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
155
|
+
btn.textContent = action.label;
|
|
156
|
+
btn.type = 'button';
|
|
157
|
+
btn.addEventListener('click', () => {
|
|
158
|
+
if (action.click)
|
|
159
|
+
action.click();
|
|
160
|
+
});
|
|
161
|
+
footerEl.appendChild(btn);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
166
|
+
* FLUENT API (Just set state, update() handles DOM)
|
|
167
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
168
|
+
title(value) {
|
|
169
|
+
this.state.title = value;
|
|
43
170
|
return this;
|
|
44
171
|
}
|
|
45
172
|
content(value) {
|
|
46
173
|
this.state.content = value;
|
|
47
|
-
if (this._overlay) {
|
|
48
|
-
const body = this._overlay.querySelector('.jux-modal-body');
|
|
49
|
-
if (body) {
|
|
50
|
-
body.innerHTML = value;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
174
|
return this;
|
|
54
175
|
}
|
|
55
|
-
|
|
56
|
-
this.state.
|
|
176
|
+
icon(value) {
|
|
177
|
+
this.state.icon = value;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
close(value) {
|
|
181
|
+
this.state.close = value;
|
|
57
182
|
return this;
|
|
58
183
|
}
|
|
59
|
-
|
|
60
|
-
this.state.
|
|
184
|
+
backdropClose(value) {
|
|
185
|
+
this.state.backdropClose = value;
|
|
61
186
|
return this;
|
|
62
187
|
}
|
|
63
188
|
size(value) {
|
|
64
189
|
this.state.size = value;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
actions(value) {
|
|
193
|
+
this.state.actions = value;
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
addAction(action) {
|
|
197
|
+
this.state.actions = [...this.state.actions, action];
|
|
71
198
|
return this;
|
|
72
199
|
}
|
|
73
200
|
open() {
|
|
74
201
|
this.state.open = true;
|
|
75
|
-
if (this._overlay) {
|
|
76
|
-
this._overlay.style.display = 'flex';
|
|
77
|
-
}
|
|
78
|
-
// 🎯 Fire the open callback event
|
|
79
|
-
this._triggerCallback('open');
|
|
80
202
|
return this;
|
|
81
203
|
}
|
|
82
|
-
|
|
204
|
+
closeModal() {
|
|
83
205
|
this.state.open = false;
|
|
84
|
-
|
|
85
|
-
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
toggle() {
|
|
209
|
+
this.state.open = !this.state.open;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Check if modal is currently open
|
|
214
|
+
*/
|
|
215
|
+
isOpen() {
|
|
216
|
+
return this.state.open;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check if modal is currently closed
|
|
220
|
+
*/
|
|
221
|
+
isClosed() {
|
|
222
|
+
return !this.state.open;
|
|
223
|
+
}
|
|
224
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
225
|
+
* DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
|
|
226
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
227
|
+
/**
|
|
228
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
229
|
+
*/
|
|
230
|
+
addContent(content) {
|
|
231
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
232
|
+
if (!contentEl) {
|
|
233
|
+
console.warn('[Modal] Content element not found');
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
const items = Array.isArray(content) ? content : [content];
|
|
237
|
+
items.forEach(item => {
|
|
238
|
+
if (typeof item === 'string') {
|
|
239
|
+
const tempDiv = document.createElement('div');
|
|
240
|
+
tempDiv.innerHTML = item;
|
|
241
|
+
while (tempDiv.firstChild) {
|
|
242
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (item instanceof BaseComponent) {
|
|
246
|
+
item.render(contentEl);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Clear all content from modal
|
|
253
|
+
*/
|
|
254
|
+
clearContent() {
|
|
255
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
256
|
+
if (contentEl) {
|
|
257
|
+
contentEl.innerHTML = '';
|
|
86
258
|
}
|
|
87
|
-
// 🎯 Fire the close callback event
|
|
88
|
-
this._triggerCallback('close');
|
|
89
259
|
return this;
|
|
90
260
|
}
|
|
91
261
|
/**
|
|
92
|
-
* Get the modal
|
|
93
|
-
* @returns The ID of the modal body element (e.g., 'mymodal-body')
|
|
94
|
-
*
|
|
95
|
-
* Usage:
|
|
96
|
-
* const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
|
|
97
|
-
* jux.table('files-table', {...}).render(modal.bodyId());
|
|
262
|
+
* Get the modal content element ID for rendering child components
|
|
98
263
|
*/
|
|
99
|
-
|
|
100
|
-
return `${this._id}-
|
|
264
|
+
contentId() {
|
|
265
|
+
return `${this._id}-content`;
|
|
101
266
|
}
|
|
102
267
|
/**
|
|
103
|
-
* Get the modal
|
|
104
|
-
* @returns The modal body element or null if not rendered
|
|
268
|
+
* Get the modal content element (only available after render)
|
|
105
269
|
*/
|
|
106
|
-
|
|
107
|
-
return document.getElementById(this.
|
|
270
|
+
getContentElement() {
|
|
271
|
+
return document.getElementById(this.contentId());
|
|
108
272
|
}
|
|
109
273
|
/* ═════════════════════════════════════════════════════════════════
|
|
110
274
|
* RENDER
|
|
111
275
|
* ═════════════════════════════════════════════════════════════════ */
|
|
112
276
|
render(targetId) {
|
|
113
277
|
const container = this._setupContainer(targetId);
|
|
114
|
-
const { open, title, content, size,
|
|
278
|
+
const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
|
|
115
279
|
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
116
280
|
const overlay = document.createElement('div');
|
|
117
281
|
overlay.className = 'jux-modal-overlay';
|
|
@@ -124,42 +288,96 @@ export class Modal extends BaseComponent {
|
|
|
124
288
|
this._overlay = overlay;
|
|
125
289
|
const modal = document.createElement('div');
|
|
126
290
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
127
|
-
if (
|
|
291
|
+
if (close) {
|
|
128
292
|
const closeButton = document.createElement('button');
|
|
129
293
|
closeButton.className = 'jux-modal-close';
|
|
130
294
|
closeButton.innerHTML = '×';
|
|
295
|
+
closeButton.type = 'button';
|
|
131
296
|
modal.appendChild(closeButton);
|
|
132
297
|
}
|
|
133
|
-
if (title) {
|
|
298
|
+
if (title || icon) {
|
|
134
299
|
const header = document.createElement('div');
|
|
135
300
|
header.className = 'jux-modal-header';
|
|
136
|
-
|
|
301
|
+
if (icon) {
|
|
302
|
+
const iconEl = document.createElement('span');
|
|
303
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
304
|
+
iconEl.appendChild(renderIcon(icon.icon, icon.collection));
|
|
305
|
+
header.appendChild(iconEl);
|
|
306
|
+
}
|
|
307
|
+
if (title) {
|
|
308
|
+
const titleEl = document.createElement('span');
|
|
309
|
+
titleEl.className = 'jux-modal-header-title';
|
|
310
|
+
titleEl.textContent = title;
|
|
311
|
+
header.appendChild(titleEl);
|
|
312
|
+
}
|
|
137
313
|
modal.appendChild(header);
|
|
138
314
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
315
|
+
const contentElement = document.createElement('div');
|
|
316
|
+
contentElement.className = 'jux-modal-content';
|
|
317
|
+
contentElement.id = this.contentId();
|
|
318
|
+
modal.appendChild(contentElement);
|
|
319
|
+
// Render initial content
|
|
320
|
+
if (content) {
|
|
321
|
+
const items = Array.isArray(content) ? content : [content];
|
|
322
|
+
items.forEach(item => {
|
|
323
|
+
if (typeof item === 'string') {
|
|
324
|
+
const tempDiv = document.createElement('div');
|
|
325
|
+
tempDiv.innerHTML = item;
|
|
326
|
+
while (tempDiv.firstChild) {
|
|
327
|
+
contentElement.appendChild(tempDiv.firstChild);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (item instanceof BaseComponent) {
|
|
331
|
+
item.render(contentElement);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (actions.length > 0) {
|
|
336
|
+
const footer = document.createElement('div');
|
|
337
|
+
footer.className = 'jux-modal-footer';
|
|
338
|
+
actions.forEach(action => {
|
|
339
|
+
// Check if it's an array of BaseComponents
|
|
340
|
+
if (Array.isArray(action)) {
|
|
341
|
+
action.forEach(component => {
|
|
342
|
+
if (component instanceof BaseComponent) {
|
|
343
|
+
component.render(footer);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Otherwise it's an action object with label property
|
|
348
|
+
else if ('label' in action) {
|
|
349
|
+
const btn = document.createElement('button');
|
|
350
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
351
|
+
btn.textContent = action.label;
|
|
352
|
+
btn.type = 'button';
|
|
353
|
+
btn.addEventListener('click', () => {
|
|
354
|
+
if (action.click)
|
|
355
|
+
action.click();
|
|
356
|
+
});
|
|
357
|
+
footer.appendChild(btn);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
modal.appendChild(footer);
|
|
361
|
+
}
|
|
144
362
|
overlay.appendChild(modal);
|
|
363
|
+
// Default dismiss behavior (only if NOT using sync)
|
|
145
364
|
if (!hasOpenSync) {
|
|
146
|
-
if (
|
|
365
|
+
if (close) {
|
|
147
366
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
148
367
|
closeButton?.addEventListener('click', () => {
|
|
149
368
|
this.state.open = false;
|
|
150
|
-
overlay.style.display = 'none';
|
|
151
369
|
});
|
|
152
370
|
}
|
|
153
|
-
if (
|
|
371
|
+
if (backdropClose) {
|
|
154
372
|
overlay.addEventListener('click', (e) => {
|
|
155
373
|
if (e.target === overlay) {
|
|
156
374
|
this.state.open = false;
|
|
157
|
-
overlay.style.display = 'none';
|
|
158
375
|
}
|
|
159
376
|
});
|
|
160
377
|
}
|
|
161
378
|
}
|
|
162
379
|
this._wireStandardEvents(overlay);
|
|
380
|
+
// Wire sync bindings
|
|
163
381
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
164
382
|
if (property === 'open') {
|
|
165
383
|
const transformToState = toState || ((v) => Boolean(v));
|
|
@@ -170,28 +388,25 @@ export class Modal extends BaseComponent {
|
|
|
170
388
|
return;
|
|
171
389
|
const transformed = transformToComponent(val);
|
|
172
390
|
this.state.open = transformed;
|
|
173
|
-
overlay.style.display = transformed ? 'flex' : 'none';
|
|
174
391
|
});
|
|
175
|
-
if (
|
|
392
|
+
if (close) {
|
|
176
393
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
177
394
|
closeButton?.addEventListener('click', () => {
|
|
178
395
|
if (isUpdating)
|
|
179
396
|
return;
|
|
180
397
|
isUpdating = true;
|
|
181
398
|
this.state.open = false;
|
|
182
|
-
overlay.style.display = 'none';
|
|
183
399
|
stateObj.set(transformToState(false));
|
|
184
400
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
185
401
|
});
|
|
186
402
|
}
|
|
187
|
-
if (
|
|
403
|
+
if (backdropClose) {
|
|
188
404
|
overlay.addEventListener('click', (e) => {
|
|
189
405
|
if (e.target === overlay) {
|
|
190
406
|
if (isUpdating)
|
|
191
407
|
return;
|
|
192
408
|
isUpdating = true;
|
|
193
409
|
this.state.open = false;
|
|
194
|
-
overlay.style.display = 'none';
|
|
195
410
|
stateObj.set(transformToState(false));
|
|
196
411
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
197
412
|
}
|
|
@@ -202,7 +417,6 @@ export class Modal extends BaseComponent {
|
|
|
202
417
|
const transform = toComponent || ((v) => String(v));
|
|
203
418
|
stateObj.subscribe((val) => {
|
|
204
419
|
const transformed = transform(val);
|
|
205
|
-
body.innerHTML = transformed;
|
|
206
420
|
this.state.content = transformed;
|
|
207
421
|
});
|
|
208
422
|
}
|
|
@@ -210,20 +424,18 @@ export class Modal extends BaseComponent {
|
|
|
210
424
|
const transform = toComponent || ((v) => String(v));
|
|
211
425
|
stateObj.subscribe((val) => {
|
|
212
426
|
const transformed = transform(val);
|
|
213
|
-
const header = modal.querySelector('.jux-modal-header');
|
|
214
|
-
if (header) {
|
|
215
|
-
header.textContent = transformed;
|
|
216
|
-
}
|
|
217
427
|
this.state.title = transformed;
|
|
218
428
|
});
|
|
219
429
|
}
|
|
220
430
|
});
|
|
221
431
|
container.appendChild(overlay);
|
|
432
|
+
requestAnimationFrame(() => {
|
|
433
|
+
if (window.Iconify) {
|
|
434
|
+
window.Iconify.scan();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
222
437
|
return this;
|
|
223
438
|
}
|
|
224
|
-
update(prop, value) {
|
|
225
|
-
// No reactive updates needed
|
|
226
|
-
}
|
|
227
439
|
}
|
|
228
440
|
export function modal(id, options = {}) {
|
|
229
441
|
return new Modal(id, options);
|
package/lib/components/modal.ts
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
2
3
|
|
|
3
4
|
// Event definitions
|
|
4
5
|
const TRIGGER_EVENTS = [] as const;
|
|
5
6
|
const CALLBACK_EVENTS = ['open', 'close'] as const;
|
|
6
7
|
|
|
8
|
+
export type EmojiIconType = {
|
|
9
|
+
// Can be an emoji character or an icon name from a collection
|
|
10
|
+
icon: string;
|
|
11
|
+
collection?: 'lucide' | 'heroicons' | 'material-symbols' | 'material' | 'icon-park' | string;
|
|
12
|
+
}
|
|
7
13
|
export interface ModalOptions {
|
|
8
14
|
title?: string;
|
|
9
|
-
content?: string
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
content?: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
|
|
16
|
+
icon?: EmojiIconType | undefined;
|
|
17
|
+
close?: boolean;
|
|
18
|
+
backdropClose?: boolean;
|
|
19
|
+
position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
|
|
12
20
|
size?: 'small' | 'medium' | 'large';
|
|
21
|
+
actions?: Array<{ label: string; variant?: string; click?: () => void } | Array<BaseComponent<any>>>;
|
|
13
22
|
style?: string;
|
|
14
23
|
class?: string;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
type ModalState = {
|
|
18
27
|
title: string;
|
|
19
|
-
content: string
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
content: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
|
|
29
|
+
icon: EmojiIconType | undefined;
|
|
30
|
+
close: boolean;
|
|
31
|
+
backdropClose: boolean;
|
|
32
|
+
position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
|
|
22
33
|
size: string;
|
|
23
34
|
open: boolean;
|
|
35
|
+
actions: Array<{ label: string; variant?: string; click?: () => void } | Array<BaseComponent<any>>>;
|
|
24
36
|
style: string;
|
|
25
37
|
class: string;
|
|
26
38
|
};
|
|
@@ -32,10 +44,13 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
32
44
|
super(id, {
|
|
33
45
|
title: options.title ?? '',
|
|
34
46
|
content: options.content ?? '',
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
icon: options.icon ?? undefined,
|
|
48
|
+
close: options.close ?? true,
|
|
49
|
+
backdropClose: options.backdropClose ?? true,
|
|
50
|
+
position: options.position,
|
|
37
51
|
size: options.size ?? 'medium',
|
|
38
52
|
open: false,
|
|
53
|
+
actions: options.actions ?? [],
|
|
39
54
|
style: options.style ?? '',
|
|
40
55
|
class: options.class ?? ''
|
|
41
56
|
});
|
|
@@ -50,98 +65,289 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
/* ═════════════════════════════════════════════════════════════════
|
|
53
|
-
*
|
|
68
|
+
* REACTIVE UPDATE (Precision DOM edits)
|
|
54
69
|
* ═════════════════════════════════════════════════════════════════ */
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// - bind(), sync(), renderTo()
|
|
59
|
-
// - addClass(), removeClass(), toggleClass()
|
|
60
|
-
// - visible(), show(), hide()
|
|
61
|
-
// - attr(), attrs(), removeAttr()
|
|
71
|
+
update(prop: string, value: any): void {
|
|
72
|
+
super.update(prop, value);
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
if (!this._overlay) return;
|
|
75
|
+
|
|
76
|
+
const modal = this._overlay.querySelector('.jux-modal');
|
|
77
|
+
if (!modal) return;
|
|
78
|
+
|
|
79
|
+
switch (prop) {
|
|
80
|
+
case 'open':
|
|
81
|
+
this._overlay.style.display = value ? 'flex' : 'none';
|
|
82
|
+
if (value) {
|
|
83
|
+
this._triggerCallback('open');
|
|
84
|
+
} else {
|
|
85
|
+
this._triggerCallback('close');
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'title':
|
|
90
|
+
this._updateTitle(value);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'content':
|
|
94
|
+
this._updateContent(value);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'icon':
|
|
98
|
+
this._updateIcon(value);
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'size':
|
|
102
|
+
modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'actions':
|
|
106
|
+
this._rebuildActions();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private _updateTitle(value: string): void {
|
|
112
|
+
let headerEl = this._overlay!.querySelector('.jux-modal-header');
|
|
113
|
+
|
|
114
|
+
if (!headerEl && value) {
|
|
115
|
+
// Create header if it doesn't exist
|
|
116
|
+
headerEl = document.createElement('div');
|
|
117
|
+
headerEl.className = 'jux-modal-header';
|
|
118
|
+
|
|
119
|
+
const modal = this._overlay!.querySelector('.jux-modal');
|
|
120
|
+
const body = modal!.querySelector('.jux-modal-body');
|
|
121
|
+
modal!.insertBefore(headerEl, body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (headerEl) {
|
|
125
|
+
// Preserve icon if exists
|
|
126
|
+
const iconEl = headerEl.querySelector('.jux-modal-header-icon');
|
|
127
|
+
headerEl.innerHTML = '';
|
|
128
|
+
|
|
129
|
+
if (iconEl) {
|
|
130
|
+
headerEl.appendChild(iconEl);
|
|
70
131
|
}
|
|
132
|
+
|
|
133
|
+
const titleEl = document.createElement('span');
|
|
134
|
+
titleEl.className = 'jux-modal-header-title';
|
|
135
|
+
titleEl.textContent = value;
|
|
136
|
+
headerEl.appendChild(titleEl);
|
|
71
137
|
}
|
|
72
|
-
return this;
|
|
73
138
|
}
|
|
74
139
|
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
140
|
+
private _updateIcon(value?: EmojiIconType): void {
|
|
141
|
+
const headerEl = this._overlay!.querySelector('.jux-modal-header');
|
|
142
|
+
if (!headerEl) return;
|
|
143
|
+
|
|
144
|
+
let iconEl = headerEl.querySelector('.jux-modal-header-icon');
|
|
145
|
+
|
|
146
|
+
if (value) {
|
|
147
|
+
if (!iconEl) {
|
|
148
|
+
iconEl = document.createElement('span');
|
|
149
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
150
|
+
headerEl.insertBefore(iconEl, headerEl.firstChild);
|
|
151
|
+
}
|
|
152
|
+
iconEl.innerHTML = '';
|
|
153
|
+
iconEl.appendChild(renderIcon(value.icon, value.collection));
|
|
154
|
+
} else if (iconEl) {
|
|
155
|
+
iconEl.remove();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _updateContent(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): void {
|
|
160
|
+
const contentEl = this._overlay!.querySelector('.jux-modal-content') as HTMLElement;
|
|
161
|
+
if (!contentEl) return;
|
|
162
|
+
|
|
163
|
+
contentEl.innerHTML = '';
|
|
164
|
+
|
|
165
|
+
const items = Array.isArray(value) ? value : [value];
|
|
166
|
+
|
|
167
|
+
items.forEach(item => {
|
|
168
|
+
if (typeof item === 'string') {
|
|
169
|
+
const tempDiv = document.createElement('div');
|
|
170
|
+
tempDiv.innerHTML = item;
|
|
171
|
+
while (tempDiv.firstChild) {
|
|
172
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
173
|
+
}
|
|
174
|
+
} else if (item instanceof BaseComponent) {
|
|
175
|
+
item.render(contentEl);
|
|
81
176
|
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _rebuildActions(): void {
|
|
181
|
+
const modal = this._overlay!.querySelector('.jux-modal');
|
|
182
|
+
if (!modal) return;
|
|
183
|
+
|
|
184
|
+
let footerEl = modal.querySelector('.jux-modal-footer') as HTMLElement | null;
|
|
185
|
+
|
|
186
|
+
if (this.state.actions.length === 0) {
|
|
187
|
+
if (footerEl) footerEl.remove();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!footerEl) {
|
|
192
|
+
footerEl = document.createElement('div');
|
|
193
|
+
footerEl.className = 'jux-modal-footer';
|
|
194
|
+
modal.appendChild(footerEl);
|
|
82
195
|
}
|
|
196
|
+
|
|
197
|
+
footerEl.innerHTML = '';
|
|
198
|
+
|
|
199
|
+
this.state.actions.forEach(action => {
|
|
200
|
+
// Check if it's an array of BaseComponents
|
|
201
|
+
if (Array.isArray(action)) {
|
|
202
|
+
action.forEach(component => {
|
|
203
|
+
if (component instanceof BaseComponent) {
|
|
204
|
+
component.render(footerEl!);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Otherwise it's an action object with label property
|
|
209
|
+
else if ('label' in action) {
|
|
210
|
+
const btn = document.createElement('button');
|
|
211
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
212
|
+
btn.textContent = action.label;
|
|
213
|
+
btn.type = 'button';
|
|
214
|
+
|
|
215
|
+
btn.addEventListener('click', () => {
|
|
216
|
+
if (action.click) action.click();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
footerEl!.appendChild(btn);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
225
|
+
* FLUENT API (Just set state, update() handles DOM)
|
|
226
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
227
|
+
|
|
228
|
+
title(value: string): this {
|
|
229
|
+
this.state.title = value;
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
|
|
234
|
+
this.state.content = value;
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
icon(value: EmojiIconType): this {
|
|
239
|
+
this.state.icon = value;
|
|
83
240
|
return this;
|
|
84
241
|
}
|
|
85
242
|
|
|
86
|
-
|
|
87
|
-
this.state.
|
|
243
|
+
close(value: boolean): this {
|
|
244
|
+
this.state.close = value;
|
|
88
245
|
return this;
|
|
89
246
|
}
|
|
90
247
|
|
|
91
|
-
|
|
92
|
-
this.state.
|
|
248
|
+
backdropClose(value: boolean): this {
|
|
249
|
+
this.state.backdropClose = value;
|
|
93
250
|
return this;
|
|
94
251
|
}
|
|
95
252
|
|
|
96
253
|
size(value: 'small' | 'medium' | 'large'): this {
|
|
97
254
|
this.state.size = value;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
actions(value: Array<{ label: string; variant?: string; click?: () => void }>): this {
|
|
259
|
+
this.state.actions = value;
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
addAction(action: { label: string; variant?: string; click?: () => void }): this {
|
|
264
|
+
this.state.actions = [...this.state.actions, action];
|
|
104
265
|
return this;
|
|
105
266
|
}
|
|
106
267
|
|
|
107
268
|
open(): this {
|
|
108
269
|
this.state.open = true;
|
|
109
|
-
if (this._overlay) {
|
|
110
|
-
this._overlay.style.display = 'flex';
|
|
111
|
-
}
|
|
112
|
-
// 🎯 Fire the open callback event
|
|
113
|
-
this._triggerCallback('open');
|
|
114
270
|
return this;
|
|
115
271
|
}
|
|
116
272
|
|
|
117
|
-
|
|
273
|
+
closeModal(): this {
|
|
118
274
|
this.state.open = false;
|
|
119
|
-
|
|
120
|
-
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
toggle(): this {
|
|
279
|
+
this.state.open = !this.state.open;
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if modal is currently open
|
|
285
|
+
*/
|
|
286
|
+
isOpen(): boolean {
|
|
287
|
+
return this.state.open;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if modal is currently closed
|
|
292
|
+
*/
|
|
293
|
+
isClosed(): boolean {
|
|
294
|
+
return !this.state.open;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
298
|
+
* DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
|
|
299
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
303
|
+
*/
|
|
304
|
+
addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
|
|
305
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content') as HTMLElement;
|
|
306
|
+
if (!contentEl) {
|
|
307
|
+
console.warn('[Modal] Content element not found');
|
|
308
|
+
return this;
|
|
121
309
|
}
|
|
122
|
-
|
|
123
|
-
|
|
310
|
+
|
|
311
|
+
const items = Array.isArray(content) ? content : [content];
|
|
312
|
+
|
|
313
|
+
items.forEach(item => {
|
|
314
|
+
if (typeof item === 'string') {
|
|
315
|
+
const tempDiv = document.createElement('div');
|
|
316
|
+
tempDiv.innerHTML = item;
|
|
317
|
+
while (tempDiv.firstChild) {
|
|
318
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
319
|
+
}
|
|
320
|
+
} else if (item instanceof BaseComponent) {
|
|
321
|
+
item.render(contentEl);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
124
325
|
return this;
|
|
125
326
|
}
|
|
126
327
|
|
|
127
328
|
/**
|
|
128
|
-
*
|
|
129
|
-
* @returns The ID of the modal body element (e.g., 'mymodal-body')
|
|
130
|
-
*
|
|
131
|
-
* Usage:
|
|
132
|
-
* const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
|
|
133
|
-
* jux.table('files-table', {...}).render(modal.bodyId());
|
|
329
|
+
* Clear all content from modal
|
|
134
330
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
331
|
+
clearContent(): this {
|
|
332
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
333
|
+
if (contentEl) {
|
|
334
|
+
contentEl.innerHTML = '';
|
|
335
|
+
}
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get the modal content element ID for rendering child components
|
|
341
|
+
*/
|
|
342
|
+
contentId(): string {
|
|
343
|
+
return `${this._id}-content`;
|
|
137
344
|
}
|
|
138
345
|
|
|
139
346
|
/**
|
|
140
|
-
* Get the modal
|
|
141
|
-
* @returns The modal body element or null if not rendered
|
|
347
|
+
* Get the modal content element (only available after render)
|
|
142
348
|
*/
|
|
143
|
-
|
|
144
|
-
return document.getElementById(this.
|
|
349
|
+
getContentElement(): HTMLElement | null {
|
|
350
|
+
return document.getElementById(this.contentId());
|
|
145
351
|
}
|
|
146
352
|
|
|
147
353
|
/* ═════════════════════════════════════════════════════════════════
|
|
@@ -151,7 +357,7 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
151
357
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
152
358
|
const container = this._setupContainer(targetId);
|
|
153
359
|
|
|
154
|
-
const { open, title, content, size,
|
|
360
|
+
const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
|
|
155
361
|
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
156
362
|
|
|
157
363
|
const overlay = document.createElement('div');
|
|
@@ -165,42 +371,102 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
165
371
|
const modal = document.createElement('div');
|
|
166
372
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
167
373
|
|
|
168
|
-
if (
|
|
374
|
+
if (close) {
|
|
169
375
|
const closeButton = document.createElement('button');
|
|
170
376
|
closeButton.className = 'jux-modal-close';
|
|
171
377
|
closeButton.innerHTML = '×';
|
|
378
|
+
closeButton.type = 'button';
|
|
172
379
|
modal.appendChild(closeButton);
|
|
173
380
|
}
|
|
174
381
|
|
|
175
|
-
if (title) {
|
|
382
|
+
if (title || icon) {
|
|
176
383
|
const header = document.createElement('div');
|
|
177
384
|
header.className = 'jux-modal-header';
|
|
178
|
-
|
|
385
|
+
|
|
386
|
+
if (icon) {
|
|
387
|
+
const iconEl = document.createElement('span');
|
|
388
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
389
|
+
iconEl.appendChild(renderIcon(icon.icon, icon.collection));
|
|
390
|
+
header.appendChild(iconEl);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (title) {
|
|
394
|
+
const titleEl = document.createElement('span');
|
|
395
|
+
titleEl.className = 'jux-modal-header-title';
|
|
396
|
+
titleEl.textContent = title;
|
|
397
|
+
header.appendChild(titleEl);
|
|
398
|
+
}
|
|
399
|
+
|
|
179
400
|
modal.appendChild(header);
|
|
180
401
|
}
|
|
181
402
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
403
|
+
const contentElement = document.createElement('div');
|
|
404
|
+
contentElement.className = 'jux-modal-content';
|
|
405
|
+
contentElement.id = this.contentId();
|
|
406
|
+
modal.appendChild(contentElement);
|
|
407
|
+
|
|
408
|
+
// Render initial content
|
|
409
|
+
if (content) {
|
|
410
|
+
const items = Array.isArray(content) ? content : [content];
|
|
411
|
+
items.forEach(item => {
|
|
412
|
+
if (typeof item === 'string') {
|
|
413
|
+
const tempDiv = document.createElement('div');
|
|
414
|
+
tempDiv.innerHTML = item;
|
|
415
|
+
while (tempDiv.firstChild) {
|
|
416
|
+
contentElement.appendChild(tempDiv.firstChild);
|
|
417
|
+
}
|
|
418
|
+
} else if (item instanceof BaseComponent) {
|
|
419
|
+
item.render(contentElement);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (actions.length > 0) {
|
|
425
|
+
const footer = document.createElement('div');
|
|
426
|
+
footer.className = 'jux-modal-footer';
|
|
427
|
+
|
|
428
|
+
actions.forEach(action => {
|
|
429
|
+
// Check if it's an array of BaseComponents
|
|
430
|
+
if (Array.isArray(action)) {
|
|
431
|
+
action.forEach(component => {
|
|
432
|
+
if (component instanceof BaseComponent) {
|
|
433
|
+
component.render(footer);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Otherwise it's an action object with label property
|
|
438
|
+
else if ('label' in action) {
|
|
439
|
+
const btn = document.createElement('button');
|
|
440
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
441
|
+
btn.textContent = action.label;
|
|
442
|
+
btn.type = 'button';
|
|
443
|
+
|
|
444
|
+
btn.addEventListener('click', () => {
|
|
445
|
+
if (action.click) action.click();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
footer.appendChild(btn);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
modal.appendChild(footer);
|
|
453
|
+
}
|
|
187
454
|
|
|
188
455
|
overlay.appendChild(modal);
|
|
189
456
|
|
|
457
|
+
// Default dismiss behavior (only if NOT using sync)
|
|
190
458
|
if (!hasOpenSync) {
|
|
191
|
-
if (
|
|
459
|
+
if (close) {
|
|
192
460
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
193
461
|
closeButton?.addEventListener('click', () => {
|
|
194
462
|
this.state.open = false;
|
|
195
|
-
overlay.style.display = 'none';
|
|
196
463
|
});
|
|
197
464
|
}
|
|
198
465
|
|
|
199
|
-
if (
|
|
466
|
+
if (backdropClose) {
|
|
200
467
|
overlay.addEventListener('click', (e) => {
|
|
201
468
|
if (e.target === overlay) {
|
|
202
469
|
this.state.open = false;
|
|
203
|
-
overlay.style.display = 'none';
|
|
204
470
|
}
|
|
205
471
|
});
|
|
206
472
|
}
|
|
@@ -208,6 +474,7 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
208
474
|
|
|
209
475
|
this._wireStandardEvents(overlay);
|
|
210
476
|
|
|
477
|
+
// Wire sync bindings
|
|
211
478
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
212
479
|
if (property === 'open') {
|
|
213
480
|
const transformToState = toState || ((v: any) => Boolean(v));
|
|
@@ -219,31 +486,28 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
219
486
|
if (isUpdating) return;
|
|
220
487
|
const transformed = transformToComponent(val);
|
|
221
488
|
this.state.open = transformed;
|
|
222
|
-
overlay.style.display = transformed ? 'flex' : 'none';
|
|
223
489
|
});
|
|
224
490
|
|
|
225
|
-
if (
|
|
491
|
+
if (close) {
|
|
226
492
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
227
493
|
closeButton?.addEventListener('click', () => {
|
|
228
494
|
if (isUpdating) return;
|
|
229
495
|
isUpdating = true;
|
|
230
496
|
|
|
231
497
|
this.state.open = false;
|
|
232
|
-
overlay.style.display = 'none';
|
|
233
498
|
stateObj.set(transformToState(false));
|
|
234
499
|
|
|
235
500
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
236
501
|
});
|
|
237
502
|
}
|
|
238
503
|
|
|
239
|
-
if (
|
|
504
|
+
if (backdropClose) {
|
|
240
505
|
overlay.addEventListener('click', (e) => {
|
|
241
506
|
if (e.target === overlay) {
|
|
242
507
|
if (isUpdating) return;
|
|
243
508
|
isUpdating = true;
|
|
244
509
|
|
|
245
510
|
this.state.open = false;
|
|
246
|
-
overlay.style.display = 'none';
|
|
247
511
|
stateObj.set(transformToState(false));
|
|
248
512
|
|
|
249
513
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
@@ -256,7 +520,6 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
256
520
|
|
|
257
521
|
stateObj.subscribe((val: any) => {
|
|
258
522
|
const transformed = transform(val);
|
|
259
|
-
body.innerHTML = transformed;
|
|
260
523
|
this.state.content = transformed;
|
|
261
524
|
});
|
|
262
525
|
}
|
|
@@ -265,21 +528,20 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
265
528
|
|
|
266
529
|
stateObj.subscribe((val: any) => {
|
|
267
530
|
const transformed = transform(val);
|
|
268
|
-
const header = modal.querySelector('.jux-modal-header');
|
|
269
|
-
if (header) {
|
|
270
|
-
header.textContent = transformed;
|
|
271
|
-
}
|
|
272
531
|
this.state.title = transformed;
|
|
273
532
|
});
|
|
274
533
|
}
|
|
275
534
|
});
|
|
276
535
|
|
|
277
536
|
container.appendChild(overlay);
|
|
278
|
-
return this;
|
|
279
|
-
}
|
|
280
537
|
|
|
281
|
-
|
|
282
|
-
|
|
538
|
+
requestAnimationFrame(() => {
|
|
539
|
+
if ((window as any).Iconify) {
|
|
540
|
+
(window as any).Iconify.scan();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
return this;
|
|
283
545
|
}
|
|
284
546
|
}
|
|
285
547
|
|