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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { BaseComponent } from './BaseComponent.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base state interface for all form inputs
|
|
5
|
+
*/
|
|
6
|
+
export interface FormInputState extends Record<string, any> {
|
|
7
|
+
label: string;
|
|
8
|
+
required: boolean;
|
|
9
|
+
disabled: boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
style: string;
|
|
12
|
+
class: string;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Abstract base class for all form input components
|
|
18
|
+
* Extends BaseComponent with form-specific functionality
|
|
19
|
+
*/
|
|
20
|
+
export abstract class FormInput<TState extends FormInputState> extends BaseComponent<TState> {
|
|
21
|
+
protected _inputElement: HTMLElement | null = null;
|
|
22
|
+
protected _labelElement: HTMLLabelElement | null = null;
|
|
23
|
+
protected _errorElement: HTMLElement | null = null;
|
|
24
|
+
protected _onValidate?: (value: any) => boolean | string;
|
|
25
|
+
protected _hasBeenValidated: boolean = false; // NEW: Track if user has submitted/validated
|
|
26
|
+
|
|
27
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
28
|
+
* ABSTRACT METHODS (Child must implement)
|
|
29
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current value of the input
|
|
33
|
+
*/
|
|
34
|
+
abstract getValue(): any;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the value of the input
|
|
38
|
+
*/
|
|
39
|
+
abstract setValue(value: any): this;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the actual input element (input, select, textarea, etc.)
|
|
43
|
+
*/
|
|
44
|
+
protected abstract _buildInputElement(): HTMLElement;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate the current value
|
|
48
|
+
*/
|
|
49
|
+
protected abstract _validateValue(value: any): boolean | string;
|
|
50
|
+
|
|
51
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
52
|
+
* COMMON FORM INPUT API
|
|
53
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
54
|
+
|
|
55
|
+
label(value: string): this {
|
|
56
|
+
this.state.label = value;
|
|
57
|
+
if (this._labelElement) {
|
|
58
|
+
this._labelElement.textContent = value;
|
|
59
|
+
}
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
required(value: boolean): this {
|
|
64
|
+
this.state.required = value;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
name(value: string): this {
|
|
69
|
+
this.state.name = value;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onValidate(handler: (value: any) => boolean | string): this {
|
|
74
|
+
this._onValidate = handler;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
79
|
+
* VALIDATION
|
|
80
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate the current value and show/hide errors
|
|
84
|
+
*/
|
|
85
|
+
validate(): boolean {
|
|
86
|
+
this._hasBeenValidated = true; // Mark as validated
|
|
87
|
+
const value = this.getValue();
|
|
88
|
+
const result = this._validateValue(value);
|
|
89
|
+
|
|
90
|
+
if (result === true) {
|
|
91
|
+
this._clearError();
|
|
92
|
+
return true;
|
|
93
|
+
} else {
|
|
94
|
+
this._showError(result as string);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if current value is valid without showing errors
|
|
101
|
+
*/
|
|
102
|
+
isValid(): boolean {
|
|
103
|
+
const value = this.getValue();
|
|
104
|
+
return this._validateValue(value) === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Show error message
|
|
109
|
+
*/
|
|
110
|
+
protected _showError(message: string): void {
|
|
111
|
+
if (this._errorElement) {
|
|
112
|
+
this._errorElement.textContent = message;
|
|
113
|
+
this._errorElement.style.display = 'block';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this._inputElement) {
|
|
117
|
+
this._inputElement.classList.add('jux-input-invalid');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.state.errorMessage = message;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clear error message
|
|
125
|
+
*/
|
|
126
|
+
protected _clearError(): void {
|
|
127
|
+
if (this._errorElement) {
|
|
128
|
+
this._errorElement.textContent = '';
|
|
129
|
+
this._errorElement.style.display = 'none';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this._inputElement) {
|
|
133
|
+
this._inputElement.classList.remove('jux-input-invalid');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.state.errorMessage = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
140
|
+
* COMMON RENDER HELPERS
|
|
141
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build label element
|
|
145
|
+
*/
|
|
146
|
+
protected _renderLabel(): HTMLLabelElement {
|
|
147
|
+
const { label, required } = this.state;
|
|
148
|
+
|
|
149
|
+
const labelEl = document.createElement('label');
|
|
150
|
+
labelEl.className = 'jux-input-label';
|
|
151
|
+
labelEl.htmlFor = `${this._id}-input`;
|
|
152
|
+
labelEl.textContent = label;
|
|
153
|
+
|
|
154
|
+
if (required) {
|
|
155
|
+
const requiredSpan = document.createElement('span');
|
|
156
|
+
requiredSpan.className = 'jux-input-required';
|
|
157
|
+
requiredSpan.textContent = ' *';
|
|
158
|
+
labelEl.appendChild(requiredSpan);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this._labelElement = labelEl;
|
|
162
|
+
return labelEl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build error element
|
|
167
|
+
*/
|
|
168
|
+
protected _renderError(): HTMLElement {
|
|
169
|
+
const errorEl = document.createElement('div');
|
|
170
|
+
errorEl.className = 'jux-input-error';
|
|
171
|
+
errorEl.id = `${this._id}-error`;
|
|
172
|
+
errorEl.style.display = 'none';
|
|
173
|
+
|
|
174
|
+
this._errorElement = errorEl;
|
|
175
|
+
return errorEl;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Wire up two-way sync for value property
|
|
180
|
+
*/
|
|
181
|
+
protected _wireFormSync(inputElement: HTMLElement, eventName: string = 'input'): void {
|
|
182
|
+
const valueSync = this._syncBindings.find(b => b.property === 'value');
|
|
183
|
+
|
|
184
|
+
if (valueSync) {
|
|
185
|
+
const { stateObj, toState, toComponent } = valueSync;
|
|
186
|
+
|
|
187
|
+
// Default transforms
|
|
188
|
+
const transformToState = toState || ((v: any) => v);
|
|
189
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
190
|
+
|
|
191
|
+
let isUpdating = false;
|
|
192
|
+
|
|
193
|
+
// State → Component
|
|
194
|
+
stateObj.subscribe((val: any) => {
|
|
195
|
+
if (isUpdating) return;
|
|
196
|
+
const transformed = transformToComponent(val);
|
|
197
|
+
this.setValue(transformed);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Component → State
|
|
201
|
+
inputElement.addEventListener(eventName, () => {
|
|
202
|
+
if (isUpdating) return;
|
|
203
|
+
isUpdating = true;
|
|
204
|
+
|
|
205
|
+
const value = this.getValue();
|
|
206
|
+
const transformed = transformToState(value);
|
|
207
|
+
this._clearError();
|
|
208
|
+
|
|
209
|
+
stateObj.set(transformed);
|
|
210
|
+
|
|
211
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
// Default behavior without sync
|
|
215
|
+
inputElement.addEventListener(eventName, () => {
|
|
216
|
+
this._clearError();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Only validate on blur IF the field has been validated before (e.g., after submit)
|
|
221
|
+
inputElement.addEventListener('blur', () => {
|
|
222
|
+
if (this._hasBeenValidated) {
|
|
223
|
+
this.validate();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Inject default form input styles
|
|
230
|
+
*/
|
|
231
|
+
protected _injectFormStyles(): void {
|
|
232
|
+
const styleId = 'jux-form-input-styles';
|
|
233
|
+
if (document.getElementById(styleId)) return;
|
|
234
|
+
|
|
235
|
+
const style = document.createElement('style');
|
|
236
|
+
style.id = styleId;
|
|
237
|
+
style.textContent = `
|
|
238
|
+
.jux-input {
|
|
239
|
+
margin-bottom: 16px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.jux-input-container {
|
|
243
|
+
position: relative;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.jux-input-with-icon .jux-input-element {
|
|
247
|
+
padding-left: 40px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.jux-input-icon {
|
|
251
|
+
position: absolute;
|
|
252
|
+
left: 12px;
|
|
253
|
+
top: 50%;
|
|
254
|
+
transform: translateY(-50%);
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: center;
|
|
258
|
+
color: #6b7280;
|
|
259
|
+
pointer-events: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.jux-input-icon svg {
|
|
263
|
+
width: 18px;
|
|
264
|
+
height: 18px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.jux-input-label {
|
|
268
|
+
display: block;
|
|
269
|
+
margin-bottom: 6px;
|
|
270
|
+
font-weight: 500;
|
|
271
|
+
color: #374151;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.jux-input-required {
|
|
275
|
+
color: #ef4444;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.jux-input-element {
|
|
279
|
+
width: 100%;
|
|
280
|
+
padding: 8px 12px;
|
|
281
|
+
border: 1px solid #d1d5db;
|
|
282
|
+
border-radius: 6px;
|
|
283
|
+
font-size: 14px;
|
|
284
|
+
transition: border-color 0.2s;
|
|
285
|
+
box-sizing: border-box;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.jux-input-element:focus {
|
|
289
|
+
outline: none;
|
|
290
|
+
border-color: #3b82f6;
|
|
291
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.jux-input-element:disabled {
|
|
295
|
+
background-color: #f3f4f6;
|
|
296
|
+
cursor: not-allowed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.jux-input-element.jux-input-invalid {
|
|
300
|
+
border-color: #ef4444;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.jux-input-element.jux-input-invalid:focus {
|
|
304
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.jux-input-error {
|
|
308
|
+
color: #ef4444;
|
|
309
|
+
font-size: 12px;
|
|
310
|
+
margin-top: 4px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.jux-input-counter {
|
|
314
|
+
text-align: right;
|
|
315
|
+
font-size: 12px;
|
|
316
|
+
color: #6b7280;
|
|
317
|
+
margin-top: 4px;
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
document.head.appendChild(style);
|
|
321
|
+
}
|
|
322
|
+
}
|
package/lib/components/button.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { State } from '../reactivity/state.js';
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
3
2
|
import { renderIcon } from './icons.js';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// Event definitions
|
|
5
|
+
const TRIGGER_EVENTS = [] as const;
|
|
6
|
+
const CALLBACK_EVENTS = ['click'] as const; // ✅ Button fires click events
|
|
7
|
+
|
|
8
8
|
export interface ButtonOptions {
|
|
9
9
|
label?: string;
|
|
10
10
|
icon?: string;
|
|
@@ -19,9 +19,6 @@ export interface ButtonOptions {
|
|
|
19
19
|
class?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Button component state
|
|
24
|
-
*/
|
|
25
22
|
type ButtonState = {
|
|
26
23
|
label: string;
|
|
27
24
|
icon: string;
|
|
@@ -36,31 +33,13 @@ type ButtonState = {
|
|
|
36
33
|
class: string;
|
|
37
34
|
};
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
export class Button {
|
|
43
|
-
state: ButtonState;
|
|
44
|
-
container: HTMLElement | null = null;
|
|
45
|
-
_id: string;
|
|
46
|
-
id: string;
|
|
47
|
-
|
|
48
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
49
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
50
|
-
private _syncBindings: Array<{
|
|
51
|
-
property: string,
|
|
52
|
-
stateObj: State<any>,
|
|
53
|
-
toState?: Function,
|
|
54
|
-
toComponent?: Function
|
|
55
|
-
}> = [];
|
|
36
|
+
export class Button extends BaseComponent<ButtonState> {
|
|
37
|
+
private _buttonElement: HTMLButtonElement | null = null; // ✅ Store button reference
|
|
56
38
|
|
|
57
39
|
constructor(id: string, options?: ButtonOptions) {
|
|
58
|
-
this._id = id;
|
|
59
|
-
this.id = id;
|
|
60
|
-
|
|
61
40
|
const opts = options || {};
|
|
62
41
|
|
|
63
|
-
|
|
42
|
+
super(id, {
|
|
64
43
|
label: opts.label ?? 'Button',
|
|
65
44
|
icon: opts.icon ?? '',
|
|
66
45
|
variant: opts.variant ?? 'primary',
|
|
@@ -72,12 +51,22 @@ export class Button {
|
|
|
72
51
|
type: opts.type ?? 'button',
|
|
73
52
|
style: opts.style ?? '',
|
|
74
53
|
class: opts.class ?? ''
|
|
75
|
-
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected getTriggerEvents(): readonly string[] {
|
|
58
|
+
return TRIGGER_EVENTS;
|
|
76
59
|
}
|
|
77
60
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
61
|
+
protected getCallbackEvents(): readonly string[] {
|
|
62
|
+
return CALLBACK_EVENTS;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
66
|
+
* FLUENT API
|
|
67
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
68
|
+
|
|
69
|
+
// ✅ Inherited from BaseComponent
|
|
81
70
|
|
|
82
71
|
label(value: string): this {
|
|
83
72
|
this.state.label = value;
|
|
@@ -99,16 +88,6 @@ export class Button {
|
|
|
99
88
|
return this;
|
|
100
89
|
}
|
|
101
90
|
|
|
102
|
-
disabled(value: boolean): this {
|
|
103
|
-
this.state.disabled = value;
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
loading(value: boolean): this {
|
|
108
|
-
this.state.loading = value;
|
|
109
|
-
return this;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
91
|
iconPosition(value: 'left' | 'right'): this {
|
|
113
92
|
this.state.iconPosition = value;
|
|
114
93
|
return this;
|
|
@@ -124,62 +103,30 @@ export class Button {
|
|
|
124
103
|
return this;
|
|
125
104
|
}
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
class(value: string): this {
|
|
133
|
-
this.state.class = value;
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
106
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
107
|
+
* RENDER
|
|
108
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
136
109
|
|
|
137
110
|
/**
|
|
138
|
-
*
|
|
139
|
-
* Stores handlers for wiring in render()
|
|
111
|
+
* Override visible() to update the actual button element
|
|
140
112
|
*/
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
113
|
+
visible(value: boolean): this {
|
|
114
|
+
(this.state as any).visible = value;
|
|
145
115
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*/
|
|
150
|
-
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
151
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
152
|
-
throw new Error(`Button.sync: Expected a State object for property "${property}"`);
|
|
116
|
+
// Update the button element directly, not the container
|
|
117
|
+
if (this._buttonElement) {
|
|
118
|
+
this._buttonElement.style.display = value ? '' : 'none';
|
|
153
119
|
}
|
|
154
|
-
|
|
120
|
+
|
|
155
121
|
return this;
|
|
156
122
|
}
|
|
157
123
|
|
|
158
|
-
/* -------------------------
|
|
159
|
-
* Render
|
|
160
|
-
* ------------------------- */
|
|
161
|
-
|
|
162
124
|
render(targetId?: string): this {
|
|
163
|
-
|
|
164
|
-
let container: HTMLElement;
|
|
165
|
-
if (targetId) {
|
|
166
|
-
const target = document.querySelector(targetId);
|
|
167
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
168
|
-
throw new Error(`Button: Target "${targetId}" not found`);
|
|
169
|
-
}
|
|
170
|
-
container = target;
|
|
171
|
-
} else {
|
|
172
|
-
container = getOrCreateContainer(this._id);
|
|
173
|
-
}
|
|
174
|
-
this.container = container;
|
|
175
|
-
|
|
176
|
-
// === 2. PREPARE: Destructure state ===
|
|
125
|
+
const container = this._setupContainer(targetId);
|
|
177
126
|
const { label: text, variant, size, disabled, icon, iconPosition, loading, style, class: className } = this.state;
|
|
178
|
-
const hasTextSync = this._syncBindings.some(b => b.property === 'text');
|
|
179
|
-
const hasDisabledSync = this._syncBindings.some(b => b.property === 'disabled');
|
|
180
127
|
|
|
181
|
-
// === 3. BUILD: Create DOM elements ===
|
|
182
128
|
const button = document.createElement('button');
|
|
129
|
+
this._buttonElement = button; // ✅ Store reference
|
|
183
130
|
button.className = `jux-button jux-button-${variant} jux-button-${size}`;
|
|
184
131
|
button.id = this._id;
|
|
185
132
|
button.disabled = disabled || loading;
|
|
@@ -205,46 +152,15 @@ export class Button {
|
|
|
205
152
|
button.appendChild(iconEl);
|
|
206
153
|
}
|
|
207
154
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
212
|
-
button.addEventListener(event, handler as EventListener);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Wire sync bindings from .sync() calls
|
|
216
|
-
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
217
|
-
if (property === 'text') {
|
|
218
|
-
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
219
|
-
|
|
220
|
-
stateObj.subscribe((val: any) => {
|
|
221
|
-
const transformed = transformToComponent(val);
|
|
222
|
-
textEl.textContent = this.state.loading ? 'Loading...' : transformed;
|
|
223
|
-
this.state.label = transformed;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
else if (property === 'disabled') {
|
|
227
|
-
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
228
|
-
|
|
229
|
-
stateObj.subscribe((val: any) => {
|
|
230
|
-
const transformed = transformToComponent(val);
|
|
231
|
-
button.disabled = transformed || this.state.loading;
|
|
232
|
-
this.state.disabled = transformed;
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
else if (property === 'loading') {
|
|
236
|
-
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
237
|
-
|
|
238
|
-
stateObj.subscribe((val: any) => {
|
|
239
|
-
const transformed = transformToComponent(val);
|
|
240
|
-
button.disabled = this.state.disabled || transformed;
|
|
241
|
-
textEl.textContent = transformed ? 'Loading...' : this.state.label;
|
|
242
|
-
this.state.loading = transformed;
|
|
243
|
-
});
|
|
155
|
+
button.addEventListener('click', (e) => {
|
|
156
|
+
if (!disabled && !loading) {
|
|
157
|
+
this._triggerCallback('click', e);
|
|
244
158
|
}
|
|
245
159
|
});
|
|
246
160
|
|
|
247
|
-
|
|
161
|
+
this._wireStandardEvents(button);
|
|
162
|
+
this._wireAllSyncs();
|
|
163
|
+
|
|
248
164
|
container.appendChild(button);
|
|
249
165
|
|
|
250
166
|
requestAnimationFrame(() => {
|
|
@@ -255,13 +171,6 @@ export class Button {
|
|
|
255
171
|
|
|
256
172
|
return this;
|
|
257
173
|
}
|
|
258
|
-
|
|
259
|
-
renderTo(juxComponent: any): this {
|
|
260
|
-
if (!juxComponent?._id) {
|
|
261
|
-
throw new Error('Button.renderTo: Invalid component');
|
|
262
|
-
}
|
|
263
|
-
return this.render(`#${juxComponent._id}`);
|
|
264
|
-
}
|
|
265
174
|
}
|
|
266
175
|
|
|
267
176
|
export function button(id: string, options?: ButtonOptions): Button {
|