juxscript 1.1.239 → 1.1.243
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/index.js +7 -137
- package/lib/components/dataframe.ts +0 -569
- package/lib/components/tag.ts +68 -0
- package/lib/styles/shadcn.css +20 -7
- package/lib/utils/idgen.ts +6 -0
- package/package.json +5 -6
- package/index.d.ts +0 -239
- package/index.d.ts.map +0 -1
- package/lib/components/alert.d.ts +0 -36
- package/lib/components/alert.d.ts.map +0 -1
- package/lib/components/alert.js +0 -172
- package/lib/components/alert.ts +0 -219
- package/lib/components/app.d.ts +0 -89
- package/lib/components/app.d.ts.map +0 -1
- package/lib/components/app.js +0 -175
- package/lib/components/app.ts +0 -247
- package/lib/components/badge.d.ts +0 -26
- package/lib/components/badge.d.ts.map +0 -1
- package/lib/components/badge.js +0 -91
- package/lib/components/badge.ts +0 -118
- package/lib/components/base/Animations.d.ts +0 -36
- package/lib/components/base/Animations.d.ts.map +0 -1
- package/lib/components/base/Animations.js +0 -70
- package/lib/components/base/Animations.ts +0 -112
- package/lib/components/base/BaseComponent.d.ts +0 -294
- package/lib/components/base/BaseComponent.d.ts.map +0 -1
- package/lib/components/base/BaseComponent.js +0 -735
- package/lib/components/base/BaseComponent.ts +0 -884
- package/lib/components/base/FormInput.d.ts +0 -77
- package/lib/components/base/FormInput.d.ts.map +0 -1
- package/lib/components/base/FormInput.js +0 -171
- package/lib/components/base/FormInput.ts +0 -237
- package/lib/components/blueprint.d.ts +0 -40
- package/lib/components/blueprint.d.ts.map +0 -1
- package/lib/components/blueprint.js +0 -327
- package/lib/components/button.d.ts +0 -70
- package/lib/components/button.d.ts.map +0 -1
- package/lib/components/button.js +0 -177
- package/lib/components/button.ts +0 -237
- package/lib/components/card.d.ts +0 -35
- package/lib/components/card.d.ts.map +0 -1
- package/lib/components/card.js +0 -130
- package/lib/components/card.ts +0 -177
- package/lib/components/chart.d.ts +0 -49
- package/lib/components/chart.d.ts.map +0 -1
- package/lib/components/chart.js +0 -205
- package/lib/components/chart.ts +0 -254
- package/lib/components/checkbox.d.ts +0 -33
- package/lib/components/checkbox.d.ts.map +0 -1
- package/lib/components/checkbox.js +0 -202
- package/lib/components/checkbox.ts +0 -260
- package/lib/components/code.d.ts +0 -52
- package/lib/components/code.d.ts.map +0 -1
- package/lib/components/code.js +0 -201
- package/lib/components/code.ts +0 -260
- package/lib/components/container.d.ts +0 -60
- package/lib/components/container.d.ts.map +0 -1
- package/lib/components/container.js +0 -195
- package/lib/components/container.ts +0 -259
- package/lib/components/data.d.ts +0 -36
- package/lib/components/data.d.ts.map +0 -1
- package/lib/components/data.js +0 -110
- package/lib/components/data.ts +0 -135
- package/lib/components/dataframe/DataFrameSource.d.ts +0 -118
- package/lib/components/dataframe/DataFrameSource.d.ts.map +0 -1
- package/lib/components/dataframe/DataFrameSource.js +0 -421
- package/lib/components/dataframe/DataFrameSource.ts +0 -532
- package/lib/components/dataframe/ImportSettingsModal.d.ts +0 -60
- package/lib/components/dataframe/ImportSettingsModal.d.ts.map +0 -1
- package/lib/components/dataframe/ImportSettingsModal.js +0 -442
- package/lib/components/dataframe/ImportSettingsModal.ts +0 -531
- package/lib/components/dataframe.d.ts +0 -110
- package/lib/components/dataframe.d.ts.map +0 -1
- package/lib/components/dataframe.js +0 -470
- package/lib/components/datepicker.d.ts +0 -40
- package/lib/components/datepicker.d.ts.map +0 -1
- package/lib/components/datepicker.js +0 -193
- package/lib/components/datepicker.ts +0 -251
- package/lib/components/dialog.d.ts +0 -39
- package/lib/components/dialog.d.ts.map +0 -1
- package/lib/components/dialog.js +0 -131
- package/lib/components/dialog.ts +0 -178
- package/lib/components/divider.d.ts +0 -31
- package/lib/components/divider.d.ts.map +0 -1
- package/lib/components/divider.js +0 -72
- package/lib/components/divider.ts +0 -104
- package/lib/components/dropdown-menu.d.ts +0 -42
- package/lib/components/dropdown-menu.d.ts.map +0 -1
- package/lib/components/dropdown-menu.js +0 -177
- package/lib/components/dropdown-menu.ts +0 -214
- package/lib/components/dropdown.d.ts +0 -41
- package/lib/components/dropdown.d.ts.map +0 -1
- package/lib/components/dropdown.js +0 -136
- package/lib/components/dropdown.ts +0 -188
- package/lib/components/element.d.ts +0 -51
- package/lib/components/element.d.ts.map +0 -1
- package/lib/components/element.js +0 -209
- package/lib/components/element.ts +0 -271
- package/lib/components/event-chain.d.ts +0 -9
- package/lib/components/event-chain.d.ts.map +0 -1
- package/lib/components/event-chain.js +0 -33
- package/lib/components/fileupload.d.ts +0 -98
- package/lib/components/fileupload.d.ts.map +0 -1
- package/lib/components/fileupload.js +0 -351
- package/lib/components/fileupload.ts +0 -449
- package/lib/components/grid.d.ts +0 -88
- package/lib/components/grid.d.ts.map +0 -1
- package/lib/components/grid.js +0 -208
- package/lib/components/grid.ts +0 -295
- package/lib/components/heading.d.ts +0 -25
- package/lib/components/heading.d.ts.map +0 -1
- package/lib/components/heading.js +0 -83
- package/lib/components/heading.ts +0 -113
- package/lib/components/helpers.d.ts +0 -9
- package/lib/components/helpers.d.ts.map +0 -1
- package/lib/components/helpers.js +0 -30
- package/lib/components/helpers.ts +0 -41
- package/lib/components/hero.d.ts +0 -60
- package/lib/components/hero.d.ts.map +0 -1
- package/lib/components/hero.js +0 -239
- package/lib/components/hero.ts +0 -302
- package/lib/components/history/StateHistory.d.ts +0 -91
- package/lib/components/history/StateHistory.d.ts.map +0 -1
- package/lib/components/history/StateHistory.js +0 -154
- package/lib/components/history/StateHistory.ts +0 -200
- package/lib/components/icon.d.ts +0 -36
- package/lib/components/icon.d.ts.map +0 -1
- package/lib/components/icon.js +0 -135
- package/lib/components/icon.ts +0 -182
- package/lib/components/icons.d.ts +0 -25
- package/lib/components/icons.d.ts.map +0 -1
- package/lib/components/icons.js +0 -440
- package/lib/components/icons.ts +0 -464
- package/lib/components/image.d.ts +0 -42
- package/lib/components/image.d.ts.map +0 -1
- package/lib/components/image.js +0 -204
- package/lib/components/image.ts +0 -260
- package/lib/components/include.d.ts +0 -86
- package/lib/components/include.d.ts.map +0 -1
- package/lib/components/include.js +0 -238
- package/lib/components/include.ts +0 -281
- package/lib/components/input.d.ts +0 -85
- package/lib/components/input.d.ts.map +0 -1
- package/lib/components/input.js +0 -362
- package/lib/components/input.ts +0 -473
- package/lib/components/layer.d.ts +0 -72
- package/lib/components/layer.d.ts.map +0 -1
- package/lib/components/layer.js +0 -219
- package/lib/components/layer.ts +0 -304
- package/lib/components/link.d.ts +0 -41
- package/lib/components/link.d.ts.map +0 -1
- package/lib/components/link.js +0 -216
- package/lib/components/link.ts +0 -268
- package/lib/components/list.d.ts +0 -83
- package/lib/components/list.d.ts.map +0 -1
- package/lib/components/list.js +0 -314
- package/lib/components/list.ts +0 -423
- package/lib/components/loading.d.ts +0 -25
- package/lib/components/loading.d.ts.map +0 -1
- package/lib/components/loading.js +0 -76
- package/lib/components/loading.ts +0 -104
- package/lib/components/menu.d.ts +0 -38
- package/lib/components/menu.d.ts.map +0 -1
- package/lib/components/menu.js +0 -205
- package/lib/components/menu.ts +0 -279
- package/lib/components/modal.d.ts +0 -97
- package/lib/components/modal.d.ts.map +0 -1
- package/lib/components/modal.js +0 -463
- package/lib/components/modal.ts +0 -576
- package/lib/components/nav.d.ts +0 -46
- package/lib/components/nav.d.ts.map +0 -1
- package/lib/components/nav.js +0 -193
- package/lib/components/nav.ts +0 -261
- package/lib/components/paragraph.d.ts +0 -30
- package/lib/components/paragraph.d.ts.map +0 -1
- package/lib/components/paragraph.js +0 -93
- package/lib/components/paragraph.ts +0 -123
- package/lib/components/pen.d.ts +0 -125
- package/lib/components/pen.d.ts.map +0 -1
- package/lib/components/pen.js +0 -443
- package/lib/components/pen.ts +0 -567
- package/lib/components/progress.d.ts +0 -40
- package/lib/components/progress.d.ts.map +0 -1
- package/lib/components/progress.js +0 -116
- package/lib/components/progress.ts +0 -163
- package/lib/components/radio.d.ts +0 -43
- package/lib/components/radio.d.ts.map +0 -1
- package/lib/components/radio.js +0 -226
- package/lib/components/radio.ts +0 -303
- package/lib/components/registry.d.ts +0 -34
- package/lib/components/registry.d.ts.map +0 -1
- package/lib/components/registry.js +0 -163
- package/lib/components/registry.ts +0 -193
- package/lib/components/req.d.ts +0 -155
- package/lib/components/req.d.ts.map +0 -1
- package/lib/components/req.js +0 -253
- package/lib/components/req.ts +0 -303
- package/lib/components/script.d.ts +0 -14
- package/lib/components/script.d.ts.map +0 -1
- package/lib/components/script.js +0 -33
- package/lib/components/script.ts +0 -41
- package/lib/components/select.d.ts +0 -42
- package/lib/components/select.d.ts.map +0 -1
- package/lib/components/select.js +0 -209
- package/lib/components/select.ts +0 -281
- package/lib/components/sidebar.d.ts +0 -59
- package/lib/components/sidebar.d.ts.map +0 -1
- package/lib/components/sidebar.js +0 -298
- package/lib/components/sidebar.ts +0 -395
- package/lib/components/stack/BaseStack.d.ts +0 -65
- package/lib/components/stack/BaseStack.d.ts.map +0 -1
- package/lib/components/stack/BaseStack.js +0 -274
- package/lib/components/stack/BaseStack.ts +0 -328
- package/lib/components/stack/HStack.d.ts +0 -18
- package/lib/components/stack/HStack.d.ts.map +0 -1
- package/lib/components/stack/HStack.js +0 -22
- package/lib/components/stack/HStack.ts +0 -25
- package/lib/components/stack/VStack.d.ts +0 -19
- package/lib/components/stack/VStack.d.ts.map +0 -1
- package/lib/components/stack/VStack.js +0 -23
- package/lib/components/stack/VStack.ts +0 -26
- package/lib/components/stack/ZStack.d.ts +0 -18
- package/lib/components/stack/ZStack.d.ts.map +0 -1
- package/lib/components/stack/ZStack.js +0 -22
- package/lib/components/stack/ZStack.ts +0 -25
- package/lib/components/style.d.ts +0 -14
- package/lib/components/style.d.ts.map +0 -1
- package/lib/components/style.js +0 -33
- package/lib/components/style.ts +0 -41
- package/lib/components/switch.d.ts +0 -34
- package/lib/components/switch.d.ts.map +0 -1
- package/lib/components/switch.js +0 -209
- package/lib/components/switch.ts +0 -272
- package/lib/components/table.d.ts +0 -137
- package/lib/components/table.d.ts.map +0 -1
- package/lib/components/table.js +0 -1019
- package/lib/components/table.ts +0 -1225
- package/lib/components/tabs.d.ts +0 -53
- package/lib/components/tabs.d.ts.map +0 -1
- package/lib/components/tabs.js +0 -275
- package/lib/components/tabs.ts +0 -349
- package/lib/components/theme-toggle.d.ts +0 -45
- package/lib/components/theme-toggle.d.ts.map +0 -1
- package/lib/components/theme-toggle.js +0 -218
- package/lib/components/theme-toggle.ts +0 -297
- package/lib/components/tooltip.d.ts +0 -31
- package/lib/components/tooltip.d.ts.map +0 -1
- package/lib/components/tooltip.js +0 -112
- package/lib/components/tooltip.ts +0 -148
- package/lib/components/watcher.d.ts +0 -195
- package/lib/components/watcher.d.ts.map +0 -1
- package/lib/components/watcher.js +0 -241
- package/lib/components/watcher.ts +0 -261
- package/lib/components/write.d.ts +0 -107
- package/lib/components/write.d.ts.map +0 -1
- package/lib/components/write.js +0 -222
- package/lib/components/write.ts +0 -272
- package/lib/data/DataPipeline.d.ts +0 -113
- package/lib/data/DataPipeline.d.ts.map +0 -1
- package/lib/data/DataPipeline.js +0 -359
- package/lib/data/DataPipeline.ts +0 -452
- package/lib/facades/dataframe.jux +0 -0
- package/lib/globals.d.ts +0 -21
- package/lib/reactivity/state.d.ts +0 -36
- package/lib/reactivity/state.d.ts.map +0 -1
- package/lib/reactivity/state.js +0 -67
- package/lib/reactivity/state.ts +0 -78
- package/lib/storage/DataFrame.d.ts +0 -284
- package/lib/storage/DataFrame.d.ts.map +0 -1
- package/lib/storage/DataFrame.js +0 -1022
- package/lib/storage/DataFrame.ts +0 -1195
- package/lib/storage/DataFrameSource.d.ts +0 -158
- package/lib/storage/DataFrameSource.d.ts.map +0 -1
- package/lib/storage/DataFrameSource.js +0 -409
- package/lib/storage/DataFrameSource.ts +0 -556
- package/lib/storage/FileStorage.d.ts +0 -53
- package/lib/storage/FileStorage.d.ts.map +0 -1
- package/lib/storage/FileStorage.js +0 -80
- package/lib/storage/FileStorage.ts +0 -95
- package/lib/storage/IndexedDBDriver.d.ts +0 -75
- package/lib/storage/IndexedDBDriver.d.ts.map +0 -1
- package/lib/storage/IndexedDBDriver.js +0 -177
- package/lib/storage/IndexedDBDriver.ts +0 -226
- package/lib/storage/TabularDriver.d.ts +0 -118
- package/lib/storage/TabularDriver.d.ts.map +0 -1
- package/lib/storage/TabularDriver.js +0 -731
- package/lib/storage/TabularDriver.ts +0 -874
- package/lib/utils/codeparser.d.ts +0 -29
- package/lib/utils/codeparser.d.ts.map +0 -1
- package/lib/utils/codeparser.js +0 -409
- package/lib/utils/fetch.d.ts +0 -176
- package/lib/utils/fetch.d.ts.map +0 -1
- package/lib/utils/fetch.js +0 -427
- package/lib/utils/formatId.d.ts +0 -16
- package/lib/utils/formatId.d.ts.map +0 -1
- package/lib/utils/formatId.js +0 -27
- package/lib/utils/path-resolver.js +0 -23
|
@@ -1,884 +0,0 @@
|
|
|
1
|
-
import { State } from '../../reactivity/state.js';
|
|
2
|
-
import { getOrCreateContainer } from '../helpers.js';
|
|
3
|
-
import { registry } from '../registry.js';
|
|
4
|
-
import { stateHistory } from '../history/StateHistory.js';
|
|
5
|
-
import { formatIdAsLabel } from '../../utils/formatId.js';
|
|
6
|
-
import { applyAnimations, Animations } from './Animations.js'; // ✅ Import Animations
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Base state interface that ALL component states must extend
|
|
10
|
-
*/
|
|
11
|
-
export interface BaseState {
|
|
12
|
-
visible?: boolean;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
loading?: boolean;
|
|
15
|
-
class?: string;
|
|
16
|
-
style?: string;
|
|
17
|
-
attributes?: Record<string, string>;
|
|
18
|
-
// ✅ Form-specific properties (optional for all components)
|
|
19
|
-
label?: string;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
name?: string;
|
|
22
|
-
errorMessage?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Abstract base class for all JUX components
|
|
27
|
-
* Provides common storage, event routing, and lifecycle methods
|
|
28
|
-
*
|
|
29
|
-
* Children must provide:
|
|
30
|
-
* - TRIGGER_EVENTS constant (readonly string[])
|
|
31
|
-
* - CALLBACK_EVENTS constant (readonly string[])
|
|
32
|
-
* - render() implementation
|
|
33
|
-
*/
|
|
34
|
-
export abstract class BaseComponent<TState extends BaseState = BaseState> {
|
|
35
|
-
// Common properties (all components have these)
|
|
36
|
-
state: TState;
|
|
37
|
-
container: HTMLElement | null = null;
|
|
38
|
-
_id: string;
|
|
39
|
-
id: string;
|
|
40
|
-
|
|
41
|
-
// Event & sync storage (populated by bind() and sync())
|
|
42
|
-
protected _bindings: Array<{ event: string, handler: Function }> = [];
|
|
43
|
-
protected _syncBindings: Array<{
|
|
44
|
-
property: string,
|
|
45
|
-
stateObj: State<any>,
|
|
46
|
-
toState?: Function,
|
|
47
|
-
toComponent?: Function
|
|
48
|
-
}> = [];
|
|
49
|
-
protected _triggerHandlers: Map<string, Function> = new Map();
|
|
50
|
-
protected _callbackHandlers: Map<string, Function> = new Map();
|
|
51
|
-
protected _isUpdatingSync: boolean = false;
|
|
52
|
-
|
|
53
|
-
// Form-specific protected properties (only used by form components)
|
|
54
|
-
protected _inputElement: HTMLElement | null = null;
|
|
55
|
-
protected _labelElement: HTMLLabelElement | null = null;
|
|
56
|
-
protected _errorElement: HTMLElement | null = null;
|
|
57
|
-
protected _onValidate?: (value: any) => boolean | string;
|
|
58
|
-
protected _hasBeenValidated: boolean = false;
|
|
59
|
-
|
|
60
|
-
constructor(id: string, initialState: TState) {
|
|
61
|
-
this._id = id;
|
|
62
|
-
this.id = id;
|
|
63
|
-
|
|
64
|
-
const stateWithDefaults: TState = {
|
|
65
|
-
visible: true,
|
|
66
|
-
disabled: false,
|
|
67
|
-
loading: false,
|
|
68
|
-
class: '',
|
|
69
|
-
style: '',
|
|
70
|
-
attributes: {},
|
|
71
|
-
...initialState
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
this.state = new Proxy(stateWithDefaults, {
|
|
75
|
-
set: (target, prop, value) => {
|
|
76
|
-
const key = prop as keyof TState;
|
|
77
|
-
const oldValue = target[key];
|
|
78
|
-
|
|
79
|
-
if (oldValue !== value) {
|
|
80
|
-
stateHistory.recordStateChange(this._id, prop as string, oldValue, value);
|
|
81
|
-
target[key] = value;
|
|
82
|
-
this.update(prop as string, value);
|
|
83
|
-
|
|
84
|
-
if (!this._isUpdatingSync) {
|
|
85
|
-
this._notifySyncedState(prop as string, value);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// ✅ Apply ONLY animation extensions
|
|
93
|
-
applyAnimations(this);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
97
|
-
* ABSTRACT METHODS (Child must implement)
|
|
98
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
99
|
-
|
|
100
|
-
protected abstract getTriggerEvents(): readonly string[];
|
|
101
|
-
protected abstract getCallbackEvents(): readonly string[];
|
|
102
|
-
abstract render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* REACTIVE UPDATE HOOK (PUBLIC, CONCRETE)
|
|
106
|
-
* Called automatically when this.state[prop] changes via Proxy.
|
|
107
|
-
* Default implementation handles base properties.
|
|
108
|
-
* Children can override to add component-specific logic.
|
|
109
|
-
*/
|
|
110
|
-
update(prop: string, value: any): void {
|
|
111
|
-
if (!this.container) return;
|
|
112
|
-
|
|
113
|
-
const el = this.container.querySelector(`#${this._id}`) as HTMLElement;
|
|
114
|
-
if (!el) return;
|
|
115
|
-
|
|
116
|
-
// Handle base properties
|
|
117
|
-
switch (prop) {
|
|
118
|
-
case 'visible':
|
|
119
|
-
el.style.display = value ? '' : 'none';
|
|
120
|
-
break;
|
|
121
|
-
|
|
122
|
-
case 'class':
|
|
123
|
-
const baseClasses = el.className.split(' ').filter(c => c.startsWith('jux-'));
|
|
124
|
-
const userClasses = value.split(' ').filter((c: string) => c);
|
|
125
|
-
el.className = [...baseClasses, ...userClasses].join(' ');
|
|
126
|
-
break;
|
|
127
|
-
|
|
128
|
-
case 'style':
|
|
129
|
-
el.setAttribute('style', value);
|
|
130
|
-
break;
|
|
131
|
-
|
|
132
|
-
case 'disabled':
|
|
133
|
-
el.setAttribute('aria-disabled', String(value));
|
|
134
|
-
const inputs = el.querySelectorAll('input, button, select, textarea');
|
|
135
|
-
inputs.forEach(input => {
|
|
136
|
-
(input as HTMLInputElement).disabled = value;
|
|
137
|
-
});
|
|
138
|
-
break;
|
|
139
|
-
|
|
140
|
-
case 'loading':
|
|
141
|
-
el.classList.toggle('jux-loading', value);
|
|
142
|
-
el.setAttribute('aria-busy', String(value));
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* ✨ Notify external State<T> objects when component state changes
|
|
149
|
-
* Prevents infinite loops with guard flag
|
|
150
|
-
*/
|
|
151
|
-
protected _notifySyncedState(prop: string, value: any): void {
|
|
152
|
-
const syncBinding = this._syncBindings.find(b => b.property === prop);
|
|
153
|
-
|
|
154
|
-
if (syncBinding) {
|
|
155
|
-
const { stateObj, toState } = syncBinding;
|
|
156
|
-
|
|
157
|
-
// Set guard flag to prevent recursion
|
|
158
|
-
this._isUpdatingSync = true;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const transformedValue = toState ? toState(value) : value;
|
|
162
|
-
stateObj.set(transformedValue);
|
|
163
|
-
} finally {
|
|
164
|
-
// Always clear guard flag
|
|
165
|
-
this._isUpdatingSync = false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
171
|
-
* COMMON FLUENT API (Inherited by all components)
|
|
172
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Set component style
|
|
176
|
-
*/
|
|
177
|
-
style(value: string): this {
|
|
178
|
-
this.state.style = value; // ✅ Triggers update()
|
|
179
|
-
return this;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Set component class
|
|
184
|
-
*/
|
|
185
|
-
class(value: string): this {
|
|
186
|
-
this.state.class = value; // ✅ Triggers update()
|
|
187
|
-
return this;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
191
|
-
* CSS CLASS MANAGEMENT
|
|
192
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Add a CSS class to the component
|
|
196
|
-
*/
|
|
197
|
-
addClass(value: string): this {
|
|
198
|
-
const current = this.state.class || '';
|
|
199
|
-
const classes = current.split(' ').filter((c: string) => c && c !== value);
|
|
200
|
-
classes.push(value);
|
|
201
|
-
this.state.class = classes.join(' '); // ✅ Triggers update()
|
|
202
|
-
return this;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Remove a CSS class from the component
|
|
207
|
-
*/
|
|
208
|
-
removeClass(value: string): this {
|
|
209
|
-
const current = this.state.class || '';
|
|
210
|
-
const classes = current.split(' ').filter((c: string) => c && c !== value);
|
|
211
|
-
this.state.class = classes.join(' '); // ✅ Triggers update()
|
|
212
|
-
return this;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Toggle a CSS class on the component
|
|
217
|
-
*/
|
|
218
|
-
toggleClass(value: string): this {
|
|
219
|
-
const current = this.state.class || '';
|
|
220
|
-
const hasClass = current.split(' ').includes(value);
|
|
221
|
-
return hasClass ? this.removeClass(value) : this.addClass(value);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
225
|
-
* VISIBILITY CONTROL
|
|
226
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Set component visibility
|
|
230
|
-
*/
|
|
231
|
-
visible(value: boolean): this {
|
|
232
|
-
this.state.visible = value; // ✅ Triggers update()
|
|
233
|
-
return this;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Show the component
|
|
238
|
-
*/
|
|
239
|
-
show(): this {
|
|
240
|
-
return this.visible(true);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Hide the component
|
|
245
|
-
*/
|
|
246
|
-
hide(): this {
|
|
247
|
-
return this.visible(false);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Toggle component visibility
|
|
252
|
-
*/
|
|
253
|
-
toggleVisibility(): this {
|
|
254
|
-
const isVisible = (this.state as any).visible ?? true;
|
|
255
|
-
return this.visible(!isVisible);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
259
|
-
* ATTRIBUTE MANAGEMENT
|
|
260
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Set a single HTML attribute
|
|
264
|
-
*/
|
|
265
|
-
attr(name: string, value: string): this {
|
|
266
|
-
const attrs = (this.state as any).attributes || {};
|
|
267
|
-
(this.state as any).attributes = { ...attrs, [name]: value };
|
|
268
|
-
if (this.container) this.container.setAttribute(name, value);
|
|
269
|
-
return this;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Set multiple HTML attributes
|
|
274
|
-
*/
|
|
275
|
-
attrs(attributes: Record<string, string>): this {
|
|
276
|
-
Object.entries(attributes).forEach(([name, value]) => {
|
|
277
|
-
this.attr(name, value);
|
|
278
|
-
});
|
|
279
|
-
return this;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Remove an HTML attribute
|
|
284
|
-
*/
|
|
285
|
-
removeAttr(name: string): this {
|
|
286
|
-
const attrs = (this.state as any).attributes || {};
|
|
287
|
-
delete attrs[name];
|
|
288
|
-
if (this.container) this.container.removeAttribute(name);
|
|
289
|
-
return this;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
293
|
-
* DISABLED STATE
|
|
294
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Set disabled state for interactive elements
|
|
298
|
-
*/
|
|
299
|
-
disabled(value: boolean): this {
|
|
300
|
-
this.state.disabled = value; // ✅ Triggers update()
|
|
301
|
-
return this;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Enable the component
|
|
306
|
-
*/
|
|
307
|
-
enable(): this {
|
|
308
|
-
return this.disabled(false);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Disable the component
|
|
313
|
-
*/
|
|
314
|
-
disable(): this {
|
|
315
|
-
return this.disabled(true);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
319
|
-
* LOADING STATE
|
|
320
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Set loading state
|
|
324
|
-
*/
|
|
325
|
-
loading(value: boolean): this {
|
|
326
|
-
this.state.loading = value; // ✅ Triggers update()
|
|
327
|
-
return this;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
331
|
-
* FOCUS MANAGEMENT
|
|
332
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Focus the first focusable element in the component
|
|
336
|
-
*/
|
|
337
|
-
focus(): this {
|
|
338
|
-
if (this.container) {
|
|
339
|
-
const focusable = this.container.querySelector('input, button, select, textarea, [tabindex]');
|
|
340
|
-
if (focusable) (focusable as HTMLElement).focus();
|
|
341
|
-
}
|
|
342
|
-
return this;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Blur the currently focused element in the component
|
|
347
|
-
*/
|
|
348
|
-
blur(): this {
|
|
349
|
-
if (this.container) {
|
|
350
|
-
const focused = this.container.querySelector(':focus');
|
|
351
|
-
if (focused) (focused as HTMLElement).blur();
|
|
352
|
-
}
|
|
353
|
-
return this;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
357
|
-
* DOM MANIPULATION
|
|
358
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Remove the component from the DOM
|
|
362
|
-
*/
|
|
363
|
-
remove(): this {
|
|
364
|
-
if (this.container) {
|
|
365
|
-
this.container.remove();
|
|
366
|
-
this.container = null;
|
|
367
|
-
|
|
368
|
-
// ✅ Unregister when removed
|
|
369
|
-
registry.unregister(this._id);
|
|
370
|
-
}
|
|
371
|
-
return this;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
375
|
-
* EVENT BINDING (Shared logic)
|
|
376
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
377
|
-
|
|
378
|
-
bind(event: string, handler: Function): this {
|
|
379
|
-
// ✅ Record bind event
|
|
380
|
-
stateHistory.recordEvent(this._id, 'bind', event, { handlerName: handler.name });
|
|
381
|
-
|
|
382
|
-
if (this._isTriggerEvent(event)) {
|
|
383
|
-
this._triggerHandlers.set(event, handler);
|
|
384
|
-
} else if (this._isCallbackEvent(event)) {
|
|
385
|
-
this._callbackHandlers.set(event, handler);
|
|
386
|
-
} else {
|
|
387
|
-
this._bindings.push({ event, handler });
|
|
388
|
-
}
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
sync(property: string, stateObj: State<any>, toStateOrTransform?: Function, toComponent?: Function): this {
|
|
393
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
394
|
-
throw new Error(`${this.constructor.name}.sync: Expected a State object for property "${property}"`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const actualToState = (toComponent !== undefined) ? toStateOrTransform : undefined;
|
|
398
|
-
const actualToComponent = (toComponent !== undefined) ? toComponent : toStateOrTransform;
|
|
399
|
-
|
|
400
|
-
// ✅ Record sync event
|
|
401
|
-
stateHistory.recordEvent(this._id, 'sync', property, {
|
|
402
|
-
hasToState: !!actualToState,
|
|
403
|
-
hasToComponent: !!actualToComponent
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
this._syncBindings.push({
|
|
407
|
-
property,
|
|
408
|
-
stateObj,
|
|
409
|
-
toState: actualToState,
|
|
410
|
-
toComponent: actualToComponent
|
|
411
|
-
});
|
|
412
|
-
return this;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
protected _isTriggerEvent(event: string): boolean {
|
|
416
|
-
return this.getTriggerEvents().includes(event);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
protected _isCallbackEvent(event: string): boolean {
|
|
420
|
-
return this.getCallbackEvents().includes(event);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
protected _triggerCallback(eventName: string, ...args: any[]): void {
|
|
424
|
-
// ✅ Record callback event
|
|
425
|
-
stateHistory.recordEvent(this._id, 'callback', eventName, { argsCount: args.length });
|
|
426
|
-
|
|
427
|
-
if (this._callbackHandlers.has(eventName)) {
|
|
428
|
-
const handler = this._callbackHandlers.get(eventName)!;
|
|
429
|
-
handler(...args);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
434
|
-
* FLUENT EVENT SHORTHANDS
|
|
435
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Register a click event handler
|
|
439
|
-
*/
|
|
440
|
-
click(handler: (e: MouseEvent) => void): this {
|
|
441
|
-
return this.bind('click', handler);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Register a double-click event handler
|
|
446
|
-
*/
|
|
447
|
-
dblclick(handler: (e: MouseEvent) => void): this {
|
|
448
|
-
return this.bind('dblclick', handler);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Register a change event handler
|
|
453
|
-
*/
|
|
454
|
-
change(handler: (e: Event) => void): this {
|
|
455
|
-
return this.bind('change', handler);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Register an input event handler
|
|
460
|
-
*/
|
|
461
|
-
input(handler: (e: Event) => void): this {
|
|
462
|
-
return this.bind('input', handler);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Register a mouseover event handler
|
|
467
|
-
*/
|
|
468
|
-
hover(handler: (e: MouseEvent) => void): this {
|
|
469
|
-
return this.bind('mouseover', handler);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Register a mouseout event handler
|
|
474
|
-
*/
|
|
475
|
-
mouseout(handler: (e: MouseEvent) => void): this {
|
|
476
|
-
return this.bind('mouseout', handler);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Register a mousedown event handler
|
|
481
|
-
*/
|
|
482
|
-
mousedown(handler: (e: MouseEvent) => void): this {
|
|
483
|
-
return this.bind('mousedown', handler);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Register a mouseup event handler
|
|
488
|
-
*/
|
|
489
|
-
mouseup(handler: (e: MouseEvent) => void): this {
|
|
490
|
-
return this.bind('mouseup', handler);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Register a focus event handler (renamed to onFocus to avoid conflict with focus() method)
|
|
495
|
-
*/
|
|
496
|
-
onFocus(handler: (e: FocusEvent) => void): this {
|
|
497
|
-
return this.bind('focus', handler);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Register a blur event handler (renamed to onBlur to avoid conflict with blur() method)
|
|
502
|
-
*/
|
|
503
|
-
onBlur(handler: (e: FocusEvent) => void): this {
|
|
504
|
-
return this.bind('blur', handler);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Register a keydown event handler
|
|
509
|
-
*/
|
|
510
|
-
keydown(handler: (e: KeyboardEvent) => void): this {
|
|
511
|
-
return this.bind('keydown', handler);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Register a keyup event handler
|
|
516
|
-
*/
|
|
517
|
-
keyup(handler: (e: KeyboardEvent) => void): this {
|
|
518
|
-
return this.bind('keyup', handler);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Register a keypress event handler
|
|
523
|
-
*/
|
|
524
|
-
keypress(handler: (e: KeyboardEvent) => void): this {
|
|
525
|
-
return this.bind('keypress', handler);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Register a submit event handler
|
|
530
|
-
*/
|
|
531
|
-
submit(handler: (e: Event) => void): this {
|
|
532
|
-
return this.bind('submit', handler);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Register a scroll event handler
|
|
537
|
-
*/
|
|
538
|
-
scroll(handler: (e: Event) => void): this {
|
|
539
|
-
return this.bind('scroll', handler);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Register a contextmenu (right-click) event handler
|
|
544
|
-
*/
|
|
545
|
-
contextmenu(handler: (e: MouseEvent) => void): this {
|
|
546
|
-
return this.bind('contextmenu', handler);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
550
|
-
* TIME-TRAVEL DEBUGGING (PUBLIC API)
|
|
551
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Roll back to previous state
|
|
555
|
-
*/
|
|
556
|
-
rollback(): this {
|
|
557
|
-
const snapshot = stateHistory.moveBack();
|
|
558
|
-
if (snapshot && snapshot.componentId === this._id) {
|
|
559
|
-
stateHistory.startReplay();
|
|
560
|
-
(this.state as any)[snapshot.property] = snapshot.oldValue;
|
|
561
|
-
stateHistory.endReplay();
|
|
562
|
-
}
|
|
563
|
-
return this;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Roll forward to next state
|
|
568
|
-
*/
|
|
569
|
-
rollforward(): this {
|
|
570
|
-
const snapshot = stateHistory.moveForward();
|
|
571
|
-
if (snapshot && snapshot.componentId === this._id) {
|
|
572
|
-
stateHistory.startReplay();
|
|
573
|
-
(this.state as any)[snapshot.property] = snapshot.newValue;
|
|
574
|
-
stateHistory.endReplay();
|
|
575
|
-
}
|
|
576
|
-
return this;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Get complete timeline of this component's changes
|
|
581
|
-
*/
|
|
582
|
-
timeline(): Array<any> {
|
|
583
|
-
const stateChanges = stateHistory.getComponentHistory(this._id);
|
|
584
|
-
const events = stateHistory.getComponentEvents(this._id);
|
|
585
|
-
return [...stateChanges, ...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Get only state changes for this component
|
|
590
|
-
*/
|
|
591
|
-
stateHistory() {
|
|
592
|
-
return stateHistory.getComponentHistory(this._id);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Get only events for this component
|
|
597
|
-
*/
|
|
598
|
-
eventHistory() {
|
|
599
|
-
return stateHistory.getComponentEvents(this._id);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
603
|
-
* COMMON RENDER HELPERS
|
|
604
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
605
|
-
|
|
606
|
-
protected _setupContainer(targetId?: string | HTMLElement | BaseComponent<any>): HTMLElement {
|
|
607
|
-
// Determine Target Container Type
|
|
608
|
-
|
|
609
|
-
let container: HTMLElement;
|
|
610
|
-
if (targetId) {
|
|
611
|
-
if (typeof targetId === 'string') {
|
|
612
|
-
const id = targetId.startsWith('#') ? targetId.slice(1) : targetId;
|
|
613
|
-
const target = document.getElementById(id);
|
|
614
|
-
if (target) {
|
|
615
|
-
container = target;
|
|
616
|
-
} else {
|
|
617
|
-
console.warn(`[Jux] Target "${targetId}" not found, creating it with graceful fallback`);
|
|
618
|
-
container = getOrCreateContainer(id);
|
|
619
|
-
}
|
|
620
|
-
} else if (targetId instanceof HTMLElement) {
|
|
621
|
-
container = targetId;
|
|
622
|
-
} else if (targetId instanceof BaseComponent) {
|
|
623
|
-
container = getOrCreateContainer(targetId._id);
|
|
624
|
-
} else {
|
|
625
|
-
throw new Error(`[Jux] Invalid targetId type: ${typeof targetId}`);
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
container = getOrCreateContainer(this._id);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
this.container = container;
|
|
632
|
-
|
|
633
|
-
// ✅ Auto-register component when container is set up
|
|
634
|
-
registry.register(this);
|
|
635
|
-
|
|
636
|
-
return container;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
protected _wireStandardEvents(element: HTMLElement): void {
|
|
640
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
641
|
-
element.addEventListener(event, handler as EventListener);
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Wire sync subscriptions with guard flag
|
|
647
|
-
*/
|
|
648
|
-
protected _wireAllSyncs(): void {
|
|
649
|
-
this._syncBindings.forEach(({ property, stateObj, toComponent }) => {
|
|
650
|
-
const transform = toComponent || ((v: any) => v);
|
|
651
|
-
|
|
652
|
-
const method = (this as any)[property];
|
|
653
|
-
|
|
654
|
-
if (typeof method === 'function') {
|
|
655
|
-
// Set initial value
|
|
656
|
-
const initialValue = transform(stateObj.value);
|
|
657
|
-
this._isUpdatingSync = true;
|
|
658
|
-
method.call(this, initialValue);
|
|
659
|
-
this._isUpdatingSync = false;
|
|
660
|
-
|
|
661
|
-
// Subscribe to changes with guard
|
|
662
|
-
stateObj.subscribe((val: any) => {
|
|
663
|
-
if (this._isUpdatingSync) return;
|
|
664
|
-
|
|
665
|
-
const transformed = transform(val);
|
|
666
|
-
this._isUpdatingSync = true;
|
|
667
|
-
method.call(this, transformed);
|
|
668
|
-
this._isUpdatingSync = false;
|
|
669
|
-
});
|
|
670
|
-
} else {
|
|
671
|
-
console.warn(
|
|
672
|
-
`[Jux] ${this.constructor.name}.sync('${property}'): ` +
|
|
673
|
-
`No method .${property}() found. Property will not be synced.`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
680
|
-
* PROPS ACCESSOR - Read-only access to component state
|
|
681
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* ✅ Read-only accessor for component state (getter, not a method)
|
|
685
|
-
*
|
|
686
|
-
* Usage:
|
|
687
|
-
* const myState = component.props; // ✅ Correct
|
|
688
|
-
* const myState = component.props(); // ❌ Error: props is not a function
|
|
689
|
-
*/
|
|
690
|
-
get props(): Readonly<TState> {
|
|
691
|
-
return this.state as Readonly<TState>;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
695
|
-
* FORM INPUT API (Optional - only used by form components)
|
|
696
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Set label for form inputs
|
|
700
|
-
*/
|
|
701
|
-
label(value: string): this {
|
|
702
|
-
(this.state as any).label = value;
|
|
703
|
-
if (this._labelElement) {
|
|
704
|
-
const requiredSpan = this._labelElement.querySelector('.jux-input-required');
|
|
705
|
-
this._labelElement.textContent = value;
|
|
706
|
-
if (requiredSpan) this._labelElement.appendChild(requiredSpan);
|
|
707
|
-
}
|
|
708
|
-
return this;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Set required state for form inputs
|
|
713
|
-
*/
|
|
714
|
-
required(value: boolean): this {
|
|
715
|
-
(this.state as any).required = value;
|
|
716
|
-
return this;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Set name attribute for form inputs
|
|
721
|
-
*/
|
|
722
|
-
name(value: string): this {
|
|
723
|
-
(this.state as any).name = value;
|
|
724
|
-
return this;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Set custom validation handler
|
|
729
|
-
*/
|
|
730
|
-
onValidate(handler: (value: any) => boolean | string): this {
|
|
731
|
-
this._onValidate = handler;
|
|
732
|
-
return this;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Validate form input (override in form components)
|
|
737
|
-
*/
|
|
738
|
-
validate(): boolean {
|
|
739
|
-
console.warn(`${this.constructor.name}.validate() not implemented`);
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Check if form input is valid (override in form components)
|
|
745
|
-
*/
|
|
746
|
-
isValid(): boolean {
|
|
747
|
-
console.warn(`${this.constructor.name}.isValid() not implemented`);
|
|
748
|
-
return true;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Get current value (override in form components)
|
|
753
|
-
*/
|
|
754
|
-
getValue(): any {
|
|
755
|
-
console.warn(`${this.constructor.name}.getValue() not implemented`);
|
|
756
|
-
return undefined;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Set current value (override in form components)
|
|
761
|
-
*/
|
|
762
|
-
setValue(value: any): this {
|
|
763
|
-
console.warn(`${this.constructor.name}.setValue() not implemented`);
|
|
764
|
-
return this;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
768
|
-
* FORM VALIDATION HELPERS (Protected - for form components)
|
|
769
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
770
|
-
|
|
771
|
-
protected _showError(message: string): void {
|
|
772
|
-
if (this._errorElement) {
|
|
773
|
-
this._errorElement.textContent = message;
|
|
774
|
-
this._errorElement.style.display = 'block';
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (this._inputElement) {
|
|
778
|
-
this._inputElement.classList.add('jux-input-invalid');
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
(this.state as any).errorMessage = message;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
protected _clearError(): void {
|
|
785
|
-
if (this._errorElement) {
|
|
786
|
-
this._errorElement.textContent = '';
|
|
787
|
-
this._errorElement.style.display = 'none';
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (this._inputElement) {
|
|
791
|
-
this._inputElement.classList.remove('jux-input-invalid');
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
(this.state as any).errorMessage = undefined;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Build label element for form inputs
|
|
799
|
-
*/
|
|
800
|
-
protected _renderLabel(): HTMLLabelElement {
|
|
801
|
-
const label = (this.state as any).label || formatIdAsLabel(this._id); // ✅ Auto-generate
|
|
802
|
-
const required = (this.state as any).required || false;
|
|
803
|
-
|
|
804
|
-
const labelEl = document.createElement('label');
|
|
805
|
-
labelEl.className = 'jux-input-label';
|
|
806
|
-
labelEl.htmlFor = `${this._id}-input`;
|
|
807
|
-
labelEl.textContent = label;
|
|
808
|
-
|
|
809
|
-
if (required) {
|
|
810
|
-
const requiredSpan = document.createElement('span');
|
|
811
|
-
requiredSpan.className = 'jux-input-required';
|
|
812
|
-
requiredSpan.textContent = ' *';
|
|
813
|
-
labelEl.appendChild(requiredSpan);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
this._labelElement = labelEl;
|
|
817
|
-
return labelEl;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Build error element for form inputs
|
|
822
|
-
*/
|
|
823
|
-
protected _renderError(): HTMLElement {
|
|
824
|
-
const errorEl = document.createElement('div');
|
|
825
|
-
errorEl.className = 'jux-input-error';
|
|
826
|
-
errorEl.id = `${this._id}-error`;
|
|
827
|
-
errorEl.style.display = 'none';
|
|
828
|
-
|
|
829
|
-
this._errorElement = errorEl;
|
|
830
|
-
return errorEl;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Wire up two-way sync for form inputs
|
|
835
|
-
*/
|
|
836
|
-
protected _wireFormSync(inputElement: HTMLElement, eventName: string = 'input'): void {
|
|
837
|
-
const valueSync = this._syncBindings.find(b => b.property === 'value');
|
|
838
|
-
|
|
839
|
-
if (valueSync) {
|
|
840
|
-
const { stateObj, toState, toComponent } = valueSync;
|
|
841
|
-
|
|
842
|
-
const transformToState = toState || ((v: any) => v);
|
|
843
|
-
const transformToComponent = toComponent || ((v: any) => v);
|
|
844
|
-
|
|
845
|
-
let isUpdating = false;
|
|
846
|
-
|
|
847
|
-
// State → Component
|
|
848
|
-
stateObj.subscribe((val: any) => {
|
|
849
|
-
if (isUpdating) return;
|
|
850
|
-
const transformed = transformToComponent(val);
|
|
851
|
-
this.setValue(transformed);
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
// Component → State
|
|
855
|
-
inputElement.addEventListener(eventName, () => {
|
|
856
|
-
if (isUpdating) return;
|
|
857
|
-
isUpdating = true;
|
|
858
|
-
|
|
859
|
-
const value = this.getValue();
|
|
860
|
-
const transformed = transformToState(value);
|
|
861
|
-
this._clearError();
|
|
862
|
-
|
|
863
|
-
stateObj.set(transformed);
|
|
864
|
-
|
|
865
|
-
setTimeout(() => { isUpdating = false; }, 0);
|
|
866
|
-
});
|
|
867
|
-
} else {
|
|
868
|
-
// Default behavior without sync
|
|
869
|
-
inputElement.addEventListener(eventName, () => {
|
|
870
|
-
this._clearError();
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Only validate on blur IF the field has been validated before
|
|
875
|
-
inputElement.addEventListener('blur', () => {
|
|
876
|
-
if (this._hasBeenValidated) {
|
|
877
|
-
this.validate();
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// ✅ Type assertion for Animations (all components have animation methods)
|
|
884
|
-
export interface BaseComponent extends Animations { }
|