juxscript 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/alert.ts +124 -128
- package/lib/components/areachart.ts +169 -287
- package/lib/components/areachartsmooth.ts +2 -2
- package/lib/components/badge.ts +63 -72
- package/lib/components/barchart.ts +120 -48
- package/lib/components/button.ts +99 -101
- package/lib/components/card.ts +97 -121
- package/lib/components/chart-types.ts +159 -0
- package/lib/components/chart-utils.ts +160 -0
- package/lib/components/chart.ts +628 -48
- package/lib/components/checkbox.ts +137 -51
- package/lib/components/code.ts +89 -75
- package/lib/components/container.ts +1 -1
- package/lib/components/datepicker.ts +93 -78
- package/lib/components/dialog.ts +163 -130
- package/lib/components/divider.ts +111 -193
- package/lib/components/docs-data.json +711 -264
- package/lib/components/doughnutchart.ts +125 -57
- package/lib/components/dropdown.ts +172 -85
- package/lib/components/element.ts +66 -61
- package/lib/components/fileupload.ts +142 -171
- package/lib/components/heading.ts +64 -21
- package/lib/components/hero.ts +109 -34
- package/lib/components/icon.ts +247 -0
- package/lib/components/icons.ts +174 -0
- package/lib/components/include.ts +77 -2
- package/lib/components/input.ts +174 -125
- package/lib/components/list.ts +120 -79
- package/lib/components/menu.ts +97 -2
- package/lib/components/modal.ts +144 -63
- package/lib/components/nav.ts +153 -52
- package/lib/components/paragraph.ts +78 -28
- package/lib/components/progress.ts +83 -107
- package/lib/components/radio.ts +151 -52
- package/lib/components/select.ts +110 -102
- package/lib/components/sidebar.ts +148 -105
- package/lib/components/switch.ts +124 -125
- package/lib/components/table.ts +214 -137
- package/lib/components/tabs.ts +194 -113
- package/lib/components/theme-toggle.ts +38 -7
- package/lib/components/tooltip.ts +207 -47
- package/lib/jux.ts +24 -5
- package/lib/reactivity/state.ts +13 -299
- package/package.json +1 -2
package/lib/components/radio.ts
CHANGED
|
@@ -17,7 +17,6 @@ export interface RadioOptions {
|
|
|
17
17
|
options?: RadioOption[];
|
|
18
18
|
value?: string;
|
|
19
19
|
name?: string;
|
|
20
|
-
onChange?: (value: string) => void;
|
|
21
20
|
orientation?: 'vertical' | 'horizontal';
|
|
22
21
|
style?: string;
|
|
23
22
|
class?: string;
|
|
@@ -45,26 +44,35 @@ type RadioState = {
|
|
|
45
44
|
* { label: 'Medium', value: 'm' },
|
|
46
45
|
* { label: 'Large', value: 'l' }
|
|
47
46
|
* ],
|
|
48
|
-
* value: 'm'
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* value: 'm'
|
|
48
|
+
* })
|
|
49
|
+
* .bind('change', (e) => console.log(e.target.value))
|
|
50
|
+
* .render('#form');
|
|
51
51
|
*
|
|
52
|
-
* // Two-way binding
|
|
52
|
+
* // Two-way binding with state
|
|
53
53
|
* const sizeState = state('m');
|
|
54
|
-
* jux.radio('size')
|
|
54
|
+
* jux.radio('size')
|
|
55
|
+
* .sync('value', sizeState)
|
|
56
|
+
* .render('#form');
|
|
55
57
|
*/
|
|
56
58
|
export class Radio {
|
|
57
59
|
state: RadioState;
|
|
58
60
|
container: HTMLElement | null = null;
|
|
59
61
|
_id: string;
|
|
60
62
|
id: string;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
|
|
64
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
65
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
66
|
+
private _syncBindings: Array<{
|
|
67
|
+
property: string,
|
|
68
|
+
stateObj: State<any>,
|
|
69
|
+
toState?: Function,
|
|
70
|
+
toComponent?: Function
|
|
71
|
+
}> = [];
|
|
63
72
|
|
|
64
73
|
constructor(id: string, options: RadioOptions = {}) {
|
|
65
74
|
this._id = id;
|
|
66
75
|
this.id = id;
|
|
67
|
-
this._onChange = options.onChange;
|
|
68
76
|
|
|
69
77
|
this.state = {
|
|
70
78
|
options: options.options ?? [],
|
|
@@ -116,26 +124,28 @@ export class Radio {
|
|
|
116
124
|
return this;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Bind event handler (stores for wiring in render)
|
|
129
|
+
* DOM events only: change, click, focus, blur, etc.
|
|
130
|
+
*/
|
|
131
|
+
bind(event: string, handler: Function): this {
|
|
132
|
+
this._bindings.push({ event, handler });
|
|
121
133
|
return this;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/**
|
|
125
|
-
* Two-way
|
|
137
|
+
* Two-way sync with state (stores for wiring in render)
|
|
138
|
+
*
|
|
139
|
+
* @param property - Component property to sync ('value', 'options')
|
|
140
|
+
* @param stateObj - State object to sync with
|
|
141
|
+
* @param toState - Optional transform function when going from component to state
|
|
142
|
+
* @param toComponent - Optional transform function when going from state to component
|
|
126
143
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.state.value = val;
|
|
133
|
-
this._updateElement();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Update state when radio changes
|
|
137
|
-
this.onChange((value) => stateObj.set(value));
|
|
138
|
-
|
|
144
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
145
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
146
|
+
throw new Error(`Radio.sync: Expected a State object for property "${property}"`);
|
|
147
|
+
}
|
|
148
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
139
149
|
return this;
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -160,65 +170,154 @@ export class Radio {
|
|
|
160
170
|
* ------------------------- */
|
|
161
171
|
|
|
162
172
|
render(targetId?: string): this {
|
|
173
|
+
// === 1. SETUP: Get or create container ===
|
|
163
174
|
let container: HTMLElement;
|
|
164
|
-
|
|
165
175
|
if (targetId) {
|
|
166
176
|
const target = document.querySelector(targetId);
|
|
167
177
|
if (!target || !(target instanceof HTMLElement)) {
|
|
168
|
-
throw new Error(`Radio: Target
|
|
178
|
+
throw new Error(`Radio: Target "${targetId}" not found`);
|
|
169
179
|
}
|
|
170
180
|
container = target;
|
|
171
181
|
} else {
|
|
172
182
|
container = getOrCreateContainer(this._id);
|
|
173
183
|
}
|
|
174
|
-
|
|
175
184
|
this.container = container;
|
|
185
|
+
|
|
186
|
+
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
176
187
|
const { options, value, name, orientation, style, class: className } = this.state;
|
|
188
|
+
const hasValueSync = this._syncBindings.some(b => b.property === 'value');
|
|
177
189
|
|
|
190
|
+
// === 3. BUILD: Create DOM elements ===
|
|
178
191
|
const wrapper = document.createElement('div');
|
|
179
192
|
wrapper.className = `jux-radio jux-radio-${orientation}`;
|
|
180
193
|
wrapper.id = this._id;
|
|
181
|
-
wrapper.
|
|
194
|
+
if (className) wrapper.className += ` ${className}`;
|
|
195
|
+
if (style) wrapper.setAttribute('style', style);
|
|
182
196
|
|
|
183
|
-
|
|
184
|
-
wrapper.className += ` ${className}`;
|
|
185
|
-
}
|
|
197
|
+
const inputs: HTMLInputElement[] = [];
|
|
186
198
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
options.forEach((option) => {
|
|
200
|
+
const radioItem = document.createElement('div');
|
|
201
|
+
radioItem.className = 'jux-radio-item';
|
|
190
202
|
|
|
191
|
-
options.forEach((opt, index) => {
|
|
192
203
|
const label = document.createElement('label');
|
|
193
204
|
label.className = 'jux-radio-label';
|
|
194
205
|
|
|
195
206
|
const input = document.createElement('input');
|
|
196
207
|
input.type = 'radio';
|
|
197
208
|
input.className = 'jux-radio-input';
|
|
198
|
-
input.id = `${this._id}-${index}`;
|
|
199
209
|
input.name = name;
|
|
200
|
-
input.value =
|
|
201
|
-
input.checked =
|
|
202
|
-
input.disabled =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
input.value = option.value;
|
|
211
|
+
input.checked = option.value === value;
|
|
212
|
+
input.disabled = option.disabled || false;
|
|
213
|
+
inputs.push(input);
|
|
214
|
+
|
|
215
|
+
const checkmark = document.createElement('span');
|
|
216
|
+
checkmark.className = 'jux-radio-checkmark';
|
|
217
|
+
|
|
218
|
+
const text = document.createElement('span');
|
|
219
|
+
text.className = 'jux-radio-text';
|
|
220
|
+
text.textContent = option.label;
|
|
211
221
|
|
|
212
222
|
label.appendChild(input);
|
|
223
|
+
label.appendChild(checkmark);
|
|
224
|
+
label.appendChild(text);
|
|
225
|
+
radioItem.appendChild(label);
|
|
226
|
+
wrapper.appendChild(radioItem);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
213
230
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
231
|
+
// Default behavior (only if NOT using sync)
|
|
232
|
+
if (!hasValueSync) {
|
|
233
|
+
inputs.forEach(input => {
|
|
234
|
+
input.addEventListener('change', () => {
|
|
235
|
+
if (input.checked) {
|
|
236
|
+
this.state.value = input.value;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Wire custom bindings from .bind() calls
|
|
243
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
244
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
245
|
+
});
|
|
218
246
|
|
|
219
|
-
|
|
247
|
+
// Wire sync bindings from .sync() calls
|
|
248
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
249
|
+
if (property === 'value') {
|
|
250
|
+
const transformToState = toState || ((v: any) => v);
|
|
251
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
252
|
+
|
|
253
|
+
let isUpdating = false;
|
|
254
|
+
|
|
255
|
+
// State → Component
|
|
256
|
+
stateObj.subscribe((val: any) => {
|
|
257
|
+
if (isUpdating) return;
|
|
258
|
+
const transformed = transformToComponent(val);
|
|
259
|
+
inputs.forEach(input => {
|
|
260
|
+
input.checked = input.value === transformed;
|
|
261
|
+
});
|
|
262
|
+
this.state.value = transformed;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Component → State
|
|
266
|
+
inputs.forEach(input => {
|
|
267
|
+
input.addEventListener('change', () => {
|
|
268
|
+
if (isUpdating || !input.checked) return;
|
|
269
|
+
isUpdating = true;
|
|
270
|
+
|
|
271
|
+
const transformed = transformToState(input.value);
|
|
272
|
+
this.state.value = input.value;
|
|
273
|
+
stateObj.set(transformed);
|
|
274
|
+
|
|
275
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else if (property === 'options') {
|
|
280
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
281
|
+
|
|
282
|
+
stateObj.subscribe((val: any) => {
|
|
283
|
+
const transformed = transformToComponent(val);
|
|
284
|
+
this.state.options = transformed;
|
|
285
|
+
|
|
286
|
+
// Re-render options
|
|
287
|
+
wrapper.innerHTML = '';
|
|
288
|
+
transformed.forEach((option: any) => {
|
|
289
|
+
const radioItem = document.createElement('div');
|
|
290
|
+
radioItem.className = 'jux-radio-item';
|
|
291
|
+
|
|
292
|
+
const label = document.createElement('label');
|
|
293
|
+
label.className = 'jux-radio-label';
|
|
294
|
+
|
|
295
|
+
const input = document.createElement('input');
|
|
296
|
+
input.type = 'radio';
|
|
297
|
+
input.className = 'jux-radio-input';
|
|
298
|
+
input.name = this.state.name;
|
|
299
|
+
input.value = option.value;
|
|
300
|
+
input.checked = option.value === this.state.value;
|
|
301
|
+
input.disabled = option.disabled || false;
|
|
302
|
+
|
|
303
|
+
const checkmark = document.createElement('span');
|
|
304
|
+
checkmark.className = 'jux-radio-checkmark';
|
|
305
|
+
|
|
306
|
+
const text = document.createElement('span');
|
|
307
|
+
text.className = 'jux-radio-text';
|
|
308
|
+
text.textContent = option.label;
|
|
309
|
+
|
|
310
|
+
label.appendChild(input);
|
|
311
|
+
label.appendChild(checkmark);
|
|
312
|
+
label.appendChild(text);
|
|
313
|
+
radioItem.appendChild(label);
|
|
314
|
+
wrapper.appendChild(radioItem);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
220
318
|
});
|
|
221
319
|
|
|
320
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
222
321
|
container.appendChild(wrapper);
|
|
223
322
|
return this;
|
|
224
323
|
}
|
package/lib/components/select.ts
CHANGED
|
@@ -14,7 +14,6 @@ export interface SelectOptions {
|
|
|
14
14
|
label?: string;
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
name?: string;
|
|
17
|
-
onChange?: (value: string) => void;
|
|
18
17
|
style?: string;
|
|
19
18
|
class?: string;
|
|
20
19
|
}
|
|
@@ -35,13 +34,19 @@ export class Select {
|
|
|
35
34
|
container: HTMLElement | null = null;
|
|
36
35
|
_id: string;
|
|
37
36
|
id: string;
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
|
|
38
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
39
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
40
|
+
private _syncBindings: Array<{
|
|
41
|
+
property: string,
|
|
42
|
+
stateObj: State<any>,
|
|
43
|
+
toState?: Function,
|
|
44
|
+
toComponent?: Function
|
|
45
|
+
}> = [];
|
|
40
46
|
|
|
41
47
|
constructor(id: string, options: SelectOptions = {}) {
|
|
42
48
|
this._id = id;
|
|
43
49
|
this.id = id;
|
|
44
|
-
this._onChange = options.onChange;
|
|
45
50
|
|
|
46
51
|
this.state = {
|
|
47
52
|
options: options.options ?? [],
|
|
@@ -55,6 +60,10 @@ export class Select {
|
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/* -------------------------
|
|
64
|
+
* Fluent API
|
|
65
|
+
* ------------------------- */
|
|
66
|
+
|
|
58
67
|
options(value: SelectOption[]): this {
|
|
59
68
|
this.state.options = value;
|
|
60
69
|
return this;
|
|
@@ -102,35 +111,35 @@ export class Select {
|
|
|
102
111
|
return this;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Bind event handler (stores for wiring in render)
|
|
116
|
+
* DOM events only: change, focus, blur, etc.
|
|
117
|
+
*/
|
|
118
|
+
bind(event: string, handler: Function): this {
|
|
119
|
+
this._bindings.push({ event, handler });
|
|
107
120
|
return this;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
/**
|
|
111
|
-
* Two-way
|
|
124
|
+
* Two-way sync with state (stores for wiring in render)
|
|
125
|
+
*
|
|
126
|
+
* @param property - Component property to sync ('value', 'label', 'disabled', 'options')
|
|
127
|
+
* @param stateObj - State object to sync with
|
|
128
|
+
* @param toState - Optional transform function when going from component to state
|
|
129
|
+
* @param toComponent - Optional transform function when going from state to component
|
|
112
130
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.state.value = val;
|
|
119
|
-
this._updateElement();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Update state when select changes
|
|
123
|
-
const originalOnChange = this._onChange;
|
|
124
|
-
this._onChange = (value) => {
|
|
125
|
-
stateObj.set(value);
|
|
126
|
-
if (originalOnChange) {
|
|
127
|
-
originalOnChange(value);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
132
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
133
|
+
throw new Error(`Select.sync: Expected a State object for property "${property}"`);
|
|
134
|
+
}
|
|
135
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
131
136
|
return this;
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
/* -------------------------
|
|
140
|
+
* Helpers
|
|
141
|
+
* ------------------------- */
|
|
142
|
+
|
|
134
143
|
private _updateElement(): void {
|
|
135
144
|
const select = document.getElementById(`${this._id}-select`) as HTMLSelectElement;
|
|
136
145
|
if (select) {
|
|
@@ -143,35 +152,35 @@ export class Select {
|
|
|
143
152
|
return this.state.value;
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
/* -------------------------
|
|
156
|
+
* Render
|
|
157
|
+
* ------------------------- */
|
|
158
|
+
|
|
146
159
|
render(targetId?: string): this {
|
|
160
|
+
// === 1. SETUP: Get or create container ===
|
|
147
161
|
let container: HTMLElement;
|
|
148
|
-
|
|
149
162
|
if (targetId) {
|
|
150
163
|
const target = document.querySelector(targetId);
|
|
151
164
|
if (!target || !(target instanceof HTMLElement)) {
|
|
152
|
-
throw new Error(`Select: Target
|
|
165
|
+
throw new Error(`Select: Target "${targetId}" not found`);
|
|
153
166
|
}
|
|
154
167
|
container = target;
|
|
155
168
|
} else {
|
|
156
169
|
container = getOrCreateContainer(this._id);
|
|
157
170
|
}
|
|
158
|
-
|
|
159
171
|
this.container = container;
|
|
160
|
-
const { options, value, placeholder, label, disabled, name, style, class: className } = this.state;
|
|
161
172
|
|
|
173
|
+
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
174
|
+
const { value, options, label, disabled, name, style, class: className } = this.state;
|
|
175
|
+
const hasValueSync = this._syncBindings.some(b => b.property === 'value');
|
|
176
|
+
|
|
177
|
+
// === 3. BUILD: Create DOM elements ===
|
|
162
178
|
const wrapper = document.createElement('div');
|
|
163
179
|
wrapper.className = 'jux-select';
|
|
164
180
|
wrapper.id = this._id;
|
|
181
|
+
if (className) wrapper.className += ` ${className}`;
|
|
182
|
+
if (style) wrapper.setAttribute('style', style);
|
|
165
183
|
|
|
166
|
-
if (className) {
|
|
167
|
-
wrapper.className += ` ${className}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (style) {
|
|
171
|
-
wrapper.setAttribute('style', style);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Label
|
|
175
184
|
if (label) {
|
|
176
185
|
const labelEl = document.createElement('label');
|
|
177
186
|
labelEl.className = 'jux-select-label';
|
|
@@ -184,87 +193,86 @@ export class Select {
|
|
|
184
193
|
select.className = 'jux-select-element';
|
|
185
194
|
select.id = `${this._id}-select`;
|
|
186
195
|
select.name = name;
|
|
196
|
+
select.value = value;
|
|
187
197
|
select.disabled = disabled;
|
|
188
198
|
|
|
189
|
-
// Placeholder option
|
|
190
|
-
if (placeholder) {
|
|
191
|
-
const placeholderOpt = document.createElement('option');
|
|
192
|
-
placeholderOpt.value = '';
|
|
193
|
-
placeholderOpt.textContent = placeholder;
|
|
194
|
-
placeholderOpt.disabled = true;
|
|
195
|
-
placeholderOpt.selected = value === '';
|
|
196
|
-
select.appendChild(placeholderOpt);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Options
|
|
200
199
|
options.forEach(opt => {
|
|
201
200
|
const option = document.createElement('option');
|
|
202
201
|
option.value = opt.value;
|
|
203
202
|
option.textContent = opt.label;
|
|
204
|
-
|
|
205
|
-
option.selected = opt.value === value;
|
|
203
|
+
if (opt.value === value) option.selected = true;
|
|
206
204
|
select.appendChild(option);
|
|
207
205
|
});
|
|
208
206
|
|
|
209
|
-
select.addEventListener('change', (e) => {
|
|
210
|
-
const target = e.target as HTMLSelectElement;
|
|
211
|
-
this.state.value = target.value;
|
|
212
|
-
if (this._onChange) {
|
|
213
|
-
this._onChange(target.value);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
207
|
wrapper.appendChild(select);
|
|
218
|
-
container.appendChild(wrapper);
|
|
219
|
-
|
|
220
|
-
// Add default styles if not already present
|
|
221
|
-
this._injectDefaultStyles();
|
|
222
|
-
|
|
223
|
-
return this;
|
|
224
|
-
}
|
|
225
208
|
|
|
226
|
-
|
|
227
|
-
const styleId = 'jux-select-styles';
|
|
228
|
-
if (document.getElementById(styleId)) return;
|
|
209
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
229
210
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
211
|
+
// Default behavior (only if NOT using sync)
|
|
212
|
+
if (!hasValueSync) {
|
|
213
|
+
select.addEventListener('change', () => {
|
|
214
|
+
this.state.value = select.value;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
236
217
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
color: #374151;
|
|
242
|
-
}
|
|
218
|
+
// Wire custom bindings from .bind() calls
|
|
219
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
220
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
221
|
+
});
|
|
243
222
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
223
|
+
// Wire sync bindings from .sync() calls
|
|
224
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
225
|
+
if (property === 'value') {
|
|
226
|
+
const transformToState = toState || ((v: any) => v);
|
|
227
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
228
|
+
|
|
229
|
+
let isUpdating = false;
|
|
230
|
+
|
|
231
|
+
// State → Component
|
|
232
|
+
stateObj.subscribe((val: any) => {
|
|
233
|
+
if (isUpdating) return;
|
|
234
|
+
const transformed = transformToComponent(val);
|
|
235
|
+
if (select.value !== transformed) {
|
|
236
|
+
select.value = transformed;
|
|
237
|
+
this.state.value = transformed;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Component → State
|
|
242
|
+
select.addEventListener('change', () => {
|
|
243
|
+
if (isUpdating) return;
|
|
244
|
+
isUpdating = true;
|
|
245
|
+
|
|
246
|
+
const transformed = transformToState(select.value);
|
|
247
|
+
this.state.value = select.value;
|
|
248
|
+
stateObj.set(transformed);
|
|
249
|
+
|
|
250
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
251
|
+
});
|
|
254
252
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
else if (property === 'options') {
|
|
254
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
255
|
+
|
|
256
|
+
stateObj.subscribe((val: any) => {
|
|
257
|
+
const transformed = transformToComponent(val);
|
|
258
|
+
this.state.options = transformed;
|
|
259
|
+
|
|
260
|
+
// Re-render options
|
|
261
|
+
select.innerHTML = '';
|
|
262
|
+
transformed.forEach((opt: any) => {
|
|
263
|
+
const option = document.createElement('option');
|
|
264
|
+
option.value = opt.value;
|
|
265
|
+
option.textContent = opt.label;
|
|
266
|
+
if (opt.value === this.state.value) option.selected = true;
|
|
267
|
+
select.appendChild(option);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
260
270
|
}
|
|
271
|
+
});
|
|
261
272
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
`;
|
|
267
|
-
document.head.appendChild(style);
|
|
273
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
274
|
+
container.appendChild(wrapper);
|
|
275
|
+
return this;
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
renderTo(juxComponent: any): this {
|