juxscript 1.1.80 → 1.1.81

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.
Files changed (52) hide show
  1. package/dom-structure-map.json +1 -1
  2. package/index.d.ts +2 -2
  3. package/index.d.ts.map +1 -1
  4. package/index.js +2 -2
  5. package/lib/components/badge.d.ts.map +1 -1
  6. package/lib/components/badge.js +2 -1
  7. package/lib/components/badge.ts +2 -1
  8. package/lib/components/base/BaseComponent.d.ts +55 -1
  9. package/lib/components/base/BaseComponent.d.ts.map +1 -1
  10. package/lib/components/base/BaseComponent.js +168 -2
  11. package/lib/components/base/BaseComponent.ts +203 -3
  12. package/lib/components/checkbox.d.ts +5 -4
  13. package/lib/components/checkbox.d.ts.map +1 -1
  14. package/lib/components/checkbox.js +33 -16
  15. package/lib/components/checkbox.ts +39 -22
  16. package/lib/components/datepicker.d.ts +5 -4
  17. package/lib/components/datepicker.d.ts.map +1 -1
  18. package/lib/components/datepicker.js +31 -16
  19. package/lib/components/datepicker.ts +37 -22
  20. package/lib/components/dropdown.d.ts.map +1 -1
  21. package/lib/components/dropdown.js +2 -1
  22. package/lib/components/dropdown.ts +2 -1
  23. package/lib/components/fileupload.d.ts +6 -6
  24. package/lib/components/fileupload.d.ts.map +1 -1
  25. package/lib/components/fileupload.js +77 -52
  26. package/lib/components/fileupload.ts +88 -58
  27. package/lib/components/input.d.ts +5 -4
  28. package/lib/components/input.d.ts.map +1 -1
  29. package/lib/components/input.js +38 -24
  30. package/lib/components/input.ts +48 -33
  31. package/lib/components/radio.d.ts +5 -4
  32. package/lib/components/radio.d.ts.map +1 -1
  33. package/lib/components/radio.js +37 -14
  34. package/lib/components/radio.ts +40 -16
  35. package/lib/components/select.d.ts +5 -4
  36. package/lib/components/select.d.ts.map +1 -1
  37. package/lib/components/select.js +32 -11
  38. package/lib/components/select.ts +38 -16
  39. package/lib/components/switch.d.ts +5 -4
  40. package/lib/components/switch.d.ts.map +1 -1
  41. package/lib/components/switch.js +34 -11
  42. package/lib/components/switch.ts +42 -16
  43. package/lib/components/watcher.d.ts +195 -0
  44. package/lib/components/watcher.d.ts.map +1 -0
  45. package/lib/components/watcher.js +241 -0
  46. package/lib/components/watcher.ts +261 -0
  47. package/package.json +1 -1
  48. package/lib/components/base/FormInput.d.ts +0 -77
  49. package/lib/components/base/FormInput.d.ts.map +0 -1
  50. package/lib/components/base/FormInput.js +0 -171
  51. package/lib/components/base/FormInput.ts +0 -237
  52. package/lib/components/event-chain.ts +0 -31
