juxscript 1.0.20 → 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 +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- 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/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -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 -1
- 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 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- 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,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
2
|
import { renderIcon } from './icons.js';
|
|
3
3
|
import { req } from './req.js';
|
|
4
|
-
import { State } from '../reactivity/state.js';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Event definitions
|
|
6
|
+
const TRIGGER_EVENTS = [] as const;
|
|
7
|
+
const CALLBACK_EVENTS = ['itemClick'] as const; // ✅ Fire when menu item is clicked
|
|
8
|
+
|
|
9
9
|
export interface MenuItem {
|
|
10
10
|
label: string;
|
|
11
11
|
href?: string;
|
|
@@ -13,11 +13,9 @@ export interface MenuItem {
|
|
|
13
13
|
icon?: string;
|
|
14
14
|
items?: MenuItem[];
|
|
15
15
|
active?: boolean;
|
|
16
|
+
itemClass?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
* Menu component options
|
|
20
|
-
*/
|
|
21
19
|
export interface MenuOptions {
|
|
22
20
|
items?: MenuItem[];
|
|
23
21
|
orientation?: 'vertical' | 'horizontal';
|
|
@@ -25,9 +23,6 @@ export interface MenuOptions {
|
|
|
25
23
|
class?: string;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
/**
|
|
29
|
-
* Menu component state
|
|
30
|
-
*/
|
|
31
26
|
type MenuState = {
|
|
32
27
|
items: MenuItem[];
|
|
33
28
|
orientation: string;
|
|
@@ -35,56 +30,30 @@ type MenuState = {
|
|
|
35
30
|
class: string;
|
|
36
31
|
};
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
* Menu component
|
|
40
|
-
*
|
|
41
|
-
* Usage:
|
|
42
|
-
* const menu = jux.menu('myMenu', {
|
|
43
|
-
* orientation: 'vertical',
|
|
44
|
-
* items: [
|
|
45
|
-
* { label: 'Home', href: '/' },
|
|
46
|
-
* { label: 'About', href: '/about' }
|
|
47
|
-
* ]
|
|
48
|
-
* });
|
|
49
|
-
* menu.render();
|
|
50
|
-
*
|
|
51
|
-
* Active states are automatically set based on current URL
|
|
52
|
-
*/
|
|
53
|
-
export class Menu {
|
|
54
|
-
state: MenuState;
|
|
55
|
-
container: HTMLElement | null = null;
|
|
56
|
-
_id: string;
|
|
57
|
-
id: string;
|
|
58
|
-
|
|
59
|
-
// Store bind() instructions (DOM events only)
|
|
60
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
61
|
-
|
|
62
|
-
// Store sync() instructions (state synchronization)
|
|
63
|
-
private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
|
|
64
|
-
|
|
33
|
+
export class Menu extends BaseComponent<MenuState> {
|
|
65
34
|
constructor(id: string, options: MenuOptions = {}) {
|
|
66
|
-
|
|
67
|
-
this.id = id;
|
|
68
|
-
|
|
69
|
-
this.state = {
|
|
35
|
+
super(id, {
|
|
70
36
|
items: options.items ?? [],
|
|
71
37
|
orientation: options.orientation ?? 'vertical',
|
|
72
38
|
style: options.style ?? '',
|
|
73
39
|
class: options.class ?? ''
|
|
74
|
-
};
|
|
40
|
+
});
|
|
75
41
|
|
|
76
|
-
// Auto-set active state based on current path
|
|
77
42
|
this._setActiveStates();
|
|
78
43
|
}
|
|
79
44
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
45
|
+
protected getTriggerEvents(): readonly string[] {
|
|
46
|
+
return TRIGGER_EVENTS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected getCallbackEvents(): readonly string[] {
|
|
50
|
+
return CALLBACK_EVENTS;
|
|
51
|
+
}
|
|
52
|
+
|
|
83
53
|
private _setActiveStates(): void {
|
|
84
54
|
this.state.items = this.state.items.map(item => ({
|
|
85
55
|
...item,
|
|
86
56
|
active: item.href ? req.isActiveNavItem(item.href) : false,
|
|
87
|
-
// Recursively set active for subitems
|
|
88
57
|
items: item.items?.map(subItem => ({
|
|
89
58
|
...subItem,
|
|
90
59
|
active: subItem.href ? req.isActiveNavItem(subItem.href) : false
|
|
@@ -92,34 +61,11 @@ export class Menu {
|
|
|
92
61
|
}));
|
|
93
62
|
}
|
|
94
63
|
|
|
95
|
-
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*/
|
|
99
|
-
bind(event: string, handler: Function): this {
|
|
100
|
-
this._bindings.push({ event, handler });
|
|
101
|
-
return this;
|
|
102
|
-
}
|
|
64
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
65
|
+
* FLUENT API
|
|
66
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
103
67
|
|
|
104
|
-
|
|
105
|
-
* Two-way sync with state (stores for wiring in render)
|
|
106
|
-
*
|
|
107
|
-
* @param property - Component property to sync ('items', 'orientation', etc)
|
|
108
|
-
* @param stateObj - State object to sync with
|
|
109
|
-
* @param toState - Optional transform function when going from component to state
|
|
110
|
-
* @param toComponent - Optional transform function when going from state to component
|
|
111
|
-
*/
|
|
112
|
-
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
113
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
114
|
-
throw new Error(`Menu.sync: Expected a State object for property "${property}"`);
|
|
115
|
-
}
|
|
116
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
117
|
-
return this;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/* -------------------------
|
|
121
|
-
* Fluent API
|
|
122
|
-
* ------------------------- */
|
|
68
|
+
// ✅ Inherited from BaseComponent
|
|
123
69
|
|
|
124
70
|
items(value: MenuItem[]): this {
|
|
125
71
|
this.state.items = value;
|
|
@@ -133,30 +79,35 @@ export class Menu {
|
|
|
133
79
|
return this;
|
|
134
80
|
}
|
|
135
81
|
|
|
136
|
-
|
|
137
|
-
this.state.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}));
|
|
143
91
|
return this;
|
|
144
92
|
}
|
|
145
93
|
|
|
146
|
-
|
|
147
|
-
this.state.
|
|
94
|
+
orientation(value: 'vertical' | 'horizontal'): this {
|
|
95
|
+
this.state.orientation = value;
|
|
148
96
|
return this;
|
|
149
97
|
}
|
|
150
98
|
|
|
151
|
-
/*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
99
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
100
|
+
* HELPERS
|
|
101
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
154
102
|
|
|
155
103
|
private _renderMenuItem(item: MenuItem): HTMLElement {
|
|
156
104
|
const menuItem = document.createElement('div');
|
|
157
105
|
menuItem.className = 'jux-menu-item';
|
|
158
106
|
|
|
159
|
-
|
|
107
|
+
if (item.itemClass) {
|
|
108
|
+
menuItem.className += ` ${item.itemClass}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
160
111
|
if (item.active) {
|
|
161
112
|
menuItem.classList.add('jux-menu-item-active');
|
|
162
113
|
}
|
|
@@ -166,7 +117,6 @@ export class Menu {
|
|
|
166
117
|
link.className = 'jux-menu-link';
|
|
167
118
|
link.href = item.href;
|
|
168
119
|
|
|
169
|
-
// Add active class to link if item is active
|
|
170
120
|
if (item.active) {
|
|
171
121
|
link.classList.add('jux-menu-link-active');
|
|
172
122
|
}
|
|
@@ -174,8 +124,7 @@ export class Menu {
|
|
|
174
124
|
if (item.icon) {
|
|
175
125
|
const icon = document.createElement('span');
|
|
176
126
|
icon.className = 'jux-menu-icon';
|
|
177
|
-
|
|
178
|
-
icon.appendChild(iconElement);
|
|
127
|
+
icon.appendChild(renderIcon(item.icon));
|
|
179
128
|
link.appendChild(icon);
|
|
180
129
|
}
|
|
181
130
|
|
|
@@ -184,6 +133,11 @@ export class Menu {
|
|
|
184
133
|
label.textContent = item.label;
|
|
185
134
|
link.appendChild(label);
|
|
186
135
|
|
|
136
|
+
// ✅ Fire itemClick callback when link is clicked
|
|
137
|
+
link.addEventListener('click', (e) => {
|
|
138
|
+
this._triggerCallback('itemClick', { item, event: e });
|
|
139
|
+
});
|
|
140
|
+
|
|
187
141
|
menuItem.appendChild(link);
|
|
188
142
|
} else {
|
|
189
143
|
const button = document.createElement('button');
|
|
@@ -192,8 +146,7 @@ export class Menu {
|
|
|
192
146
|
if (item.icon) {
|
|
193
147
|
const icon = document.createElement('span');
|
|
194
148
|
icon.className = 'jux-menu-icon';
|
|
195
|
-
|
|
196
|
-
icon.appendChild(iconElement);
|
|
149
|
+
icon.appendChild(renderIcon(item.icon));
|
|
197
150
|
button.appendChild(icon);
|
|
198
151
|
}
|
|
199
152
|
|
|
@@ -204,13 +157,15 @@ export class Menu {
|
|
|
204
157
|
|
|
205
158
|
menuItem.appendChild(button);
|
|
206
159
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
});
|
|
211
167
|
}
|
|
212
168
|
|
|
213
|
-
// Nested items (submenu)
|
|
214
169
|
if (item.items && item.items.length > 0) {
|
|
215
170
|
const submenu = document.createElement('div');
|
|
216
171
|
submenu.className = 'jux-menu-submenu';
|
|
@@ -225,30 +180,15 @@ export class Menu {
|
|
|
225
180
|
return menuItem;
|
|
226
181
|
}
|
|
227
182
|
|
|
228
|
-
/*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
183
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
184
|
+
* RENDER
|
|
185
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
231
186
|
|
|
232
187
|
render(targetId?: string): this {
|
|
233
|
-
|
|
234
|
-
let container: HTMLElement;
|
|
188
|
+
const container = this._setupContainer(targetId);
|
|
235
189
|
|
|
236
|
-
if (targetId) {
|
|
237
|
-
const target = document.querySelector(targetId);
|
|
238
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
239
|
-
throw new Error(`Menu: Target element "${targetId}" not found`);
|
|
240
|
-
}
|
|
241
|
-
container = target;
|
|
242
|
-
} else {
|
|
243
|
-
container = getOrCreateContainer(this._id);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this.container = container;
|
|
247
|
-
|
|
248
|
-
// === 2. PREPARE: Destructure state ===
|
|
249
190
|
const { items, orientation, style, class: className } = this.state;
|
|
250
191
|
|
|
251
|
-
// === 3. BUILD: Create DOM elements ===
|
|
252
192
|
const menu = document.createElement('nav');
|
|
253
193
|
menu.className = `jux-menu jux-menu-${orientation}`;
|
|
254
194
|
menu.id = this._id;
|
|
@@ -265,25 +205,18 @@ export class Menu {
|
|
|
265
205
|
menu.appendChild(this._renderMenuItem(item));
|
|
266
206
|
});
|
|
267
207
|
|
|
268
|
-
|
|
208
|
+
this._wireStandardEvents(menu);
|
|
269
209
|
|
|
270
|
-
// Wire
|
|
271
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
272
|
-
menu.addEventListener(event, handler as EventListener);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Wire up sync bindings (from .sync() calls)
|
|
210
|
+
// Wire sync bindings
|
|
276
211
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
277
212
|
if (property === 'items') {
|
|
278
|
-
|
|
279
|
-
const transformToComponent = toComponent || ((v: any) => v);
|
|
213
|
+
const transform = toComponent || ((v: any) => v);
|
|
280
214
|
|
|
281
215
|
stateObj.subscribe((val: any) => {
|
|
282
|
-
const transformed =
|
|
216
|
+
const transformed = transform(val);
|
|
283
217
|
this.state.items = transformed;
|
|
284
218
|
this._setActiveStates();
|
|
285
219
|
|
|
286
|
-
// Re-render menu items
|
|
287
220
|
const existingItems = menu.querySelectorAll('.jux-menu-item');
|
|
288
221
|
existingItems.forEach(item => item.remove());
|
|
289
222
|
|
|
@@ -291,7 +224,6 @@ export class Menu {
|
|
|
291
224
|
menu.appendChild(this._renderMenuItem(item));
|
|
292
225
|
});
|
|
293
226
|
|
|
294
|
-
// Re-trigger icon rendering
|
|
295
227
|
requestAnimationFrame(() => {
|
|
296
228
|
if ((window as any).lucide) {
|
|
297
229
|
(window as any).lucide.createIcons();
|
|
@@ -300,11 +232,10 @@ export class Menu {
|
|
|
300
232
|
});
|
|
301
233
|
}
|
|
302
234
|
else if (property === 'orientation') {
|
|
303
|
-
|
|
304
|
-
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
235
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
305
236
|
|
|
306
237
|
stateObj.subscribe((val: any) => {
|
|
307
|
-
const transformed =
|
|
238
|
+
const transformed = transform(val);
|
|
308
239
|
menu.classList.remove(`jux-menu-${this.state.orientation}`);
|
|
309
240
|
this.state.orientation = transformed;
|
|
310
241
|
menu.classList.add(`jux-menu-${transformed}`);
|
|
@@ -312,10 +243,8 @@ export class Menu {
|
|
|
312
243
|
}
|
|
313
244
|
});
|
|
314
245
|
|
|
315
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
316
246
|
container.appendChild(menu);
|
|
317
247
|
|
|
318
|
-
// Trigger Lucide icon rendering
|
|
319
248
|
requestAnimationFrame(() => {
|
|
320
249
|
if ((window as any).lucide) {
|
|
321
250
|
(window as any).lucide.createIcons();
|
|
@@ -324,26 +253,8 @@ export class Menu {
|
|
|
324
253
|
|
|
325
254
|
return this;
|
|
326
255
|
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Render to another Jux component's container
|
|
330
|
-
*/
|
|
331
|
-
renderTo(juxComponent: any): this {
|
|
332
|
-
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
333
|
-
throw new Error('Menu.renderTo: Invalid component - not an object');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
337
|
-
throw new Error('Menu.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return this.render(`#${juxComponent._id}`);
|
|
341
|
-
}
|
|
342
256
|
}
|
|
343
257
|
|
|
344
|
-
/**
|
|
345
|
-
* Factory helper
|
|
346
|
-
*/
|
|
347
258
|
export function menu(id: string, options: MenuOptions = {}): Menu {
|
|
348
259
|
return new Menu(id, options);
|
|
349
260
|
}
|
package/lib/components/modal.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
4
6
|
|
|
5
|
-
/**
|
|
6
|
-
* Modal component options
|
|
7
|
-
*/
|
|
8
7
|
export interface ModalOptions {
|
|
9
8
|
title?: string;
|
|
10
9
|
content?: string;
|
|
@@ -15,68 +14,46 @@ export interface ModalOptions {
|
|
|
15
14
|
class?: string;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
* Modal component state
|
|
20
|
-
*/
|
|
21
17
|
type ModalState = {
|
|
22
18
|
title: string;
|
|
23
19
|
content: string;
|
|
24
20
|
showCloseButton: boolean;
|
|
25
21
|
closeOnBackdropClick: boolean;
|
|
26
22
|
size: string;
|
|
27
|
-
isOpen: boolean;
|
|
28
23
|
open: boolean;
|
|
29
24
|
style: string;
|
|
30
25
|
class: string;
|
|
31
26
|
};
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* Usage:
|
|
37
|
-
* const modal = jux.modal('myModal', {
|
|
38
|
-
* title: 'Confirmation',
|
|
39
|
-
* content: 'Are you sure?',
|
|
40
|
-
* size: 'medium'
|
|
41
|
-
* });
|
|
42
|
-
* modal.render();
|
|
43
|
-
* modal.open();
|
|
44
|
-
*/
|
|
45
|
-
export class Modal {
|
|
46
|
-
state: ModalState;
|
|
47
|
-
container: HTMLElement | null = null;
|
|
48
|
-
_id: string;
|
|
49
|
-
id: string;
|
|
50
|
-
|
|
51
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
52
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
53
|
-
private _syncBindings: Array<{
|
|
54
|
-
property: string,
|
|
55
|
-
stateObj: State<any>,
|
|
56
|
-
toState?: Function,
|
|
57
|
-
toComponent?: Function
|
|
58
|
-
}> = [];
|
|
28
|
+
export class Modal extends BaseComponent<ModalState> {
|
|
29
|
+
private _overlay: HTMLElement | null = null;
|
|
59
30
|
|
|
60
31
|
constructor(id: string, options: ModalOptions = {}) {
|
|
61
|
-
|
|
62
|
-
this.id = id;
|
|
63
|
-
|
|
64
|
-
this.state = {
|
|
32
|
+
super(id, {
|
|
65
33
|
title: options.title ?? '',
|
|
66
34
|
content: options.content ?? '',
|
|
67
35
|
showCloseButton: options.showCloseButton ?? true,
|
|
68
36
|
closeOnBackdropClick: options.closeOnBackdropClick ?? true,
|
|
69
37
|
size: options.size ?? 'medium',
|
|
70
|
-
isOpen: false,
|
|
71
38
|
open: false,
|
|
72
39
|
style: options.style ?? '',
|
|
73
40
|
class: options.class ?? ''
|
|
74
|
-
};
|
|
41
|
+
});
|
|
75
42
|
}
|
|
76
43
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
44
|
+
protected getTriggerEvents(): readonly string[] {
|
|
45
|
+
return TRIGGER_EVENTS;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected getCallbackEvents(): readonly string[] {
|
|
49
|
+
return CALLBACK_EVENTS;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
53
|
+
* FLUENT API
|
|
54
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
55
|
+
|
|
56
|
+
// ✅ Inherited from BaseComponent
|
|
80
57
|
|
|
81
58
|
title(value: string): this {
|
|
82
59
|
this.state.title = value;
|
|
@@ -103,84 +80,43 @@ export class Modal {
|
|
|
103
80
|
return this;
|
|
104
81
|
}
|
|
105
82
|
|
|
106
|
-
style(value: string): this {
|
|
107
|
-
this.state.style = value;
|
|
108
|
-
return this;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
class(value: string): this {
|
|
112
|
-
this.state.class = value;
|
|
113
|
-
return this;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* -------------------------
|
|
117
|
-
* Modal controls
|
|
118
|
-
* ------------------------- */
|
|
119
|
-
|
|
120
83
|
open(): this {
|
|
121
|
-
this.state.
|
|
122
|
-
if (this.
|
|
123
|
-
|
|
124
|
-
if (modalEl) {
|
|
125
|
-
modalEl.classList.add('jux-modal-open');
|
|
126
|
-
}
|
|
84
|
+
this.state.open = true;
|
|
85
|
+
if (this._overlay) {
|
|
86
|
+
this._overlay.style.display = 'flex';
|
|
127
87
|
}
|
|
88
|
+
// 🎯 Fire the open callback event
|
|
89
|
+
this._triggerCallback('open');
|
|
128
90
|
return this;
|
|
129
91
|
}
|
|
130
92
|
|
|
131
93
|
close(): this {
|
|
132
|
-
this.state.
|
|
133
|
-
if (this.
|
|
134
|
-
|
|
135
|
-
if (modalEl) {
|
|
136
|
-
modalEl.classList.remove('jux-modal-open');
|
|
137
|
-
}
|
|
94
|
+
this.state.open = false;
|
|
95
|
+
if (this._overlay) {
|
|
96
|
+
this._overlay.style.display = 'none';
|
|
138
97
|
}
|
|
98
|
+
// 🎯 Fire the close callback event
|
|
99
|
+
this._triggerCallback('close');
|
|
139
100
|
return this;
|
|
140
101
|
}
|
|
141
102
|
|
|
142
|
-
/*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
bind(event: string, handler: Function): this {
|
|
147
|
-
this._bindings.push({ event, handler });
|
|
148
|
-
return this;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
152
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
153
|
-
throw new Error(`Modal.sync: Expected a State object for property "${property}"`);
|
|
154
|
-
}
|
|
155
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
156
|
-
return this;
|
|
157
|
-
}
|
|
103
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
104
|
+
* RENDER
|
|
105
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
158
106
|
|
|
159
107
|
render(targetId?: string): this {
|
|
160
|
-
|
|
161
|
-
let container: HTMLElement;
|
|
162
|
-
if (targetId) {
|
|
163
|
-
const target = document.querySelector(targetId);
|
|
164
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
165
|
-
throw new Error(`Modal: Target "${targetId}" not found`);
|
|
166
|
-
}
|
|
167
|
-
container = target;
|
|
168
|
-
} else {
|
|
169
|
-
container = getOrCreateContainer(this._id);
|
|
170
|
-
}
|
|
171
|
-
this.container = container;
|
|
108
|
+
const container = this._setupContainer(targetId);
|
|
172
109
|
|
|
173
|
-
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
174
110
|
const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
|
|
175
111
|
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
176
112
|
|
|
177
|
-
// === 3. BUILD: Create DOM elements ===
|
|
178
113
|
const overlay = document.createElement('div');
|
|
179
114
|
overlay.className = 'jux-modal-overlay';
|
|
180
115
|
overlay.id = this._id;
|
|
181
116
|
overlay.style.display = open ? 'flex' : 'none';
|
|
182
117
|
if (className) overlay.className += ` ${className}`;
|
|
183
118
|
if (style) overlay.setAttribute('style', style);
|
|
119
|
+
this._overlay = overlay;
|
|
184
120
|
|
|
185
121
|
const modal = document.createElement('div');
|
|
186
122
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
@@ -206,9 +142,6 @@ export class Modal {
|
|
|
206
142
|
|
|
207
143
|
overlay.appendChild(modal);
|
|
208
144
|
|
|
209
|
-
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
210
|
-
|
|
211
|
-
// Default close behavior (only if NOT using sync)
|
|
212
145
|
if (!hasOpenSync) {
|
|
213
146
|
if (showClose) {
|
|
214
147
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
@@ -228,12 +161,8 @@ export class Modal {
|
|
|
228
161
|
}
|
|
229
162
|
}
|
|
230
163
|
|
|
231
|
-
|
|
232
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
233
|
-
overlay.addEventListener(event, handler as EventListener);
|
|
234
|
-
});
|
|
164
|
+
this._wireStandardEvents(overlay);
|
|
235
165
|
|
|
236
|
-
// Wire sync bindings from .sync() calls
|
|
237
166
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
238
167
|
if (property === 'open') {
|
|
239
168
|
const transformToState = toState || ((v: any) => Boolean(v));
|
|
@@ -241,7 +170,6 @@ export class Modal {
|
|
|
241
170
|
|
|
242
171
|
let isUpdating = false;
|
|
243
172
|
|
|
244
|
-
// State → Component
|
|
245
173
|
stateObj.subscribe((val: any) => {
|
|
246
174
|
if (isUpdating) return;
|
|
247
175
|
const transformed = transformToComponent(val);
|
|
@@ -249,7 +177,6 @@ export class Modal {
|
|
|
249
177
|
overlay.style.display = transformed ? 'flex' : 'none';
|
|
250
178
|
});
|
|
251
179
|
|
|
252
|
-
// Component → State (close button, backdrop)
|
|
253
180
|
if (showClose) {
|
|
254
181
|
const closeButton = modal.querySelector('.jux-modal-close');
|
|
255
182
|
closeButton?.addEventListener('click', () => {
|
|
@@ -280,19 +207,19 @@ export class Modal {
|
|
|
280
207
|
}
|
|
281
208
|
}
|
|
282
209
|
else if (property === 'content') {
|
|
283
|
-
const
|
|
210
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
284
211
|
|
|
285
212
|
stateObj.subscribe((val: any) => {
|
|
286
|
-
const transformed =
|
|
213
|
+
const transformed = transform(val);
|
|
287
214
|
body.innerHTML = transformed;
|
|
288
215
|
this.state.content = transformed;
|
|
289
216
|
});
|
|
290
217
|
}
|
|
291
218
|
else if (property === 'title') {
|
|
292
|
-
const
|
|
219
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
293
220
|
|
|
294
221
|
stateObj.subscribe((val: any) => {
|
|
295
|
-
const transformed =
|
|
222
|
+
const transformed = transform(val);
|
|
296
223
|
const header = modal.querySelector('.jux-modal-header');
|
|
297
224
|
if (header) {
|
|
298
225
|
header.textContent = transformed;
|
|
@@ -302,25 +229,11 @@ export class Modal {
|
|
|
302
229
|
}
|
|
303
230
|
});
|
|
304
231
|
|
|
305
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
306
232
|
container.appendChild(overlay);
|
|
307
233
|
return this;
|
|
308
234
|
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Render to another Jux component's container
|
|
312
|
-
*/
|
|
313
|
-
renderTo(juxComponent: any): this {
|
|
314
|
-
if (!juxComponent?._id) {
|
|
315
|
-
throw new Error('Modal.renderTo: Invalid component');
|
|
316
|
-
}
|
|
317
|
-
return this.render(`#${juxComponent._id}`);
|
|
318
|
-
}
|
|
319
235
|
}
|
|
320
236
|
|
|
321
|
-
/**
|
|
322
|
-
* Factory helper
|
|
323
|
-
*/
|
|
324
237
|
export function modal(id: string, options: ModalOptions = {}): Modal {
|
|
325
238
|
return new Modal(id, options);
|
|
326
239
|
}
|