juxscript 1.0.3 → 1.0.4
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/README.md +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +228 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1697 -388
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +166 -78
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +247 -0
- package/lib/components/sidebar.ts +86 -36
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +172 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Paragraph options
|
|
5
|
+
*/
|
|
6
|
+
export interface ParagraphOptions {
|
|
7
|
+
text?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
style?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Paragraph state
|
|
14
|
+
*/
|
|
15
|
+
type ParagraphState = {
|
|
16
|
+
text: string;
|
|
17
|
+
class: string;
|
|
18
|
+
style: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Paragraph component - semantic paragraph element
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* jux.paragraph('intro', { text: 'Welcome to JUX' }).render('#app');
|
|
26
|
+
* jux.paragraph('description').text('A simple framework').render('#app');
|
|
27
|
+
*/
|
|
28
|
+
export class Paragraph {
|
|
29
|
+
state: ParagraphState;
|
|
30
|
+
container: HTMLElement | null = null;
|
|
31
|
+
_id: string;
|
|
32
|
+
id: string;
|
|
33
|
+
|
|
34
|
+
constructor(id: string, options: ParagraphOptions = {}) {
|
|
35
|
+
this._id = id;
|
|
36
|
+
this.id = id;
|
|
37
|
+
|
|
38
|
+
this.state = {
|
|
39
|
+
text: options.text ?? '',
|
|
40
|
+
class: options.class ?? '',
|
|
41
|
+
style: options.style ?? ''
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* -------------------------
|
|
46
|
+
* Fluent API
|
|
47
|
+
* ------------------------- */
|
|
48
|
+
|
|
49
|
+
text(value: string): this {
|
|
50
|
+
this.state.text = value;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class(value: string): this {
|
|
55
|
+
this.state.class = value;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
style(value: string): this {
|
|
60
|
+
this.state.style = value;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* -------------------------
|
|
65
|
+
* Render
|
|
66
|
+
* ------------------------- */
|
|
67
|
+
|
|
68
|
+
render(targetId?: string | HTMLElement): this {
|
|
69
|
+
let container: HTMLElement;
|
|
70
|
+
|
|
71
|
+
if (targetId) {
|
|
72
|
+
if (targetId instanceof HTMLElement) {
|
|
73
|
+
container = targetId;
|
|
74
|
+
} else {
|
|
75
|
+
const target = document.querySelector(targetId);
|
|
76
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
77
|
+
throw new Error(`Paragraph: Target element "${targetId}" not found`);
|
|
78
|
+
}
|
|
79
|
+
container = target;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
container = getOrCreateContainer(this._id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.container = container;
|
|
86
|
+
const { text, class: className, style } = this.state;
|
|
87
|
+
|
|
88
|
+
const p = document.createElement('p');
|
|
89
|
+
p.id = this._id;
|
|
90
|
+
p.textContent = text;
|
|
91
|
+
|
|
92
|
+
if (className) {
|
|
93
|
+
p.className = className;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (style) {
|
|
97
|
+
p.setAttribute('style', style);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
container.appendChild(p);
|
|
101
|
+
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Factory helper
|
|
108
|
+
*/
|
|
109
|
+
export function paragraph(id: string, options: ParagraphOptions = {}): Paragraph {
|
|
110
|
+
return new Paragraph(id, options);
|
|
111
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Progress component options
|
|
6
|
+
*/
|
|
7
|
+
export interface ProgressOptions {
|
|
8
|
+
value?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
showPercentage?: boolean;
|
|
12
|
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
striped?: boolean;
|
|
15
|
+
animated?: boolean;
|
|
16
|
+
style?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Progress component state
|
|
22
|
+
*/
|
|
23
|
+
type ProgressState = {
|
|
24
|
+
value: number;
|
|
25
|
+
max: number;
|
|
26
|
+
label: string;
|
|
27
|
+
showPercentage: boolean;
|
|
28
|
+
variant: string;
|
|
29
|
+
size: string;
|
|
30
|
+
striped: boolean;
|
|
31
|
+
animated: boolean;
|
|
32
|
+
style: string;
|
|
33
|
+
class: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Progress component - Progress bar for loading/completion
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* jux.progress('upload', {
|
|
41
|
+
* value: 45,
|
|
42
|
+
* max: 100,
|
|
43
|
+
* label: 'Uploading...',
|
|
44
|
+
* showPercentage: true,
|
|
45
|
+
* animated: true
|
|
46
|
+
* }).render('#app');
|
|
47
|
+
*
|
|
48
|
+
* // Update progress
|
|
49
|
+
* const prog = jux.progress('upload').render('#app');
|
|
50
|
+
* prog.value(75);
|
|
51
|
+
*/
|
|
52
|
+
export class Progress {
|
|
53
|
+
state: ProgressState;
|
|
54
|
+
container: HTMLElement | null = null;
|
|
55
|
+
_id: string;
|
|
56
|
+
id: string;
|
|
57
|
+
private _boundState?: State<number>;
|
|
58
|
+
|
|
59
|
+
constructor(id: string, options: ProgressOptions = {}) {
|
|
60
|
+
this._id = id;
|
|
61
|
+
this.id = id;
|
|
62
|
+
|
|
63
|
+
this.state = {
|
|
64
|
+
value: options.value ?? 0,
|
|
65
|
+
max: options.max ?? 100,
|
|
66
|
+
label: options.label ?? '',
|
|
67
|
+
showPercentage: options.showPercentage ?? false,
|
|
68
|
+
variant: options.variant ?? 'default',
|
|
69
|
+
size: options.size ?? 'md',
|
|
70
|
+
striped: options.striped ?? false,
|
|
71
|
+
animated: options.animated ?? false,
|
|
72
|
+
style: options.style ?? '',
|
|
73
|
+
class: options.class ?? ''
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* -------------------------
|
|
78
|
+
* Fluent API
|
|
79
|
+
* ------------------------- */
|
|
80
|
+
|
|
81
|
+
value(value: number): this {
|
|
82
|
+
this.state.value = Math.max(0, Math.min(value, this.state.max));
|
|
83
|
+
this._updateElement();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
max(value: number): this {
|
|
88
|
+
this.state.max = value;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
label(value: string): this {
|
|
93
|
+
this.state.label = value;
|
|
94
|
+
this._updateElement();
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
showPercentage(value: boolean): this {
|
|
99
|
+
this.state.showPercentage = value;
|
|
100
|
+
this._updateElement();
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
variant(value: 'default' | 'success' | 'warning' | 'error' | 'info'): this {
|
|
105
|
+
this.state.variant = value;
|
|
106
|
+
this._updateElement();
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
size(value: 'sm' | 'md' | 'lg'): this {
|
|
111
|
+
this.state.size = value;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
striped(value: boolean): this {
|
|
116
|
+
this.state.striped = value;
|
|
117
|
+
this._updateElement();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
animated(value: boolean): this {
|
|
122
|
+
this.state.animated = value;
|
|
123
|
+
this._updateElement();
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
style(value: string): this {
|
|
128
|
+
this.state.style = value;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class(value: string): this {
|
|
133
|
+
this.state.class = value;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Two-way binding to state
|
|
139
|
+
*/
|
|
140
|
+
bind(stateObj: State<number>): this {
|
|
141
|
+
this._boundState = stateObj;
|
|
142
|
+
|
|
143
|
+
stateObj.subscribe((val) => {
|
|
144
|
+
this.value(val);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* -------------------------
|
|
151
|
+
* Helpers
|
|
152
|
+
* ------------------------- */
|
|
153
|
+
|
|
154
|
+
private _updateElement(): void {
|
|
155
|
+
const wrapper = document.getElementById(this._id);
|
|
156
|
+
const bar = document.getElementById(`${this._id}-bar`);
|
|
157
|
+
const labelEl = document.getElementById(`${this._id}-label`);
|
|
158
|
+
|
|
159
|
+
// If element has a value attribute set externally (e.g., by bindValue), sync state
|
|
160
|
+
if (wrapper && wrapper.hasAttribute('data-value')) {
|
|
161
|
+
const externalValue = parseFloat(wrapper.getAttribute('data-value') || '0');
|
|
162
|
+
this.state.value = externalValue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (bar) {
|
|
166
|
+
const percentage = (this.state.value / this.state.max) * 100;
|
|
167
|
+
bar.style.width = `${percentage}%`;
|
|
168
|
+
bar.setAttribute('aria-valuenow', this.state.value.toString());
|
|
169
|
+
bar.className = `jux-progress-bar jux-progress-bar-${this.state.variant}`;
|
|
170
|
+
|
|
171
|
+
if (this.state.striped) {
|
|
172
|
+
bar.classList.add('jux-progress-bar-striped');
|
|
173
|
+
}
|
|
174
|
+
if (this.state.animated) {
|
|
175
|
+
bar.classList.add('jux-progress-bar-animated');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (labelEl) {
|
|
180
|
+
const percentage = Math.round((this.state.value / this.state.max) * 100);
|
|
181
|
+
const text = this.state.showPercentage
|
|
182
|
+
? `${this.state.label} ${percentage}%`.trim()
|
|
183
|
+
: this.state.label;
|
|
184
|
+
labelEl.textContent = text;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getPercentage(): number {
|
|
189
|
+
return Math.round((this.state.value / this.state.max) * 100);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* -------------------------
|
|
193
|
+
* Render
|
|
194
|
+
* ------------------------- */
|
|
195
|
+
|
|
196
|
+
render(targetId?: string): this {
|
|
197
|
+
let container: HTMLElement;
|
|
198
|
+
|
|
199
|
+
if (targetId) {
|
|
200
|
+
const target = document.querySelector(targetId);
|
|
201
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
202
|
+
throw new Error(`Progress: Target element "${targetId}" not found`);
|
|
203
|
+
}
|
|
204
|
+
container = target;
|
|
205
|
+
} else {
|
|
206
|
+
container = getOrCreateContainer(this._id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.container = container;
|
|
210
|
+
const { value, max, label, showPercentage, variant, size, striped, animated, style, class: className } = this.state;
|
|
211
|
+
|
|
212
|
+
const wrapper = document.createElement('div');
|
|
213
|
+
wrapper.className = `jux-progress jux-progress-${size}`;
|
|
214
|
+
wrapper.id = this._id;
|
|
215
|
+
|
|
216
|
+
if (className) {
|
|
217
|
+
wrapper.className += ` ${className}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (style) {
|
|
221
|
+
wrapper.setAttribute('style', style);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Label
|
|
225
|
+
if (label || showPercentage) {
|
|
226
|
+
const labelEl = document.createElement('div');
|
|
227
|
+
labelEl.className = 'jux-progress-label';
|
|
228
|
+
labelEl.id = `${this._id}-label`;
|
|
229
|
+
const percentage = Math.round((value / max) * 100);
|
|
230
|
+
const text = showPercentage
|
|
231
|
+
? `${label} ${percentage}%`.trim()
|
|
232
|
+
: label;
|
|
233
|
+
labelEl.textContent = text;
|
|
234
|
+
wrapper.appendChild(labelEl);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Progress track
|
|
238
|
+
const track = document.createElement('div');
|
|
239
|
+
track.className = 'jux-progress-track';
|
|
240
|
+
|
|
241
|
+
// Progress bar
|
|
242
|
+
const bar = document.createElement('div');
|
|
243
|
+
bar.className = `jux-progress-bar jux-progress-bar-${variant}`;
|
|
244
|
+
bar.id = `${this._id}-bar`;
|
|
245
|
+
bar.setAttribute('role', 'progressbar');
|
|
246
|
+
bar.setAttribute('aria-valuenow', value.toString());
|
|
247
|
+
bar.setAttribute('aria-valuemin', '0');
|
|
248
|
+
bar.setAttribute('aria-valuemax', max.toString());
|
|
249
|
+
|
|
250
|
+
const percentage = (value / max) * 100;
|
|
251
|
+
bar.style.width = `${percentage}%`;
|
|
252
|
+
|
|
253
|
+
if (striped) {
|
|
254
|
+
bar.classList.add('jux-progress-bar-striped');
|
|
255
|
+
}
|
|
256
|
+
if (animated) {
|
|
257
|
+
bar.classList.add('jux-progress-bar-animated');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
track.appendChild(bar);
|
|
261
|
+
wrapper.appendChild(track);
|
|
262
|
+
container.appendChild(wrapper);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderTo(juxComponent: any): this {
|
|
267
|
+
if (!juxComponent?._id) {
|
|
268
|
+
throw new Error('Progress.renderTo: Invalid component');
|
|
269
|
+
}
|
|
270
|
+
return this.render(`#${juxComponent._id}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function progress(id: string, options: ProgressOptions = {}): Progress {
|
|
275
|
+
return new Progress(id, options);
|
|
276
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Radio option
|
|
6
|
+
*/
|
|
7
|
+
export interface RadioOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Radio component options
|
|
15
|
+
*/
|
|
16
|
+
export interface RadioOptions {
|
|
17
|
+
options?: RadioOption[];
|
|
18
|
+
value?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
orientation?: 'vertical' | 'horizontal';
|
|
22
|
+
style?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Radio component state
|
|
28
|
+
*/
|
|
29
|
+
type RadioState = {
|
|
30
|
+
options: RadioOption[];
|
|
31
|
+
value: string;
|
|
32
|
+
name: string;
|
|
33
|
+
orientation: string;
|
|
34
|
+
style: string;
|
|
35
|
+
class: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Radio component - Radio button group
|
|
40
|
+
*
|
|
41
|
+
* Usage:
|
|
42
|
+
* jux.radio('size', {
|
|
43
|
+
* options: [
|
|
44
|
+
* { label: 'Small', value: 's' },
|
|
45
|
+
* { label: 'Medium', value: 'm' },
|
|
46
|
+
* { label: 'Large', value: 'l' }
|
|
47
|
+
* ],
|
|
48
|
+
* value: 'm',
|
|
49
|
+
* onChange: (val) => console.log(val)
|
|
50
|
+
* }).render('#form');
|
|
51
|
+
*
|
|
52
|
+
* // Two-way binding
|
|
53
|
+
* const sizeState = state('m');
|
|
54
|
+
* jux.radio('size').bind(sizeState).render('#form');
|
|
55
|
+
*/
|
|
56
|
+
export class Radio {
|
|
57
|
+
state: RadioState;
|
|
58
|
+
container: HTMLElement | null = null;
|
|
59
|
+
_id: string;
|
|
60
|
+
id: string;
|
|
61
|
+
private _onChange?: (value: string) => void;
|
|
62
|
+
private _boundState?: State<string>;
|
|
63
|
+
|
|
64
|
+
constructor(id: string, options: RadioOptions = {}) {
|
|
65
|
+
this._id = id;
|
|
66
|
+
this.id = id;
|
|
67
|
+
this._onChange = options.onChange;
|
|
68
|
+
|
|
69
|
+
this.state = {
|
|
70
|
+
options: options.options ?? [],
|
|
71
|
+
value: options.value ?? '',
|
|
72
|
+
name: options.name ?? id,
|
|
73
|
+
orientation: options.orientation ?? 'vertical',
|
|
74
|
+
style: options.style ?? '',
|
|
75
|
+
class: options.class ?? ''
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* -------------------------
|
|
80
|
+
* Fluent API
|
|
81
|
+
* ------------------------- */
|
|
82
|
+
|
|
83
|
+
options(value: RadioOption[]): this {
|
|
84
|
+
this.state.options = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
addOption(option: RadioOption): this {
|
|
89
|
+
this.state.options = [...this.state.options, option];
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
value(value: string): this {
|
|
94
|
+
this.state.value = value;
|
|
95
|
+
this._updateElement();
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
name(value: string): this {
|
|
100
|
+
this.state.name = value;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
orientation(value: 'vertical' | 'horizontal'): this {
|
|
105
|
+
this.state.orientation = value;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
style(value: string): this {
|
|
110
|
+
this.state.style = value;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class(value: string): this {
|
|
115
|
+
this.state.class = value;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onChange(handler: (value: string) => void): this {
|
|
120
|
+
this._onChange = handler;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Two-way binding to state
|
|
126
|
+
*/
|
|
127
|
+
bind(stateObj: State<string>): this {
|
|
128
|
+
this._boundState = stateObj;
|
|
129
|
+
|
|
130
|
+
// Update radio when state changes
|
|
131
|
+
stateObj.subscribe((val) => {
|
|
132
|
+
this.state.value = val;
|
|
133
|
+
this._updateElement();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Update state when radio changes
|
|
137
|
+
this.onChange((value) => stateObj.set(value));
|
|
138
|
+
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* -------------------------
|
|
143
|
+
* Helpers
|
|
144
|
+
* ------------------------- */
|
|
145
|
+
|
|
146
|
+
private _updateElement(): void {
|
|
147
|
+
const inputs = document.querySelectorAll(`input[name="${this.state.name}"]`);
|
|
148
|
+
inputs.forEach((input) => {
|
|
149
|
+
const radioInput = input as HTMLInputElement;
|
|
150
|
+
radioInput.checked = radioInput.value === this.state.value;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getValue(): string {
|
|
155
|
+
return this.state.value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* -------------------------
|
|
159
|
+
* Render
|
|
160
|
+
* ------------------------- */
|
|
161
|
+
|
|
162
|
+
render(targetId?: string): this {
|
|
163
|
+
let container: HTMLElement;
|
|
164
|
+
|
|
165
|
+
if (targetId) {
|
|
166
|
+
const target = document.querySelector(targetId);
|
|
167
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
168
|
+
throw new Error(`Radio: Target element "${targetId}" not found`);
|
|
169
|
+
}
|
|
170
|
+
container = target;
|
|
171
|
+
} else {
|
|
172
|
+
container = getOrCreateContainer(this._id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.container = container;
|
|
176
|
+
const { options, value, name, orientation, style, class: className } = this.state;
|
|
177
|
+
|
|
178
|
+
const wrapper = document.createElement('div');
|
|
179
|
+
wrapper.className = `jux-radio jux-radio-${orientation}`;
|
|
180
|
+
wrapper.id = this._id;
|
|
181
|
+
wrapper.setAttribute('role', 'radiogroup');
|
|
182
|
+
|
|
183
|
+
if (className) {
|
|
184
|
+
wrapper.className += ` ${className}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (style) {
|
|
188
|
+
wrapper.setAttribute('style', style);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
options.forEach((opt, index) => {
|
|
192
|
+
const label = document.createElement('label');
|
|
193
|
+
label.className = 'jux-radio-label';
|
|
194
|
+
|
|
195
|
+
const input = document.createElement('input');
|
|
196
|
+
input.type = 'radio';
|
|
197
|
+
input.className = 'jux-radio-input';
|
|
198
|
+
input.id = `${this._id}-${index}`;
|
|
199
|
+
input.name = name;
|
|
200
|
+
input.value = opt.value;
|
|
201
|
+
input.checked = opt.value === value;
|
|
202
|
+
input.disabled = opt.disabled ?? false;
|
|
203
|
+
|
|
204
|
+
input.addEventListener('change', (e) => {
|
|
205
|
+
const target = e.target as HTMLInputElement;
|
|
206
|
+
this.state.value = target.value;
|
|
207
|
+
if (this._onChange) {
|
|
208
|
+
this._onChange(target.value);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
label.appendChild(input);
|
|
213
|
+
|
|
214
|
+
const span = document.createElement('span');
|
|
215
|
+
span.className = 'jux-radio-text';
|
|
216
|
+
span.textContent = opt.label;
|
|
217
|
+
label.appendChild(span);
|
|
218
|
+
|
|
219
|
+
wrapper.appendChild(label);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
container.appendChild(wrapper);
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
renderTo(juxComponent: any): this {
|
|
227
|
+
if (!juxComponent?._id) {
|
|
228
|
+
throw new Error('Radio.renderTo: Invalid component');
|
|
229
|
+
}
|
|
230
|
+
return this.render(`#${juxComponent._id}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function radio(id: string, options: RadioOptions = {}): Radio {
|
|
235
|
+
return new Radio(id, options);
|
|
236
|
+
}
|