@@ -0,0 +1,261 @@
1
+ import { BaseComponent } from "./base/BaseComponent.js";
2
+
3
+ /**
4
+ * 🔍 WATCHER - Simple Component Observer
5
+ *
6
+ * Watches JUX components and fires callbacks when their state changes.
7
+ * Think of it as a "subscribe to component updates" utility.
8
+ *
9
+ * ═══════════════════════════════════════════════════════════════════════════════
10
+ * BASIC USAGE
11
+ * ═══════════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * ```javascript
14
+ * import { input, paragraph, watcher } from 'juxscript';
15
+ *
16
+ * const nameInput = input('name').render('app');
17
+ * const greeting = paragraph('greeting').render('app');
18
+ *
19
+ * // Watch nameInput and update greeting whenever it changes
20
+ * watcher('name-watcher')
21
+ * .watch(nameInput, () => {
22
+ * greeting.text(`Hello, ${nameInput.getValue()}!`);
23
+ * });
24
+ * ```
25
+ *
26
+ * ═══════════════════════════════════════════════════════════════════════════════
27
+ * MULTIPLE COMPONENTS
28
+ * ═══════════════════════════════════════════════════════════════════════════════
29
+ *
30
+ * ```javascript
31
+ * const firstNameInput = input('firstName').render();
32
+ * const lastNameInput = input('lastName').render();
33
+ * const fullName = paragraph('fullName').render();
34
+ *
35
+ * const formWatcher = watcher('form');
36
+ *
37
+ * // Watch both inputs, same callback
38
+ * formWatcher
39
+ * .watch(firstNameInput, updateFullName)
40
+ * .watch(lastNameInput, updateFullName);
41
+ *
42
+ * function updateFullName() {
43
+ * const first = firstNameInput.getValue();
44
+ * const last = lastNameInput.getValue();
45
+ * fullName.text(`${first} ${last}`.trim());
46
+ * }
47
+ * ```
48
+ *
49
+ * ═══════════════════════════════════════════════════════════════════════════════
50
+ * DECLARATIVE STYLE - Watch Many, Do One Thing
51
+ * ═══════════════════════════════════════════════════════════════════════════════
52
+ *
53
+ * ```javascript
54
+ * const price = input('price').render();
55
+ * const quantity = input('quantity').render();
56
+ * const discount = input('discount').render();
57
+ * const total = paragraph('total').render();
58
+ *
59
+ * // Update total whenever ANY input changes
60
+ * watcher('calculator')
61
+ * .watchMany([price, quantity, discount], () => {
62
+ * const p = parseFloat(price.getValue()) || 0;
63
+ * const q = parseInt(quantity.getValue()) || 0;
64
+ * const d = parseFloat(discount.getValue()) || 0;
65
+ *
66
+ * const subtotal = p * q;
67
+ * const finalTotal = subtotal - (subtotal * d / 100);
68
+ *
69
+ * total.text(`Total: $${finalTotal.toFixed(2)}`);
70
+ * });
71
+ * ```
72
+ *
73
+ * ═══════════════════════════════════════════════════════════════════════════════
74
+ * STOP WATCHING
75
+ * ═══════════════════════════════════════════════════════════════════════════════
76
+ *
77
+ * ```javascript
78
+ * const myWatcher = watcher('temp');
79
+ * const myInput = input('field').render();
80
+ *
81
+ * const myCallback = () => console.log('Changed!');
82
+ *
83
+ * myWatcher.watch(myInput, myCallback);
84
+ *
85
+ * // Later: stop watching
86
+ * myWatcher.unwatch(myInput, myCallback);
87
+ *
88
+ * // Or stop watching ALL callbacks for this component
89
+ * myWatcher.unwatchAll(myInput);
90
+ *
91
+ * // Or clear everything
92
+ * myWatcher.clear();
93
+ * ```
94
+ *
95
+ * ═══════════════════════════════════════════════════════════════════════════════
96
+ * HOW IT WORKS
97
+ * ═══════════════════════════════════════════════════════════════════════════════
98
+ *
99
+ * 1. Watcher stores callbacks keyed by component._id
100
+ * 2. Component's Proxy detects state changes
101
+ * 3. Component calls watcher.notify(component) [if integrated]
102
+ * 4. Watcher runs all registered callbacks for that component
103
+ *
104
+ * Current limitation: Manual notification required. To auto-notify:
105
+ *
106
+ * ```javascript
107
+ * // In your component after state change:
108
+ * this._notifyWatchers();
109
+ * ```
110
+ *
111
+ * Or integrate into BaseComponent.update():
112
+ *
113
+ * ```typescript
114
+ * update(prop: string, value: any): void {
115
+ * // ...existing update logic...
116
+ * this._watchers.forEach(w => w.notify(this));
117
+ * }
118
+ * ```
119
+ */
120
+
121
+ export class Watcher {
122
+ private _id: string;
123
+ private _callbacks: Map<string, Set<Function>> = new Map();
124
+
125
+ constructor(id: string) {
126
+ this._id = id;
127
+ }
128
+
129
+ /**
130
+ * Watch a component and run callback when it changes
131
+ *
132
+ * @param component - Component to watch
133
+ * @param callback - Function to run on change
134
+ * @returns this (for chaining)
135
+ */
136
+ watch(component: BaseComponent<any>, callback: Function): this {
137
+ const id = component._id;
138
+
139
+ if (!this._callbacks.has(id)) {
140
+ this._callbacks.set(id, new Set());
141
+ }
142
+
143
+ this._callbacks.get(id)!.add(callback);
144
+ return this;
145
+ }
146
+
147
+ /**
148
+ * Watch multiple components with the same callback
149
+ *
150
+ * @param components - Array of components to watch
151
+ * @param callback - Function to run when ANY of them change
152
+ * @returns this (for chaining)
153
+ */
154
+ watchMany(components: BaseComponent<any>[], callback: Function): this {
155
+ components.forEach(component => this.watch(component, callback));
156
+ return this;
157
+ }
158
+
159
+ /**
160
+ * Stop watching a component (remove specific callback)
161
+ *
162
+ * @param component - Component to stop watching
163
+ * @param callback - Specific callback to remove
164
+ * @returns this (for chaining)
165
+ */
166
+ unwatch(component: BaseComponent<any>, callback: Function): this {
167
+ const id = component._id;
168
+ const callbacks = this._callbacks.get(id);
169
+
170
+ if (callbacks) {
171
+ callbacks.delete(callback);
172
+
173
+ // Clean up empty sets
174
+ if (callbacks.size === 0) {
175
+ this._callbacks.delete(id);
176
+ }
177
+ }
178
+
179
+ return this;
180
+ }
181
+
182
+ /**
183
+ * Stop watching a component (remove ALL callbacks)
184
+ *
185
+ * @param component - Component to stop watching completely
186
+ * @returns this (for chaining)
187
+ */
188
+ unwatchAll(component: BaseComponent<any>): this {
189
+ this._callbacks.delete(component._id);
190
+ return this;
191
+ }
192
+
193
+ /**
194
+ * Notify all callbacks for a component (call this when component changes)
195
+ *
196
+ * @param component - Component that changed
197
+ * @param args - Optional arguments to pass to callbacks
198
+ */
199
+ notify(component: BaseComponent<any>, ...args: any[]): void {
200
+ const callbacks = this._callbacks.get(component._id);
201
+
202
+ if (callbacks && callbacks.size > 0) {
203
+ callbacks.forEach(callback => {
204
+ try {
205
+ callback(...args);
206
+ } catch (error) {
207
+ console.error(`[Watcher ${this._id}] Callback error:`, error);
208
+ }
209
+ });
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Clear all watched components and callbacks
215
+ *
216
+ * @returns this (for chaining)
217
+ */
218
+ clear(): this {
219
+ this._callbacks.clear();
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * Get list of component IDs being watched
225
+ *
226
+ * @returns Array of component IDs
227
+ */
228
+ getWatchedIds(): string[] {
229
+ return Array.from(this._callbacks.keys());
230
+ }
231
+
232
+ /**
233
+ * Get count of callbacks for a component
234
+ *
235
+ * @param component - Component to check
236
+ * @returns Number of callbacks registered
237
+ */
238
+ getCallbackCount(component: BaseComponent<any>): number {
239
+ return this._callbacks.get(component._id)?.size || 0;
240
+ }
241
+
242
+ /**
243
+ * Check if watching a component
244
+ *
245
+ * @param component - Component to check
246
+ * @returns true if watching this component
247
+ */
248
+ isWatching(component: BaseComponent<any>): boolean {
249
+ return this._callbacks.has(component._id);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a new watcher instance
255
+ *
256
+ * @param id - Unique identifier for this watcher
257
+ * @returns New Watcher instance
258
+ */
259
+ export function watcher(id: string): Watcher {
260
+ return new Watcher(id);
261
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.80",
3
+ "version": "1.1.81",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",
@@ -1,77 +0,0 @@
1
- import { BaseComponent } from './BaseComponent.js';
2
- /**
3
- * Base state interface for all form inputs
4
- */
5
- export interface FormInputState extends Record<string, any> {
6
- label: string;
7
- required: boolean;
8
- disabled: boolean;
9
- name: string;
10
- style: string;
11
- class: string;
12
- errorMessage?: string;
13
- }
14
- /**
15
- * Abstract base class for all form input components
16
- * Extends BaseComponent with form-specific functionality
17
- */
18
- export declare abstract class FormInput<TState extends FormInputState> extends BaseComponent<TState> {
19
- protected _inputElement: HTMLElement | null;
20
- protected _labelElement: HTMLLabelElement | null;
21
- protected _errorElement: HTMLElement | null;
22
- protected _onValidate?: (value: any) => boolean | string;
23
- protected _hasBeenValidated: boolean;
24
- /**
25
- * Get the current value of the input
26
- */
27
- abstract getValue(): any;
28
- /**
29
- * Set the value of the input
30
- */
31
- abstract setValue(value: any): this;
32
- /**
33
- * Build the actual input element (input, select, textarea, etc.)
34
- */
35
- protected abstract _buildInputElement(): HTMLElement;
36
- /**
37
- * Validate the current value
38
- */
39
- protected abstract _validateValue(value: any): boolean | string;
40
- label(value: string): this;
41
- required(value: boolean): this;
42
- name(value: string): this;
43
- onValidate(handler: (value: any) => boolean | string): this;
44
- /**
45
- * Validate the current value and show/hide errors
46
- */
47
- validate(): boolean;
48
- /**
49
- * Check if current value is valid without showing errors
50
- */
51
- isValid(): boolean;
52
- /**
53
- * Show error message
54
- */
55
- protected _showError(message: string): void;
56
- /**
57
- * Clear error message
58
- */
59
- protected _clearError(): void;
60
- /**
61
- * Build label element with auto-generated text from ID if not provided
62
- */
63
- protected _renderLabel(): HTMLLabelElement;
64
- /**
65
- * Build error element
66
- */
67
- protected _renderError(): HTMLElement;
68
- /**
69
- * Default update implementation for form inputs
70
- */
71
- update(prop: string, value: any): void;
72
- /**
73
- * Wire up two-way sync for value property
74
- */
75
- protected _wireFormSync(inputElement: HTMLElement, eventName?: string): void;
76
- }
77
- //# sourceMappingURL=FormInput.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FormInput.d.ts","sourceRoot":"","sources":["FormInput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,8BAAsB,SAAS,CAAC,MAAM,SAAS,cAAc,CAAE,SAAQ,aAAa,CAAC,MAAM,CAAC;IACxF,SAAS,CAAC,aAAa,EAAE,WAAW,GAAG,IAAI,CAAQ;IACnD,SAAS,CAAC,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACxD,SAAS,CAAC,aAAa,EAAE,WAAW,GAAG,IAAI,CAAQ;IACnD,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,GAAG,MAAM,CAAC;IACzD,SAAS,CAAC,iBAAiB,EAAE,OAAO,CAAS;IAM7C;;OAEG;IACH,QAAQ,CAAC,QAAQ,IAAI,GAAG;IAExB;;OAEG;IACH,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI;IAEnC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,kBAAkB,IAAI,WAAW;IAEpD;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,GAAG,MAAM;IAM/D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ1B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAK9B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKzB,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,GAAG,MAAM,GAAG,IAAI;IAS3D;;OAEG;IACH,QAAQ,IAAI,OAAO;IAcnB;;OAEG;IACH,OAAO,IAAI,OAAO;IAKlB;;OAEG;IACH,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAa3C;;OAEG;IACH,SAAS,CAAC,WAAW,IAAI,IAAI;IAiB7B;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,gBAAgB;IAqB1C;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,WAAW;IAUrC;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAItC;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,YAAY,EAAE,WAAW,EAAE,SAAS,GAAE,MAAgB,GAAG,IAAI;CA8CxF"}
@@ -1,171 +0,0 @@
1
- import { BaseComponent } from './BaseComponent.js';
2
- import { formatIdAsLabel } from '../../utils/formatId.js'; // ✅ Import utility
3
- /**
4
- * Abstract base class for all form input components
5
- * Extends BaseComponent with form-specific functionality
6
- */
7
- export class FormInput extends BaseComponent {
8
- constructor() {
9
- super(...arguments);
10
- this._inputElement = null;
11
- this._labelElement = null;
12
- this._errorElement = null;
13
- this._hasBeenValidated = false; // NEW: Track if user has submitted/validated
14
- }
15
- /* ═════════════════════════════════════════════════════════════════
16
- * COMMON FORM INPUT API
17
- * ═════════════════════════════════════════════════════════════════ */
18
- label(value) {
19
- this.state.label = value;
20
- if (this._labelElement) {
21
- this._labelElement.textContent = value;
22
- }
23
- return this;
24
- }
25
- required(value) {
26
- this.state.required = value;
27
- return this;
28
- }
29
- name(value) {
30
- this.state.name = value;
31
- return this;
32
- }
33
- onValidate(handler) {
34
- this._onValidate = handler;
35
- return this;
36
- }
37
- /* ═════════════════════════════════════════════════════════════════
38
- * VALIDATION
39
- * ═════════════════════════════════════════════════════════════════ */
40
- /**
41
- * Validate the current value and show/hide errors
42
- */
43
- validate() {
44
- this._hasBeenValidated = true; // Mark as validated
45
- const value = this.getValue();
46
- const result = this._validateValue(value);
47
- if (result === true) {
48
- this._clearError();
49
- return true;
50
- }
51
- else {
52
- this._showError(result);
53
- return false;
54
- }
55
- }
56
- /**
57
- * Check if current value is valid without showing errors
58
- */
59
- isValid() {
60
- const value = this.getValue();
61
- return this._validateValue(value) === true;
62
- }
63
- /**
64
- * Show error message
65
- */
66
- _showError(message) {
67
- if (this._errorElement) {
68
- this._errorElement.textContent = message;
69
- this._errorElement.style.display = 'block';
70
- }
71
- if (this._inputElement) {
72
- this._inputElement.classList.add('jux-input-invalid');
73
- }
74
- this.state.errorMessage = message;
75
- }
76
- /**
77
- * Clear error message
78
- */
79
- _clearError() {
80
- if (this._errorElement) {
81
- this._errorElement.textContent = '';
82
- this._errorElement.style.display = 'none';
83
- }
84
- if (this._inputElement) {
85
- this._inputElement.classList.remove('jux-input-invalid');
86
- }
87
- this.state.errorMessage = undefined;
88
- }
89
- /* ═════════════════════════════════════════════════════════════════
90
- * COMMON RENDER HELPERS
91
- * ═════════════════════════════════════════════════════════════════ */
92
- /**
93
- * Build label element with auto-generated text from ID if not provided
94
- */
95
- _renderLabel() {
96
- const { label, required } = this.state;
97
- const labelEl = document.createElement('label');
98
- labelEl.className = 'jux-input-label';
99
- labelEl.htmlFor = `${this._id}-input`;
100
- // ✅ Use provided label or auto-generate from ID
101
- labelEl.textContent = label || formatIdAsLabel(this._id);
102
- if (required) {
103
- const requiredSpan = document.createElement('span');
104
- requiredSpan.className = 'jux-input-required';
105
- requiredSpan.textContent = ' *';
106
- labelEl.appendChild(requiredSpan);
107
- }
108
- this._labelElement = labelEl;
109
- return labelEl;
110
- }
111
- /**
112
- * Build error element
113
- */
114
- _renderError() {
115
- const errorEl = document.createElement('div');
116
- errorEl.className = 'jux-input-error';
117
- errorEl.id = `${this._id}-error`;
118
- errorEl.style.display = 'none';
119
- this._errorElement = errorEl;
120
- return errorEl;
121
- }
122
- /**
123
- * Default update implementation for form inputs
124
- */
125
- update(prop, value) {
126
- // No reactive updates needed - form inputs handle their own state
127
- }
128
- /**
129
- * Wire up two-way sync for value property
130
- */
131
- _wireFormSync(inputElement, eventName = 'input') {
132
- const valueSync = this._syncBindings.find(b => b.property === 'value');
133
- if (valueSync) {
134
- const { stateObj, toState, toComponent } = valueSync;
135
- // Default transforms
136
- const transformToState = toState || ((v) => v);
137
- const transformToComponent = toComponent || ((v) => v);
138
- let isUpdating = false;
139
- // State → Component
140
- stateObj.subscribe((val) => {
141
- if (isUpdating)
142
- return;
143
- const transformed = transformToComponent(val);
144
- this.setValue(transformed);
145
- });
146
- // Component → State
147
- inputElement.addEventListener(eventName, () => {
148
- if (isUpdating)
149
- return;
150
- isUpdating = true;
151
- const value = this.getValue();
152
- const transformed = transformToState(value);
153
- this._clearError();
154
- stateObj.set(transformed);
155
- setTimeout(() => { isUpdating = false; }, 0);
156
- });
157
- }
158
- else {
159
- // Default behavior without sync
160
- inputElement.addEventListener(eventName, () => {
161
- this._clearError();
162
- });
163
- }
164
- // Only validate on blur IF the field has been validated before (e.g., after submit)
165
- inputElement.addEventListener('blur', () => {
166
- if (this._hasBeenValidated) {
167
- this.validate();
168
- }
169
- });
170
- }
171
- }