juxscript 1.1.60 → 1.1.62
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 +57 -21
- package/lib/components/modal.d.ts.map +1 -1
- package/lib/components/modal.js +279 -79
- package/lib/components/modal.ts +336 -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,47 @@ 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
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
70
|
+
*/
|
|
71
|
+
addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
|
|
33
72
|
/**
|
|
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());
|
|
73
|
+
* Clear all content from modal
|
|
40
74
|
*/
|
|
41
|
-
|
|
75
|
+
clearContent(): this;
|
|
42
76
|
/**
|
|
43
|
-
* Get the modal
|
|
44
|
-
* @returns The modal body element or null if not rendered
|
|
77
|
+
* Get the modal content element ID for rendering child components
|
|
45
78
|
*/
|
|
46
|
-
|
|
79
|
+
contentId(): string;
|
|
80
|
+
/**
|
|
81
|
+
* Get the modal content element (only available after render)
|
|
82
|
+
*/
|
|
83
|
+
getContentElement(): HTMLElement | null;
|
|
47
84
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
48
|
-
update(prop: string, value: any): void;
|
|
49
85
|
}
|
|
50
86
|
export declare function modal(id: string, options?: ModalOptions): Modal;
|
|
51
87
|
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,243 @@ 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);
|
|
99
|
+
}
|
|
100
|
+
iconEl.innerHTML = '';
|
|
101
|
+
iconEl.appendChild(renderIcon(value.icon, value.collection));
|
|
102
|
+
}
|
|
103
|
+
else if (iconEl) {
|
|
104
|
+
iconEl.remove();
|
|
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);
|
|
41
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;
|
|
42
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
|
+
* DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
|
|
214
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
215
|
+
/**
|
|
216
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
217
|
+
*/
|
|
218
|
+
addContent(content) {
|
|
219
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
220
|
+
if (!contentEl) {
|
|
221
|
+
console.warn('[Modal] Content element not found');
|
|
222
|
+
return this;
|
|
86
223
|
}
|
|
87
|
-
|
|
88
|
-
|
|
224
|
+
const items = Array.isArray(content) ? content : [content];
|
|
225
|
+
items.forEach(item => {
|
|
226
|
+
if (typeof item === 'string') {
|
|
227
|
+
const tempDiv = document.createElement('div');
|
|
228
|
+
tempDiv.innerHTML = item;
|
|
229
|
+
while (tempDiv.firstChild) {
|
|
230
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (item instanceof BaseComponent) {
|
|
234
|
+
item.render(contentEl);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
89
237
|
return this;
|
|
90
238
|
}
|
|
91
239
|
/**
|
|
92
|
-
*
|
|
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());
|
|
240
|
+
* Clear all content from modal
|
|
98
241
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
242
|
+
clearContent() {
|
|
243
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
244
|
+
if (contentEl) {
|
|
245
|
+
contentEl.innerHTML = '';
|
|
246
|
+
}
|
|
247
|
+
return this;
|
|
101
248
|
}
|
|
102
249
|
/**
|
|
103
|
-
* Get the modal
|
|
104
|
-
* @returns The modal body element or null if not rendered
|
|
250
|
+
* Get the modal content element ID for rendering child components
|
|
105
251
|
*/
|
|
106
|
-
|
|
107
|
-
return
|
|
252
|
+
contentId() {
|
|
253
|
+
return `${this._id}-content`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get the modal content element (only available after render)
|
|
257
|
+
*/
|
|
258
|
+
getContentElement() {
|
|
259
|
+
return document.getElementById(this.contentId());
|
|
108
260
|
}
|
|
109
261
|
/* ═════════════════════════════════════════════════════════════════
|
|
110
262
|
* RENDER
|
|
111
263
|
* ═════════════════════════════════════════════════════════════════ */
|
|
112
264
|
render(targetId) {
|
|
113
265
|
const container = this._setupContainer(targetId);
|
|
114
|
-
const { open, title, content, size,
|
|
266
|
+
const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
|
|
115
267
|
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
116
268
|
const overlay = document.createElement('div');
|
|
117
269
|
overlay.className = 'jux-modal-overlay';
|
|
@@ -124,42 +276,96 @@ export class Modal extends BaseComponent {
|
|
|
124
276
|
this._overlay = overlay;
|
|
125
277
|
const modal = document.createElement('div');
|
|
126
278
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
127
|
-
if (
|
|
279
|
+
if (close) {
|
|
128
280
|
const closeButton = document.createElement('button');
|
|
129
281
|
closeButton.className = 'jux-modal-close';
|
|
130
282
|
closeButton.innerHTML = '×';
|
|
283
|
+
closeButton.type = 'button';
|
|
131
284
|
modal.appendChild(closeButton);
|
|
132
285
|
}
|
|
133
|
-
if (title) {
|
|
286
|
+
if (title || icon) {
|
|
134
287
|
const header = document.createElement('div');
|
|
135
288
|
header.className = 'jux-modal-header';
|
|
136
|
-
|
|
289
|
+
if (icon) {
|
|
290
|
+
const iconEl = document.createElement('span');
|
|
291
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
292
|
+
iconEl.appendChild(renderIcon(icon.icon, icon.collection));
|
|
293
|
+
header.appendChild(iconEl);
|
|
294
|
+
}
|
|
295
|
+
if (title) {
|
|
296
|
+
const titleEl = document.createElement('span');
|
|
297
|
+
titleEl.className = 'jux-modal-header-title';
|
|
298
|
+
titleEl.textContent = title;
|
|
299
|
+
header.appendChild(titleEl);
|
|
300
|
+
}
|
|
137
301
|
modal.appendChild(header);
|
|
138
302
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
303
|
+
const contentElement = document.createElement('div');
|
|
304
|
+
contentElement.className = 'jux-modal-content';
|
|
305
|
+
contentElement.id = this.contentId();
|
|
306
|
+
modal.appendChild(contentElement);
|
|
307
|
+
// Render initial content
|
|
308
|
+
if (content) {
|
|
309
|
+
const items = Array.isArray(content) ? content : [content];
|
|
310
|
+
items.forEach(item => {
|
|
311
|
+
if (typeof item === 'string') {
|
|
312
|
+
const tempDiv = document.createElement('div');
|
|
313
|
+
tempDiv.innerHTML = item;
|
|
314
|
+
while (tempDiv.firstChild) {
|
|
315
|
+
contentElement.appendChild(tempDiv.firstChild);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (item instanceof BaseComponent) {
|
|
319
|
+
item.render(contentElement);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (actions.length > 0) {
|
|
324
|
+
const footer = document.createElement('div');
|
|
325
|
+
footer.className = 'jux-modal-footer';
|
|
326
|
+
actions.forEach(action => {
|
|
327
|
+
// Check if it's an array of BaseComponents
|
|
328
|
+
if (Array.isArray(action)) {
|
|
329
|
+
action.forEach(component => {
|
|
330
|
+
if (component instanceof BaseComponent) {
|
|
331
|
+
component.render(footer);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// Otherwise it's an action object with label property
|
|
336
|
+
else if ('label' in action) {
|
|
337
|
+
const btn = document.createElement('button');
|
|
338
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
339
|
+
btn.textContent = action.label;
|
|
340
|
+
btn.type = 'button';
|
|
341
|
+
btn.addEventListener('click', () => {
|
|
342
|
+
if (action.click)
|
|
343
|
+
action.click();
|
|
344
|
+
});
|
|
345
|
+
footer.appendChild(btn);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
modal.appendChild(footer);
|
|
349
|
+
}
|
|
144
350
|
overlay.appendChild(modal);
|
|
351
|
+
// Default dismiss behavior (only if NOT using sync)
|
|
145
352
|
if (!hasOpenSync) {
|
|
146
|
-
if (
|
|
353
|
+
if (close) {
|
|
147
354
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
148
355
|
closeButton?.addEventListener('click', () => {
|
|
149
356
|
this.state.open = false;
|
|
150
|
-
overlay.style.display = 'none';
|
|
151
357
|
});
|
|
152
358
|
}
|
|
153
|
-
if (
|
|
359
|
+
if (backdropClose) {
|
|
154
360
|
overlay.addEventListener('click', (e) => {
|
|
155
361
|
if (e.target === overlay) {
|
|
156
362
|
this.state.open = false;
|
|
157
|
-
overlay.style.display = 'none';
|
|
158
363
|
}
|
|
159
364
|
});
|
|
160
365
|
}
|
|
161
366
|
}
|
|
162
367
|
this._wireStandardEvents(overlay);
|
|
368
|
+
// Wire sync bindings
|
|
163
369
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
164
370
|
if (property === 'open') {
|
|
165
371
|
const transformToState = toState || ((v) => Boolean(v));
|
|
@@ -170,28 +376,25 @@ export class Modal extends BaseComponent {
|
|
|
170
376
|
return;
|
|
171
377
|
const transformed = transformToComponent(val);
|
|
172
378
|
this.state.open = transformed;
|
|
173
|
-
overlay.style.display = transformed ? 'flex' : 'none';
|
|
174
379
|
});
|
|
175
|
-
if (
|
|
380
|
+
if (close) {
|
|
176
381
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
177
382
|
closeButton?.addEventListener('click', () => {
|
|
178
383
|
if (isUpdating)
|
|
179
384
|
return;
|
|
180
385
|
isUpdating = true;
|
|
181
386
|
this.state.open = false;
|
|
182
|
-
overlay.style.display = 'none';
|
|
183
387
|
stateObj.set(transformToState(false));
|
|
184
388
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
185
389
|
});
|
|
186
390
|
}
|
|
187
|
-
if (
|
|
391
|
+
if (backdropClose) {
|
|
188
392
|
overlay.addEventListener('click', (e) => {
|
|
189
393
|
if (e.target === overlay) {
|
|
190
394
|
if (isUpdating)
|
|
191
395
|
return;
|
|
192
396
|
isUpdating = true;
|
|
193
397
|
this.state.open = false;
|
|
194
|
-
overlay.style.display = 'none';
|
|
195
398
|
stateObj.set(transformToState(false));
|
|
196
399
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
197
400
|
}
|
|
@@ -202,7 +405,6 @@ export class Modal extends BaseComponent {
|
|
|
202
405
|
const transform = toComponent || ((v) => String(v));
|
|
203
406
|
stateObj.subscribe((val) => {
|
|
204
407
|
const transformed = transform(val);
|
|
205
|
-
body.innerHTML = transformed;
|
|
206
408
|
this.state.content = transformed;
|
|
207
409
|
});
|
|
208
410
|
}
|
|
@@ -210,20 +412,18 @@ export class Modal extends BaseComponent {
|
|
|
210
412
|
const transform = toComponent || ((v) => String(v));
|
|
211
413
|
stateObj.subscribe((val) => {
|
|
212
414
|
const transformed = transform(val);
|
|
213
|
-
const header = modal.querySelector('.jux-modal-header');
|
|
214
|
-
if (header) {
|
|
215
|
-
header.textContent = transformed;
|
|
216
|
-
}
|
|
217
415
|
this.state.title = transformed;
|
|
218
416
|
});
|
|
219
417
|
}
|
|
220
418
|
});
|
|
221
419
|
container.appendChild(overlay);
|
|
420
|
+
requestAnimationFrame(() => {
|
|
421
|
+
if (window.Iconify) {
|
|
422
|
+
window.Iconify.scan();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
222
425
|
return this;
|
|
223
426
|
}
|
|
224
|
-
update(prop, value) {
|
|
225
|
-
// No reactive updates needed
|
|
226
|
-
}
|
|
227
427
|
}
|
|
228
428
|
export function modal(id, options = {}) {
|
|
229
429
|
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,275 @@ 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;
|
|
82
189
|
}
|
|
190
|
+
|
|
191
|
+
if (!footerEl) {
|
|
192
|
+
footerEl = document.createElement('div');
|
|
193
|
+
footerEl.className = 'jux-modal-footer';
|
|
194
|
+
modal.appendChild(footerEl);
|
|
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;
|
|
83
230
|
return this;
|
|
84
231
|
}
|
|
85
232
|
|
|
86
|
-
|
|
87
|
-
this.state.
|
|
233
|
+
content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
|
|
234
|
+
this.state.content = value;
|
|
88
235
|
return this;
|
|
89
236
|
}
|
|
90
237
|
|
|
91
|
-
|
|
92
|
-
this.state.
|
|
238
|
+
icon(value: EmojiIconType): this {
|
|
239
|
+
this.state.icon = value;
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
close(value: boolean): this {
|
|
244
|
+
this.state.close = value;
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
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
|
+
* DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
|
|
285
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Add content to modal (supports strings and BaseComponents)
|
|
289
|
+
*/
|
|
290
|
+
addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
|
|
291
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content') as HTMLElement;
|
|
292
|
+
if (!contentEl) {
|
|
293
|
+
console.warn('[Modal] Content element not found');
|
|
294
|
+
return this;
|
|
121
295
|
}
|
|
122
|
-
|
|
123
|
-
|
|
296
|
+
|
|
297
|
+
const items = Array.isArray(content) ? content : [content];
|
|
298
|
+
|
|
299
|
+
items.forEach(item => {
|
|
300
|
+
if (typeof item === 'string') {
|
|
301
|
+
const tempDiv = document.createElement('div');
|
|
302
|
+
tempDiv.innerHTML = item;
|
|
303
|
+
while (tempDiv.firstChild) {
|
|
304
|
+
contentEl.appendChild(tempDiv.firstChild);
|
|
305
|
+
}
|
|
306
|
+
} else if (item instanceof BaseComponent) {
|
|
307
|
+
item.render(contentEl);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
124
311
|
return this;
|
|
125
312
|
}
|
|
126
313
|
|
|
127
314
|
/**
|
|
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());
|
|
315
|
+
* Clear all content from modal
|
|
134
316
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
317
|
+
clearContent(): this {
|
|
318
|
+
const contentEl = this._overlay?.querySelector('.jux-modal-content');
|
|
319
|
+
if (contentEl) {
|
|
320
|
+
contentEl.innerHTML = '';
|
|
321
|
+
}
|
|
322
|
+
return this;
|
|
137
323
|
}
|
|
138
324
|
|
|
139
325
|
/**
|
|
140
|
-
* Get the modal
|
|
141
|
-
* @returns The modal body element or null if not rendered
|
|
326
|
+
* Get the modal content element ID for rendering child components
|
|
142
327
|
*/
|
|
143
|
-
|
|
144
|
-
return
|
|
328
|
+
contentId(): string {
|
|
329
|
+
return `${this._id}-content`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the modal content element (only available after render)
|
|
334
|
+
*/
|
|
335
|
+
getContentElement(): HTMLElement | null {
|
|
336
|
+
return document.getElementById(this.contentId());
|
|
145
337
|
}
|
|
146
338
|
|
|
147
339
|
/* ═════════════════════════════════════════════════════════════════
|
|
@@ -151,7 +343,7 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
151
343
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
152
344
|
const container = this._setupContainer(targetId);
|
|
153
345
|
|
|
154
|
-
const { open, title, content, size,
|
|
346
|
+
const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
|
|
155
347
|
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
156
348
|
|
|
157
349
|
const overlay = document.createElement('div');
|
|
@@ -165,42 +357,102 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
165
357
|
const modal = document.createElement('div');
|
|
166
358
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
167
359
|
|
|
168
|
-
if (
|
|
360
|
+
if (close) {
|
|
169
361
|
const closeButton = document.createElement('button');
|
|
170
362
|
closeButton.className = 'jux-modal-close';
|
|
171
363
|
closeButton.innerHTML = '×';
|
|
364
|
+
closeButton.type = 'button';
|
|
172
365
|
modal.appendChild(closeButton);
|
|
173
366
|
}
|
|
174
367
|
|
|
175
|
-
if (title) {
|
|
368
|
+
if (title || icon) {
|
|
176
369
|
const header = document.createElement('div');
|
|
177
370
|
header.className = 'jux-modal-header';
|
|
178
|
-
|
|
371
|
+
|
|
372
|
+
if (icon) {
|
|
373
|
+
const iconEl = document.createElement('span');
|
|
374
|
+
iconEl.className = 'jux-modal-header-icon';
|
|
375
|
+
iconEl.appendChild(renderIcon(icon.icon, icon.collection));
|
|
376
|
+
header.appendChild(iconEl);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (title) {
|
|
380
|
+
const titleEl = document.createElement('span');
|
|
381
|
+
titleEl.className = 'jux-modal-header-title';
|
|
382
|
+
titleEl.textContent = title;
|
|
383
|
+
header.appendChild(titleEl);
|
|
384
|
+
}
|
|
385
|
+
|
|
179
386
|
modal.appendChild(header);
|
|
180
387
|
}
|
|
181
388
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
389
|
+
const contentElement = document.createElement('div');
|
|
390
|
+
contentElement.className = 'jux-modal-content';
|
|
391
|
+
contentElement.id = this.contentId();
|
|
392
|
+
modal.appendChild(contentElement);
|
|
393
|
+
|
|
394
|
+
// Render initial content
|
|
395
|
+
if (content) {
|
|
396
|
+
const items = Array.isArray(content) ? content : [content];
|
|
397
|
+
items.forEach(item => {
|
|
398
|
+
if (typeof item === 'string') {
|
|
399
|
+
const tempDiv = document.createElement('div');
|
|
400
|
+
tempDiv.innerHTML = item;
|
|
401
|
+
while (tempDiv.firstChild) {
|
|
402
|
+
contentElement.appendChild(tempDiv.firstChild);
|
|
403
|
+
}
|
|
404
|
+
} else if (item instanceof BaseComponent) {
|
|
405
|
+
item.render(contentElement);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (actions.length > 0) {
|
|
411
|
+
const footer = document.createElement('div');
|
|
412
|
+
footer.className = 'jux-modal-footer';
|
|
413
|
+
|
|
414
|
+
actions.forEach(action => {
|
|
415
|
+
// Check if it's an array of BaseComponents
|
|
416
|
+
if (Array.isArray(action)) {
|
|
417
|
+
action.forEach(component => {
|
|
418
|
+
if (component instanceof BaseComponent) {
|
|
419
|
+
component.render(footer);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
// Otherwise it's an action object with label property
|
|
424
|
+
else if ('label' in action) {
|
|
425
|
+
const btn = document.createElement('button');
|
|
426
|
+
btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
|
|
427
|
+
btn.textContent = action.label;
|
|
428
|
+
btn.type = 'button';
|
|
429
|
+
|
|
430
|
+
btn.addEventListener('click', () => {
|
|
431
|
+
if (action.click) action.click();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
footer.appendChild(btn);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
modal.appendChild(footer);
|
|
439
|
+
}
|
|
187
440
|
|
|
188
441
|
overlay.appendChild(modal);
|
|
189
442
|
|
|
443
|
+
// Default dismiss behavior (only if NOT using sync)
|
|
190
444
|
if (!hasOpenSync) {
|
|
191
|
-
if (
|
|
445
|
+
if (close) {
|
|
192
446
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
193
447
|
closeButton?.addEventListener('click', () => {
|
|
194
448
|
this.state.open = false;
|
|
195
|
-
overlay.style.display = 'none';
|
|
196
449
|
});
|
|
197
450
|
}
|
|
198
451
|
|
|
199
|
-
if (
|
|
452
|
+
if (backdropClose) {
|
|
200
453
|
overlay.addEventListener('click', (e) => {
|
|
201
454
|
if (e.target === overlay) {
|
|
202
455
|
this.state.open = false;
|
|
203
|
-
overlay.style.display = 'none';
|
|
204
456
|
}
|
|
205
457
|
});
|
|
206
458
|
}
|
|
@@ -208,6 +460,7 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
208
460
|
|
|
209
461
|
this._wireStandardEvents(overlay);
|
|
210
462
|
|
|
463
|
+
// Wire sync bindings
|
|
211
464
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
212
465
|
if (property === 'open') {
|
|
213
466
|
const transformToState = toState || ((v: any) => Boolean(v));
|
|
@@ -219,31 +472,28 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
219
472
|
if (isUpdating) return;
|
|
220
473
|
const transformed = transformToComponent(val);
|
|
221
474
|
this.state.open = transformed;
|
|
222
|
-
overlay.style.display = transformed ? 'flex' : 'none';
|
|
223
475
|
});
|
|
224
476
|
|
|
225
|
-
if (
|
|
477
|
+
if (close) {
|
|
226
478
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
227
479
|
closeButton?.addEventListener('click', () => {
|
|
228
480
|
if (isUpdating) return;
|
|
229
481
|
isUpdating = true;
|
|
230
482
|
|
|
231
483
|
this.state.open = false;
|
|
232
|
-
overlay.style.display = 'none';
|
|
233
484
|
stateObj.set(transformToState(false));
|
|
234
485
|
|
|
235
486
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
236
487
|
});
|
|
237
488
|
}
|
|
238
489
|
|
|
239
|
-
if (
|
|
490
|
+
if (backdropClose) {
|
|
240
491
|
overlay.addEventListener('click', (e) => {
|
|
241
492
|
if (e.target === overlay) {
|
|
242
493
|
if (isUpdating) return;
|
|
243
494
|
isUpdating = true;
|
|
244
495
|
|
|
245
496
|
this.state.open = false;
|
|
246
|
-
overlay.style.display = 'none';
|
|
247
497
|
stateObj.set(transformToState(false));
|
|
248
498
|
|
|
249
499
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
@@ -256,7 +506,6 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
256
506
|
|
|
257
507
|
stateObj.subscribe((val: any) => {
|
|
258
508
|
const transformed = transform(val);
|
|
259
|
-
body.innerHTML = transformed;
|
|
260
509
|
this.state.content = transformed;
|
|
261
510
|
});
|
|
262
511
|
}
|
|
@@ -265,21 +514,20 @@ export class Modal extends BaseComponent<ModalState> {
|
|
|
265
514
|
|
|
266
515
|
stateObj.subscribe((val: any) => {
|
|
267
516
|
const transformed = transform(val);
|
|
268
|
-
const header = modal.querySelector('.jux-modal-header');
|
|
269
|
-
if (header) {
|
|
270
|
-
header.textContent = transformed;
|
|
271
|
-
}
|
|
272
517
|
this.state.title = transformed;
|
|
273
518
|
});
|
|
274
519
|
}
|
|
275
520
|
});
|
|
276
521
|
|
|
277
522
|
container.appendChild(overlay);
|
|
278
|
-
return this;
|
|
279
|
-
}
|
|
280
523
|
|
|
281
|
-
|
|
282
|
-
|
|
524
|
+
requestAnimationFrame(() => {
|
|
525
|
+
if ((window as any).Iconify) {
|
|
526
|
+
(window as any).Iconify.scan();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return this;
|
|
283
531
|
}
|
|
284
532
|
}
|
|
285
533
|
|