juxscript 1.1.2 → 1.1.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/machinery/build3.js +7 -91
- package/machinery/compiler3.js +3 -209
- package/machinery/config.js +93 -6
- package/machinery/serve.js +255 -0
- package/machinery/watcher.js +49 -161
- package/package.json +19 -5
- package/lib/components/alert.ts +0 -200
- package/lib/components/app.ts +0 -247
- package/lib/components/badge.ts +0 -101
- package/lib/components/base/BaseComponent.ts +0 -421
- package/lib/components/base/FormInput.ts +0 -227
- package/lib/components/button.ts +0 -178
- package/lib/components/card.ts +0 -173
- package/lib/components/chart.ts +0 -231
- package/lib/components/checkbox.ts +0 -242
- package/lib/components/code.ts +0 -123
- package/lib/components/container.ts +0 -140
- package/lib/components/data.ts +0 -135
- package/lib/components/datepicker.ts +0 -234
- package/lib/components/dialog.ts +0 -172
- package/lib/components/divider.ts +0 -100
- package/lib/components/dropdown.ts +0 -186
- package/lib/components/element.ts +0 -267
- package/lib/components/fileupload.ts +0 -309
- package/lib/components/grid.ts +0 -291
- package/lib/components/guard.ts +0 -92
- package/lib/components/heading.ts +0 -96
- package/lib/components/helpers.ts +0 -41
- package/lib/components/hero.ts +0 -224
- package/lib/components/icon.ts +0 -178
- package/lib/components/icons.ts +0 -464
- package/lib/components/include.ts +0 -410
- package/lib/components/input.ts +0 -457
- package/lib/components/list.ts +0 -419
- package/lib/components/loading.ts +0 -100
- package/lib/components/menu.ts +0 -275
- package/lib/components/modal.ts +0 -284
- package/lib/components/nav.ts +0 -257
- package/lib/components/paragraph.ts +0 -97
- package/lib/components/progress.ts +0 -159
- package/lib/components/radio.ts +0 -278
- package/lib/components/req.ts +0 -303
- package/lib/components/script.ts +0 -41
- package/lib/components/select.ts +0 -252
- package/lib/components/sidebar.ts +0 -275
- package/lib/components/style.ts +0 -41
- package/lib/components/switch.ts +0 -246
- package/lib/components/table.ts +0 -1249
- package/lib/components/tabs.ts +0 -250
- package/lib/components/theme-toggle.ts +0 -293
- package/lib/components/tooltip.ts +0 -144
- package/lib/components/view.ts +0 -190
- package/lib/components/write.ts +0 -272
- package/lib/layouts/default.css +0 -260
- package/lib/layouts/figma.css +0 -334
- package/lib/reactivity/state.ts +0 -78
- package/lib/utils/fetch.ts +0 -553
- package/machinery/ast.js +0 -347
- package/machinery/build.js +0 -466
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/machinery/doc-generator.js +0 -136
- package/machinery/imports.js +0 -155
- package/machinery/server.js +0 -166
- package/machinery/ts-shim.js +0 -46
- package/machinery/validators/file-validator.js +0 -123
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
import { State } from '../../reactivity/state.js';
|
|
2
|
-
import { getOrCreateContainer } from '../helpers.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Abstract base class for all JUX components
|
|
6
|
-
* Provides common storage, event routing, and lifecycle methods
|
|
7
|
-
*
|
|
8
|
-
* Children must provide:
|
|
9
|
-
* - TRIGGER_EVENTS constant (readonly string[])
|
|
10
|
-
* - CALLBACK_EVENTS constant (readonly string[])
|
|
11
|
-
* - render() implementation
|
|
12
|
-
*/
|
|
13
|
-
export abstract class BaseComponent<TState extends Record<string, any>> {
|
|
14
|
-
// Common properties (all components have these)
|
|
15
|
-
state: TState;
|
|
16
|
-
container: HTMLElement | null = null;
|
|
17
|
-
_id: string;
|
|
18
|
-
id: string;
|
|
19
|
-
|
|
20
|
-
// Event & sync storage (populated by bind() and sync())
|
|
21
|
-
protected _bindings: Array<{ event: string, handler: Function }> = [];
|
|
22
|
-
protected _syncBindings: Array<{
|
|
23
|
-
property: string,
|
|
24
|
-
stateObj: State<any>,
|
|
25
|
-
toState?: Function,
|
|
26
|
-
toComponent?: Function
|
|
27
|
-
}> = [];
|
|
28
|
-
protected _triggerHandlers: Map<string, Function> = new Map();
|
|
29
|
-
protected _callbackHandlers: Map<string, Function> = new Map();
|
|
30
|
-
|
|
31
|
-
constructor(id: string, initialState: TState) {
|
|
32
|
-
this._id = id;
|
|
33
|
-
this.id = id;
|
|
34
|
-
this.state = initialState;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
38
|
-
* ABSTRACT METHODS (Child must implement)
|
|
39
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
40
|
-
|
|
41
|
-
protected abstract getTriggerEvents(): readonly string[];
|
|
42
|
-
protected abstract getCallbackEvents(): readonly string[];
|
|
43
|
-
abstract render(targetId?: string): this;
|
|
44
|
-
|
|
45
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
46
|
-
* COMMON FLUENT API (Inherited by all components)
|
|
47
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Set component style
|
|
51
|
-
*/
|
|
52
|
-
style(value: string): this {
|
|
53
|
-
(this.state as any).style = value;
|
|
54
|
-
return this;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Set component class
|
|
59
|
-
*/
|
|
60
|
-
class(value: string): this {
|
|
61
|
-
(this.state as any).class = value;
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
66
|
-
* CSS CLASS MANAGEMENT
|
|
67
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Add a CSS class to the component
|
|
71
|
-
*/
|
|
72
|
-
addClass(value: string): this {
|
|
73
|
-
const current = (this.state as any).class || '';
|
|
74
|
-
const classes = current.split(' ').filter((c: string) => c);
|
|
75
|
-
if (!classes.includes(value)) {
|
|
76
|
-
classes.push(value);
|
|
77
|
-
(this.state as any).class = classes.join(' ');
|
|
78
|
-
if (this.container) this.container.classList.add(value);
|
|
79
|
-
}
|
|
80
|
-
return this;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Remove a CSS class from the component
|
|
85
|
-
*/
|
|
86
|
-
removeClass(value: string): this {
|
|
87
|
-
const current = (this.state as any).class || '';
|
|
88
|
-
const classes = current.split(' ').filter((c: string) => c && c !== value);
|
|
89
|
-
(this.state as any).class = classes.join(' ');
|
|
90
|
-
if (this.container) this.container.classList.remove(value);
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Toggle a CSS class on the component
|
|
96
|
-
*/
|
|
97
|
-
toggleClass(value: string): this {
|
|
98
|
-
const current = (this.state as any).class || '';
|
|
99
|
-
const hasClass = current.split(' ').includes(value);
|
|
100
|
-
return hasClass ? this.removeClass(value) : this.addClass(value);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
104
|
-
* VISIBILITY CONTROL
|
|
105
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Set component visibility
|
|
109
|
-
*/
|
|
110
|
-
visible(value: boolean): this {
|
|
111
|
-
(this.state as any).visible = value;
|
|
112
|
-
if (this.container) {
|
|
113
|
-
// Find the actual component wrapper, not the parent container
|
|
114
|
-
const wrapper = this.container.querySelector(`#${this._id}`) as HTMLElement;
|
|
115
|
-
if (wrapper) {
|
|
116
|
-
wrapper.style.display = value ? '' : 'none';
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return this;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Show the component
|
|
124
|
-
*/
|
|
125
|
-
show(): this {
|
|
126
|
-
return this.visible(true);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Hide the component
|
|
131
|
-
*/
|
|
132
|
-
hide(): this {
|
|
133
|
-
return this.visible(false);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Toggle component visibility
|
|
138
|
-
*/
|
|
139
|
-
toggleVisibility(): this {
|
|
140
|
-
const isVisible = (this.state as any).visible ?? true;
|
|
141
|
-
return this.visible(!isVisible);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
145
|
-
* ATTRIBUTE MANAGEMENT
|
|
146
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Set a single HTML attribute
|
|
150
|
-
*/
|
|
151
|
-
attr(name: string, value: string): this {
|
|
152
|
-
const attrs = (this.state as any).attributes || {};
|
|
153
|
-
(this.state as any).attributes = { ...attrs, [name]: value };
|
|
154
|
-
if (this.container) this.container.setAttribute(name, value);
|
|
155
|
-
return this;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Set multiple HTML attributes
|
|
160
|
-
*/
|
|
161
|
-
attrs(attributes: Record<string, string>): this {
|
|
162
|
-
Object.entries(attributes).forEach(([name, value]) => {
|
|
163
|
-
this.attr(name, value);
|
|
164
|
-
});
|
|
165
|
-
return this;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Remove an HTML attribute
|
|
170
|
-
*/
|
|
171
|
-
removeAttr(name: string): this {
|
|
172
|
-
const attrs = (this.state as any).attributes || {};
|
|
173
|
-
delete attrs[name];
|
|
174
|
-
if (this.container) this.container.removeAttribute(name);
|
|
175
|
-
return this;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
179
|
-
* DISABLED STATE
|
|
180
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Set disabled state for interactive elements
|
|
184
|
-
*/
|
|
185
|
-
disabled(value: boolean): this {
|
|
186
|
-
(this.state as any).disabled = value;
|
|
187
|
-
if (this.container) {
|
|
188
|
-
const inputs = this.container.querySelectorAll('input, button, select, textarea');
|
|
189
|
-
inputs.forEach(el => {
|
|
190
|
-
(el as HTMLInputElement).disabled = value;
|
|
191
|
-
});
|
|
192
|
-
this.container.setAttribute('aria-disabled', String(value));
|
|
193
|
-
}
|
|
194
|
-
return this;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Enable the component
|
|
199
|
-
*/
|
|
200
|
-
enable(): this {
|
|
201
|
-
return this.disabled(false);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Disable the component
|
|
206
|
-
*/
|
|
207
|
-
disable(): this {
|
|
208
|
-
return this.disabled(true);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
212
|
-
* LOADING STATE
|
|
213
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Set loading state
|
|
217
|
-
*/
|
|
218
|
-
loading(value: boolean): this {
|
|
219
|
-
(this.state as any).loading = value;
|
|
220
|
-
if (this.container) {
|
|
221
|
-
if (value) {
|
|
222
|
-
this.container.classList.add('jux-loading');
|
|
223
|
-
this.container.setAttribute('aria-busy', 'true');
|
|
224
|
-
} else {
|
|
225
|
-
this.container.classList.remove('jux-loading');
|
|
226
|
-
this.container.removeAttribute('aria-busy');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return this;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
233
|
-
* FOCUS MANAGEMENT
|
|
234
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Focus the first focusable element in the component
|
|
238
|
-
*/
|
|
239
|
-
focus(): this {
|
|
240
|
-
if (this.container) {
|
|
241
|
-
const focusable = this.container.querySelector('input, button, select, textarea, [tabindex]');
|
|
242
|
-
if (focusable) (focusable as HTMLElement).focus();
|
|
243
|
-
}
|
|
244
|
-
return this;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Blur the currently focused element in the component
|
|
249
|
-
*/
|
|
250
|
-
blur(): this {
|
|
251
|
-
if (this.container) {
|
|
252
|
-
const focused = this.container.querySelector(':focus');
|
|
253
|
-
if (focused) (focused as HTMLElement).blur();
|
|
254
|
-
}
|
|
255
|
-
return this;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
259
|
-
* DOM MANIPULATION
|
|
260
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Remove the component from the DOM
|
|
264
|
-
*/
|
|
265
|
-
remove(): this {
|
|
266
|
-
if (this.container) {
|
|
267
|
-
this.container.remove();
|
|
268
|
-
this.container = null;
|
|
269
|
-
}
|
|
270
|
-
return this;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
274
|
-
* EVENT BINDING (Shared logic)
|
|
275
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
276
|
-
|
|
277
|
-
bind(event: string, handler: Function): this {
|
|
278
|
-
if (this._isTriggerEvent(event)) {
|
|
279
|
-
this._triggerHandlers.set(event, handler);
|
|
280
|
-
} else if (this._isCallbackEvent(event)) {
|
|
281
|
-
this._callbackHandlers.set(event, handler);
|
|
282
|
-
} else {
|
|
283
|
-
this._bindings.push({ event, handler });
|
|
284
|
-
}
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Sync a component property with a State object
|
|
290
|
-
* @param property - The property to sync
|
|
291
|
-
* @param stateObj - The State object to sync with
|
|
292
|
-
* @param toStateOrTransform - Either toState function OR a simple transform function
|
|
293
|
-
* @param toComponent - Optional toComponent function (if toState was provided)
|
|
294
|
-
*/
|
|
295
|
-
sync(property: string, stateObj: State<any>, toStateOrTransform?: Function, toComponent?: Function): this {
|
|
296
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
297
|
-
throw new Error(`${this.constructor.name}.sync: Expected a State object for property "${property}"`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// If only 3 args provided, treat the function as toComponent (the common case)
|
|
301
|
-
const actualToState = (toComponent !== undefined) ? toStateOrTransform : undefined;
|
|
302
|
-
const actualToComponent = (toComponent !== undefined) ? toComponent : toStateOrTransform;
|
|
303
|
-
|
|
304
|
-
this._syncBindings.push({
|
|
305
|
-
property,
|
|
306
|
-
stateObj,
|
|
307
|
-
toState: actualToState,
|
|
308
|
-
toComponent: actualToComponent
|
|
309
|
-
});
|
|
310
|
-
return this;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
protected _isTriggerEvent(event: string): boolean {
|
|
314
|
-
return this.getTriggerEvents().includes(event);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
protected _isCallbackEvent(event: string): boolean {
|
|
318
|
-
return this.getCallbackEvents().includes(event);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
protected _triggerCallback(eventName: string, ...args: any[]): void {
|
|
322
|
-
|
|
323
|
-
if (this._callbackHandlers.has(eventName)) {
|
|
324
|
-
const handler = this._callbackHandlers.get(eventName)!;
|
|
325
|
-
handler(...args);
|
|
326
|
-
} else {
|
|
327
|
-
console.warn(`🔍 No handler found for "${eventName}"`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
332
|
-
* COMMON RENDER HELPERS
|
|
333
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
334
|
-
|
|
335
|
-
protected _setupContainer(targetId?: string): HTMLElement {
|
|
336
|
-
let container: HTMLElement;
|
|
337
|
-
if (targetId) {
|
|
338
|
-
// Strip leading # if present
|
|
339
|
-
const id = targetId.startsWith('#') ? targetId.slice(1) : targetId;
|
|
340
|
-
const target = document.getElementById(id);
|
|
341
|
-
if (target) {
|
|
342
|
-
container = target;
|
|
343
|
-
} else {
|
|
344
|
-
// Gracefully create the container instead of throwing
|
|
345
|
-
console.warn(`[Jux] Target "${targetId}" not found, creating it with graceful fallback`);
|
|
346
|
-
container = getOrCreateContainer(id);
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
container = getOrCreateContainer(this._id);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Add universal component class for DOM inspection
|
|
353
|
-
// container.classList.add('jux-component');
|
|
354
|
-
|
|
355
|
-
this.container = container;
|
|
356
|
-
return container;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
protected _wireStandardEvents(element: HTMLElement): void {
|
|
360
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
361
|
-
element.addEventListener(event, handler as EventListener);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Automatically wire ALL sync bindings by calling the corresponding method
|
|
367
|
-
* if it exists on the component
|
|
368
|
-
*/
|
|
369
|
-
protected _wireAllSyncs(): void {
|
|
370
|
-
this._syncBindings.forEach(({ property, stateObj, toComponent }) => {
|
|
371
|
-
const transform = toComponent || ((v: any) => v);
|
|
372
|
-
|
|
373
|
-
// Check if component has a method matching the property name
|
|
374
|
-
const method = (this as any)[property];
|
|
375
|
-
|
|
376
|
-
if (typeof method === 'function') {
|
|
377
|
-
// Set initial value
|
|
378
|
-
const initialValue = transform(stateObj.value);
|
|
379
|
-
method.call(this, initialValue);
|
|
380
|
-
|
|
381
|
-
// Subscribe to changes
|
|
382
|
-
stateObj.subscribe((val: any) => {
|
|
383
|
-
const transformed = transform(val);
|
|
384
|
-
method.call(this, transformed);
|
|
385
|
-
});
|
|
386
|
-
} else {
|
|
387
|
-
console.warn(
|
|
388
|
-
`[Jux] ${this.constructor.name}.sync('${property}'): ` +
|
|
389
|
-
`No method .${property}() found. Property will not be synced.`
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
renderTo(juxComponent: any): this {
|
|
396
|
-
if (!juxComponent?._id) {
|
|
397
|
-
throw new Error(`${this.constructor.name}.renderTo: Invalid component`);
|
|
398
|
-
}
|
|
399
|
-
return this.render(`#${juxComponent._id}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
403
|
-
* PROPS ACCESSOR - Read-only access to component state
|
|
404
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* ✅ Read-only accessor for component state
|
|
408
|
-
* Provides clear separation between setters (fluent methods) and getters
|
|
409
|
-
*
|
|
410
|
-
* @example
|
|
411
|
-
* const myCard = card('example')
|
|
412
|
-
* .title('Hello') // ✅ SETTER (fluent)
|
|
413
|
-
* .content('World'); // ✅ SETTER (fluent)
|
|
414
|
-
*
|
|
415
|
-
* console.log(myCard.props.title); // ✅ GETTER: 'Hello'
|
|
416
|
-
* console.log(myCard.props.content); // ✅ GETTER: 'World'
|
|
417
|
-
*/
|
|
418
|
-
get props(): Readonly<TState> {
|
|
419
|
-
return this.state as Readonly<TState>;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
@@ -1,227 +0,0 @@
|
|
|
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
|
-
}
|