juxscript 1.0.19 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/lib/components/menu.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
2
3
|
import { req } from './req.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Event definitions
|
|
6
|
+
const TRIGGER_EVENTS = [] as const;
|
|
7
|
+
const CALLBACK_EVENTS = ['itemClick'] as const; // ✅ Fire when menu item is clicked
|
|
8
|
+
|
|
7
9
|
export interface MenuItem {
|
|
8
10
|
label: string;
|
|
9
11
|
href?: string;
|
|
@@ -11,11 +13,9 @@ export interface MenuItem {
|
|
|
11
13
|
icon?: string;
|
|
12
14
|
items?: MenuItem[];
|
|
13
15
|
active?: boolean;
|
|
16
|
+
itemClass?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
/**
|
|
17
|
-
* Menu component options
|
|
18
|
-
*/
|
|
19
19
|
export interface MenuOptions {
|
|
20
20
|
items?: MenuItem[];
|
|
21
21
|
orientation?: 'vertical' | 'horizontal';
|
|
@@ -23,9 +23,6 @@ export interface MenuOptions {
|
|
|
23
23
|
class?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* Menu component state
|
|
28
|
-
*/
|
|
29
26
|
type MenuState = {
|
|
30
27
|
items: MenuItem[];
|
|
31
28
|
orientation: string;
|
|
@@ -33,50 +30,30 @@ type MenuState = {
|
|
|
33
30
|
class: string;
|
|
34
31
|
};
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
* Menu component
|
|
38
|
-
*
|
|
39
|
-
* Usage:
|
|
40
|
-
* const menu = jux.menu('myMenu', {
|
|
41
|
-
* orientation: 'vertical',
|
|
42
|
-
* items: [
|
|
43
|
-
* { label: 'Home', href: '/' },
|
|
44
|
-
* { label: 'About', href: '/about' }
|
|
45
|
-
* ]
|
|
46
|
-
* });
|
|
47
|
-
* menu.render();
|
|
48
|
-
*
|
|
49
|
-
* Active states are automatically set based on current URL
|
|
50
|
-
*/
|
|
51
|
-
export class Menu {
|
|
52
|
-
state: MenuState;
|
|
53
|
-
container: HTMLElement | null = null;
|
|
54
|
-
_id: string;
|
|
55
|
-
id: string;
|
|
56
|
-
|
|
33
|
+
export class Menu extends BaseComponent<MenuState> {
|
|
57
34
|
constructor(id: string, options: MenuOptions = {}) {
|
|
58
|
-
|
|
59
|
-
this.id = id;
|
|
60
|
-
|
|
61
|
-
this.state = {
|
|
35
|
+
super(id, {
|
|
62
36
|
items: options.items ?? [],
|
|
63
37
|
orientation: options.orientation ?? 'vertical',
|
|
64
38
|
style: options.style ?? '',
|
|
65
39
|
class: options.class ?? ''
|
|
66
|
-
};
|
|
40
|
+
});
|
|
67
41
|
|
|
68
|
-
// Auto-set active state based on current path
|
|
69
42
|
this._setActiveStates();
|
|
70
43
|
}
|
|
71
44
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
45
|
+
protected getTriggerEvents(): readonly string[] {
|
|
46
|
+
return TRIGGER_EVENTS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected getCallbackEvents(): readonly string[] {
|
|
50
|
+
return CALLBACK_EVENTS;
|
|
51
|
+
}
|
|
52
|
+
|
|
75
53
|
private _setActiveStates(): void {
|
|
76
54
|
this.state.items = this.state.items.map(item => ({
|
|
77
55
|
...item,
|
|
78
56
|
active: item.href ? req.isActiveNavItem(item.href) : false,
|
|
79
|
-
// Recursively set active for subitems
|
|
80
57
|
items: item.items?.map(subItem => ({
|
|
81
58
|
...subItem,
|
|
82
59
|
active: subItem.href ? req.isActiveNavItem(subItem.href) : false
|
|
@@ -84,9 +61,11 @@ export class Menu {
|
|
|
84
61
|
}));
|
|
85
62
|
}
|
|
86
63
|
|
|
87
|
-
/*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
64
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
65
|
+
* FLUENT API
|
|
66
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
67
|
+
|
|
68
|
+
// ✅ Inherited from BaseComponent
|
|
90
69
|
|
|
91
70
|
items(value: MenuItem[]): this {
|
|
92
71
|
this.state.items = value;
|
|
@@ -100,30 +79,35 @@ export class Menu {
|
|
|
100
79
|
return this;
|
|
101
80
|
}
|
|
102
81
|
|
|
103
|
-
|
|
104
|
-
this.state.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
82
|
+
itemClass(className: string): this {
|
|
83
|
+
this.state.items = this.state.items.map(item => ({
|
|
84
|
+
...item,
|
|
85
|
+
itemClass: className,
|
|
86
|
+
items: item.items?.map(subItem => ({
|
|
87
|
+
...subItem,
|
|
88
|
+
itemClass: className
|
|
89
|
+
}))
|
|
90
|
+
}));
|
|
110
91
|
return this;
|
|
111
92
|
}
|
|
112
93
|
|
|
113
|
-
|
|
114
|
-
this.state.
|
|
94
|
+
orientation(value: 'vertical' | 'horizontal'): this {
|
|
95
|
+
this.state.orientation = value;
|
|
115
96
|
return this;
|
|
116
97
|
}
|
|
117
98
|
|
|
118
|
-
/*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
99
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
100
|
+
* HELPERS
|
|
101
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
121
102
|
|
|
122
103
|
private _renderMenuItem(item: MenuItem): HTMLElement {
|
|
123
104
|
const menuItem = document.createElement('div');
|
|
124
105
|
menuItem.className = 'jux-menu-item';
|
|
125
106
|
|
|
126
|
-
|
|
107
|
+
if (item.itemClass) {
|
|
108
|
+
menuItem.className += ` ${item.itemClass}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
127
111
|
if (item.active) {
|
|
128
112
|
menuItem.classList.add('jux-menu-item-active');
|
|
129
113
|
}
|
|
@@ -133,7 +117,6 @@ export class Menu {
|
|
|
133
117
|
link.className = 'jux-menu-link';
|
|
134
118
|
link.href = item.href;
|
|
135
119
|
|
|
136
|
-
// Add active class to link if item is active
|
|
137
120
|
if (item.active) {
|
|
138
121
|
link.classList.add('jux-menu-link-active');
|
|
139
122
|
}
|
|
@@ -141,7 +124,7 @@ export class Menu {
|
|
|
141
124
|
if (item.icon) {
|
|
142
125
|
const icon = document.createElement('span');
|
|
143
126
|
icon.className = 'jux-menu-icon';
|
|
144
|
-
icon.
|
|
127
|
+
icon.appendChild(renderIcon(item.icon));
|
|
145
128
|
link.appendChild(icon);
|
|
146
129
|
}
|
|
147
130
|
|
|
@@ -150,6 +133,11 @@ export class Menu {
|
|
|
150
133
|
label.textContent = item.label;
|
|
151
134
|
link.appendChild(label);
|
|
152
135
|
|
|
136
|
+
// ✅ Fire itemClick callback when link is clicked
|
|
137
|
+
link.addEventListener('click', (e) => {
|
|
138
|
+
this._triggerCallback('itemClick', { item, event: e });
|
|
139
|
+
});
|
|
140
|
+
|
|
153
141
|
menuItem.appendChild(link);
|
|
154
142
|
} else {
|
|
155
143
|
const button = document.createElement('button');
|
|
@@ -158,7 +146,7 @@ export class Menu {
|
|
|
158
146
|
if (item.icon) {
|
|
159
147
|
const icon = document.createElement('span');
|
|
160
148
|
icon.className = 'jux-menu-icon';
|
|
161
|
-
icon.
|
|
149
|
+
icon.appendChild(renderIcon(item.icon));
|
|
162
150
|
button.appendChild(icon);
|
|
163
151
|
}
|
|
164
152
|
|
|
@@ -169,13 +157,15 @@ export class Menu {
|
|
|
169
157
|
|
|
170
158
|
menuItem.appendChild(button);
|
|
171
159
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
160
|
+
// ✅ Fire both the item's click handler AND the callback event
|
|
161
|
+
button.addEventListener('click', (e) => {
|
|
162
|
+
if (item.click) {
|
|
163
|
+
item.click();
|
|
164
|
+
}
|
|
165
|
+
this._triggerCallback('itemClick', { item, event: e });
|
|
166
|
+
});
|
|
176
167
|
}
|
|
177
168
|
|
|
178
|
-
// Nested items (submenu)
|
|
179
169
|
if (item.items && item.items.length > 0) {
|
|
180
170
|
const submenu = document.createElement('div');
|
|
181
171
|
submenu.className = 'jux-menu-submenu';
|
|
@@ -190,24 +180,13 @@ export class Menu {
|
|
|
190
180
|
return menuItem;
|
|
191
181
|
}
|
|
192
182
|
|
|
193
|
-
/*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
183
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
184
|
+
* RENDER
|
|
185
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
196
186
|
|
|
197
187
|
render(targetId?: string): this {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (targetId) {
|
|
201
|
-
const target = document.querySelector(targetId);
|
|
202
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
203
|
-
throw new Error(`Menu: Target element "${targetId}" not found`);
|
|
204
|
-
}
|
|
205
|
-
container = target;
|
|
206
|
-
} else {
|
|
207
|
-
container = getOrCreateContainer(this._id);
|
|
208
|
-
}
|
|
188
|
+
const container = this._setupContainer(targetId);
|
|
209
189
|
|
|
210
|
-
this.container = container;
|
|
211
190
|
const { items, orientation, style, class: className } = this.state;
|
|
212
191
|
|
|
213
192
|
const menu = document.createElement('nav');
|
|
@@ -226,29 +205,56 @@ export class Menu {
|
|
|
226
205
|
menu.appendChild(this._renderMenuItem(item));
|
|
227
206
|
});
|
|
228
207
|
|
|
229
|
-
|
|
230
|
-
return this;
|
|
231
|
-
}
|
|
208
|
+
this._wireStandardEvents(menu);
|
|
232
209
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
238
|
-
throw new Error('Menu.renderTo: Invalid component - not an object');
|
|
239
|
-
}
|
|
210
|
+
// Wire sync bindings
|
|
211
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
212
|
+
if (property === 'items') {
|
|
213
|
+
const transform = toComponent || ((v: any) => v);
|
|
240
214
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
215
|
+
stateObj.subscribe((val: any) => {
|
|
216
|
+
const transformed = transform(val);
|
|
217
|
+
this.state.items = transformed;
|
|
218
|
+
this._setActiveStates();
|
|
244
219
|
|
|
245
|
-
|
|
220
|
+
const existingItems = menu.querySelectorAll('.jux-menu-item');
|
|
221
|
+
existingItems.forEach(item => item.remove());
|
|
222
|
+
|
|
223
|
+
this.state.items.forEach(item => {
|
|
224
|
+
menu.appendChild(this._renderMenuItem(item));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
requestAnimationFrame(() => {
|
|
228
|
+
if ((window as any).lucide) {
|
|
229
|
+
(window as any).lucide.createIcons();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (property === 'orientation') {
|
|
235
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
236
|
+
|
|
237
|
+
stateObj.subscribe((val: any) => {
|
|
238
|
+
const transformed = transform(val);
|
|
239
|
+
menu.classList.remove(`jux-menu-${this.state.orientation}`);
|
|
240
|
+
this.state.orientation = transformed;
|
|
241
|
+
menu.classList.add(`jux-menu-${transformed}`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
container.appendChild(menu);
|
|
247
|
+
|
|
248
|
+
requestAnimationFrame(() => {
|
|
249
|
+
if ((window as any).lucide) {
|
|
250
|
+
(window as any).lucide.createIcons();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return this;
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
/**
|
|
250
|
-
* Factory helper
|
|
251
|
-
*/
|
|
252
258
|
export function menu(id: string, options: MenuOptions = {}): Menu {
|
|
253
259
|
return new Menu(id, options);
|
|
254
260
|
}
|
package/lib/components/modal.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
|
|
3
|
+
// Event definitions
|
|
4
|
+
const TRIGGER_EVENTS = [] as const;
|
|
5
|
+
const CALLBACK_EVENTS = ['open', 'close'] as const;
|
|
2
6
|
|
|
3
|
-
/**
|
|
4
|
-
* Modal component options
|
|
5
|
-
*/
|
|
6
7
|
export interface ModalOptions {
|
|
7
8
|
title?: string;
|
|
8
9
|
content?: string;
|
|
@@ -13,57 +14,46 @@ export interface ModalOptions {
|
|
|
13
14
|
class?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Modal component state
|
|
18
|
-
*/
|
|
19
17
|
type ModalState = {
|
|
20
18
|
title: string;
|
|
21
19
|
content: string;
|
|
22
20
|
showCloseButton: boolean;
|
|
23
21
|
closeOnBackdropClick: boolean;
|
|
24
22
|
size: string;
|
|
25
|
-
|
|
23
|
+
open: boolean;
|
|
26
24
|
style: string;
|
|
27
25
|
class: string;
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
* Usage:
|
|
34
|
-
* const modal = jux.modal('myModal', {
|
|
35
|
-
* title: 'Confirmation',
|
|
36
|
-
* content: 'Are you sure?',
|
|
37
|
-
* size: 'medium'
|
|
38
|
-
* });
|
|
39
|
-
* modal.render();
|
|
40
|
-
* modal.open();
|
|
41
|
-
*/
|
|
42
|
-
export class Modal {
|
|
43
|
-
state: ModalState;
|
|
44
|
-
container: HTMLElement | null = null;
|
|
45
|
-
_id: string;
|
|
46
|
-
id: string;
|
|
28
|
+
export class Modal extends BaseComponent<ModalState> {
|
|
29
|
+
private _overlay: HTMLElement | null = null;
|
|
47
30
|
|
|
48
31
|
constructor(id: string, options: ModalOptions = {}) {
|
|
49
|
-
|
|
50
|
-
this.id = id;
|
|
51
|
-
|
|
52
|
-
this.state = {
|
|
32
|
+
super(id, {
|
|
53
33
|
title: options.title ?? '',
|
|
54
34
|
content: options.content ?? '',
|
|
55
35
|
showCloseButton: options.showCloseButton ?? true,
|
|
56
36
|
closeOnBackdropClick: options.closeOnBackdropClick ?? true,
|
|
57
37
|
size: options.size ?? 'medium',
|
|
58
|
-
|
|
38
|
+
open: false,
|
|
59
39
|
style: options.style ?? '',
|
|
60
40
|
class: options.class ?? ''
|
|
61
|
-
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected getTriggerEvents(): readonly string[] {
|
|
45
|
+
return TRIGGER_EVENTS;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected getCallbackEvents(): readonly string[] {
|
|
49
|
+
return CALLBACK_EVENTS;
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
/*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
52
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
53
|
+
* FLUENT API
|
|
54
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
55
|
+
|
|
56
|
+
// ✅ Inherited from BaseComponent
|
|
67
57
|
|
|
68
58
|
title(value: string): this {
|
|
69
59
|
this.state.title = value;
|
|
@@ -90,156 +80,160 @@ export class Modal {
|
|
|
90
80
|
return this;
|
|
91
81
|
}
|
|
92
82
|
|
|
93
|
-
style(value: string): this {
|
|
94
|
-
this.state.style = value;
|
|
95
|
-
return this;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
class(value: string): this {
|
|
99
|
-
this.state.class = value;
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* -------------------------
|
|
104
|
-
* Modal controls
|
|
105
|
-
* ------------------------- */
|
|
106
|
-
|
|
107
83
|
open(): this {
|
|
108
|
-
this.state.
|
|
109
|
-
if (this.
|
|
110
|
-
|
|
111
|
-
if (modalEl) {
|
|
112
|
-
modalEl.classList.add('jux-modal-open');
|
|
113
|
-
}
|
|
84
|
+
this.state.open = true;
|
|
85
|
+
if (this._overlay) {
|
|
86
|
+
this._overlay.style.display = 'flex';
|
|
114
87
|
}
|
|
88
|
+
// 🎯 Fire the open callback event
|
|
89
|
+
this._triggerCallback('open');
|
|
115
90
|
return this;
|
|
116
91
|
}
|
|
117
92
|
|
|
118
93
|
close(): this {
|
|
119
|
-
this.state.
|
|
120
|
-
if (this.
|
|
121
|
-
|
|
122
|
-
if (modalEl) {
|
|
123
|
-
modalEl.classList.remove('jux-modal-open');
|
|
124
|
-
}
|
|
94
|
+
this.state.open = false;
|
|
95
|
+
if (this._overlay) {
|
|
96
|
+
this._overlay.style.display = 'none';
|
|
125
97
|
}
|
|
98
|
+
// 🎯 Fire the close callback event
|
|
99
|
+
this._triggerCallback('close');
|
|
126
100
|
return this;
|
|
127
101
|
}
|
|
128
102
|
|
|
129
|
-
/*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
103
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
104
|
+
* RENDER
|
|
105
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
132
106
|
|
|
133
107
|
render(targetId?: string): this {
|
|
134
|
-
|
|
108
|
+
const container = this._setupContainer(targetId);
|
|
135
109
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
139
|
-
throw new Error(`Modal: Target element "${targetId}" not found`);
|
|
140
|
-
}
|
|
141
|
-
container = target;
|
|
142
|
-
} else {
|
|
143
|
-
container = getOrCreateContainer(this._id);
|
|
144
|
-
}
|
|
110
|
+
const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
|
|
111
|
+
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
145
112
|
|
|
146
|
-
|
|
147
|
-
|
|
113
|
+
const overlay = document.createElement('div');
|
|
114
|
+
overlay.className = 'jux-modal-overlay';
|
|
115
|
+
overlay.id = this._id;
|
|
116
|
+
overlay.style.display = open ? 'flex' : 'none';
|
|
117
|
+
if (className) overlay.className += ` ${className}`;
|
|
118
|
+
if (style) overlay.setAttribute('style', style);
|
|
119
|
+
this._overlay = overlay;
|
|
148
120
|
|
|
149
|
-
// Modal backdrop
|
|
150
121
|
const modal = document.createElement('div');
|
|
151
122
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
152
|
-
modal.id = this._id;
|
|
153
123
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
124
|
+
if (showClose) {
|
|
125
|
+
const closeButton = document.createElement('button');
|
|
126
|
+
closeButton.className = 'jux-modal-close';
|
|
127
|
+
closeButton.innerHTML = '×';
|
|
128
|
+
modal.appendChild(closeButton);
|
|
156
129
|
}
|
|
157
130
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
131
|
+
if (title) {
|
|
132
|
+
const header = document.createElement('div');
|
|
133
|
+
header.className = 'jux-modal-header';
|
|
134
|
+
header.textContent = title;
|
|
135
|
+
modal.appendChild(header);
|
|
160
136
|
}
|
|
161
137
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
138
|
+
const body = document.createElement('div');
|
|
139
|
+
body.className = 'jux-modal-body';
|
|
140
|
+
body.innerHTML = content;
|
|
141
|
+
modal.appendChild(body);
|
|
165
142
|
|
|
166
|
-
|
|
167
|
-
const dialog = document.createElement('div');
|
|
168
|
-
dialog.className = 'jux-modal-dialog';
|
|
143
|
+
overlay.appendChild(modal);
|
|
169
144
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
145
|
+
if (!hasOpenSync) {
|
|
146
|
+
if (showClose) {
|
|
147
|
+
const closeButton = modal.querySelector('.jux-modal-close');
|
|
148
|
+
closeButton?.addEventListener('click', () => {
|
|
149
|
+
this.state.open = false;
|
|
150
|
+
overlay.style.display = 'none';
|
|
151
|
+
});
|
|
152
|
+
}
|
|
174
153
|
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
154
|
+
if (closeOnBackdrop) {
|
|
155
|
+
overlay.addEventListener('click', (e) => {
|
|
156
|
+
if (e.target === overlay) {
|
|
157
|
+
this.state.open = false;
|
|
158
|
+
overlay.style.display = 'none';
|
|
159
|
+
}
|
|
160
|
+
});
|
|
180
161
|
}
|
|
162
|
+
}
|
|
181
163
|
|
|
182
|
-
|
|
183
|
-
const closeBtn = document.createElement('button');
|
|
184
|
-
closeBtn.className = 'jux-modal-close';
|
|
185
|
-
closeBtn.innerHTML = '×';
|
|
186
|
-
header.appendChild(closeBtn);
|
|
164
|
+
this._wireStandardEvents(overlay);
|
|
187
165
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
166
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
167
|
+
if (property === 'open') {
|
|
168
|
+
const transformToState = toState || ((v: any) => Boolean(v));
|
|
169
|
+
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
191
170
|
|
|
192
|
-
|
|
193
|
-
}
|
|
171
|
+
let isUpdating = false;
|
|
194
172
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
173
|
+
stateObj.subscribe((val: any) => {
|
|
174
|
+
if (isUpdating) return;
|
|
175
|
+
const transformed = transformToComponent(val);
|
|
176
|
+
this.state.open = transformed;
|
|
177
|
+
overlay.style.display = transformed ? 'flex' : 'none';
|
|
178
|
+
});
|
|
200
179
|
|
|
201
|
-
|
|
202
|
-
|
|
180
|
+
if (showClose) {
|
|
181
|
+
const closeButton = modal.querySelector('.jux-modal-close');
|
|
182
|
+
closeButton?.addEventListener('click', () => {
|
|
183
|
+
if (isUpdating) return;
|
|
184
|
+
isUpdating = true;
|
|
203
185
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (e.target === modal) {
|
|
208
|
-
this.close();
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
186
|
+
this.state.open = false;
|
|
187
|
+
overlay.style.display = 'none';
|
|
188
|
+
stateObj.set(transformToState(false));
|
|
212
189
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.close();
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
document.addEventListener('keydown', escapeHandler);
|
|
190
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
220
193
|
|
|
221
|
-
|
|
222
|
-
|
|
194
|
+
if (closeOnBackdrop) {
|
|
195
|
+
overlay.addEventListener('click', (e) => {
|
|
196
|
+
if (e.target === overlay) {
|
|
197
|
+
if (isUpdating) return;
|
|
198
|
+
isUpdating = true;
|
|
223
199
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
renderTo(juxComponent: any): this {
|
|
228
|
-
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
229
|
-
throw new Error('Modal.renderTo: Invalid component - not an object');
|
|
230
|
-
}
|
|
200
|
+
this.state.open = false;
|
|
201
|
+
overlay.style.display = 'none';
|
|
202
|
+
stateObj.set(transformToState(false));
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
204
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (property === 'content') {
|
|
210
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
211
|
+
|
|
212
|
+
stateObj.subscribe((val: any) => {
|
|
213
|
+
const transformed = transform(val);
|
|
214
|
+
body.innerHTML = transformed;
|
|
215
|
+
this.state.content = transformed;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else if (property === 'title') {
|
|
219
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
220
|
+
|
|
221
|
+
stateObj.subscribe((val: any) => {
|
|
222
|
+
const transformed = transform(val);
|
|
223
|
+
const header = modal.querySelector('.jux-modal-header');
|
|
224
|
+
if (header) {
|
|
225
|
+
header.textContent = transformed;
|
|
226
|
+
}
|
|
227
|
+
this.state.title = transformed;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
235
231
|
|
|
236
|
-
|
|
232
|
+
container.appendChild(overlay);
|
|
233
|
+
return this;
|
|
237
234
|
}
|
|
238
235
|
}
|
|
239
236
|
|
|
240
|
-
/**
|
|
241
|
-
* Factory helper
|
|
242
|
-
*/
|
|
243
237
|
export function modal(id: string, options: ModalOptions = {}): Modal {
|
|
244
238
|
return new Modal(id, options);
|
|
245
239
|
}
|