juxscript 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/barchart.ts +1248 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +199 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1969 -423
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +508 -77
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +280 -0
- package/lib/components/sidebar.ts +87 -37
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +178 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/lib/themes/charts.js +126 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
package/lib/components/input.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Input component options
|
|
5
6
|
*/
|
|
6
7
|
export interface InputOptions {
|
|
7
|
-
type?:
|
|
8
|
-
label?: string;
|
|
9
|
-
placeholder?: string;
|
|
8
|
+
type?: string;
|
|
10
9
|
value?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
label?: string;
|
|
11
12
|
required?: boolean;
|
|
12
13
|
disabled?: boolean;
|
|
14
|
+
name?: string;
|
|
15
|
+
rows?: number;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
minLength?: number;
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
pattern?: string;
|
|
13
22
|
onChange?: (value: string) => void;
|
|
23
|
+
onValidate?: (value: string) => boolean | string;
|
|
24
|
+
style?: string;
|
|
25
|
+
class?: string;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
/**
|
|
@@ -18,43 +30,82 @@ export interface InputOptions {
|
|
|
18
30
|
*/
|
|
19
31
|
type InputState = {
|
|
20
32
|
type: string;
|
|
21
|
-
label: string;
|
|
22
|
-
placeholder: string;
|
|
23
33
|
value: string;
|
|
34
|
+
placeholder: string;
|
|
35
|
+
label: string;
|
|
24
36
|
required: boolean;
|
|
25
37
|
disabled: boolean;
|
|
26
|
-
|
|
38
|
+
name: string;
|
|
39
|
+
rows: number;
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
step?: number;
|
|
43
|
+
minLength?: number;
|
|
44
|
+
maxLength?: number;
|
|
45
|
+
pattern?: string;
|
|
46
|
+
style: string;
|
|
47
|
+
class: string;
|
|
48
|
+
errorMessage?: string;
|
|
27
49
|
};
|
|
28
50
|
|
|
29
51
|
/**
|
|
30
|
-
* Input component
|
|
52
|
+
* Input component - text input and textarea with validation
|
|
31
53
|
*
|
|
32
54
|
* Usage:
|
|
33
|
-
*
|
|
34
|
-
* label
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
55
|
+
* jux.input('username')
|
|
56
|
+
* .label('Username')
|
|
57
|
+
* .placeholder('Enter username')
|
|
58
|
+
* .required(true)
|
|
59
|
+
* .minLength(3)
|
|
60
|
+
* .maxLength(20)
|
|
61
|
+
* .render('#form');
|
|
62
|
+
*
|
|
63
|
+
* jux.input('age')
|
|
64
|
+
* .type('number')
|
|
65
|
+
* .min(0)
|
|
66
|
+
* .max(120)
|
|
67
|
+
* .step(1)
|
|
68
|
+
* .render('#form');
|
|
69
|
+
*
|
|
70
|
+
* jux.input('bio')
|
|
71
|
+
* .type('textarea')
|
|
72
|
+
* .rows(5)
|
|
73
|
+
* .maxLength(500)
|
|
74
|
+
* .render('#form');
|
|
40
75
|
*/
|
|
41
|
-
export class Input
|
|
42
|
-
state
|
|
76
|
+
export class Input {
|
|
77
|
+
state: InputState;
|
|
43
78
|
container: HTMLElement | null = null;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
_id: string;
|
|
80
|
+
id: string;
|
|
81
|
+
private _onChange?: (value: string) => void;
|
|
82
|
+
private _onValidate?: (value: string) => boolean | string;
|
|
83
|
+
private _boundState?: State<string>;
|
|
84
|
+
|
|
85
|
+
constructor(id: string, options: InputOptions = {}) {
|
|
86
|
+
this._id = id;
|
|
87
|
+
this.id = id;
|
|
88
|
+
this._onChange = options.onChange;
|
|
89
|
+
this._onValidate = options.onValidate;
|
|
90
|
+
|
|
91
|
+
this.state = {
|
|
50
92
|
type: options.type ?? 'text',
|
|
51
|
-
label: options.label ?? '',
|
|
52
|
-
placeholder: options.placeholder ?? '',
|
|
53
93
|
value: options.value ?? '',
|
|
94
|
+
placeholder: options.placeholder ?? '',
|
|
95
|
+
label: options.label ?? '',
|
|
54
96
|
required: options.required ?? false,
|
|
55
97
|
disabled: options.disabled ?? false,
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
name: options.name ?? id,
|
|
99
|
+
rows: options.rows ?? 3,
|
|
100
|
+
min: options.min,
|
|
101
|
+
max: options.max,
|
|
102
|
+
step: options.step,
|
|
103
|
+
minLength: options.minLength,
|
|
104
|
+
maxLength: options.maxLength,
|
|
105
|
+
pattern: options.pattern,
|
|
106
|
+
style: options.style ?? '',
|
|
107
|
+
class: options.class ?? ''
|
|
108
|
+
};
|
|
58
109
|
}
|
|
59
110
|
|
|
60
111
|
/* -------------------------
|
|
@@ -66,8 +117,9 @@ export class Input extends Reactive {
|
|
|
66
117
|
return this;
|
|
67
118
|
}
|
|
68
119
|
|
|
69
|
-
|
|
70
|
-
this.state.
|
|
120
|
+
value(value: string | number): this {
|
|
121
|
+
this.state.value = String(value);
|
|
122
|
+
this._updateElement();
|
|
71
123
|
return this;
|
|
72
124
|
}
|
|
73
125
|
|
|
@@ -76,8 +128,8 @@ export class Input extends Reactive {
|
|
|
76
128
|
return this;
|
|
77
129
|
}
|
|
78
130
|
|
|
79
|
-
|
|
80
|
-
this.state.
|
|
131
|
+
label(value: string): this {
|
|
132
|
+
this.state.label = value;
|
|
81
133
|
return this;
|
|
82
134
|
}
|
|
83
135
|
|
|
@@ -88,21 +140,268 @@ export class Input extends Reactive {
|
|
|
88
140
|
|
|
89
141
|
disabled(value: boolean): this {
|
|
90
142
|
this.state.disabled = value;
|
|
143
|
+
this._updateElement();
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
name(value: string): this {
|
|
148
|
+
this.state.name = value;
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
rows(value: number): this {
|
|
153
|
+
this.state.rows = value;
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Minimum value for number inputs
|
|
159
|
+
*/
|
|
160
|
+
min(value: number): this {
|
|
161
|
+
this.state.min = value;
|
|
162
|
+
this._updateElement();
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Maximum value for number inputs
|
|
168
|
+
*/
|
|
169
|
+
max(value: number): this {
|
|
170
|
+
this.state.max = value;
|
|
171
|
+
this._updateElement();
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Step value for number inputs
|
|
177
|
+
*/
|
|
178
|
+
step(value: number): this {
|
|
179
|
+
this.state.step = value;
|
|
180
|
+
this._updateElement();
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Minimum length for text inputs
|
|
186
|
+
*/
|
|
187
|
+
minLength(value: number): this {
|
|
188
|
+
this.state.minLength = value;
|
|
189
|
+
this._updateElement();
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Maximum length for text inputs
|
|
195
|
+
*/
|
|
196
|
+
maxLength(value: number): this {
|
|
197
|
+
this.state.maxLength = value;
|
|
198
|
+
this._updateElement();
|
|
91
199
|
return this;
|
|
92
200
|
}
|
|
93
201
|
|
|
94
|
-
|
|
95
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Pattern validation for text inputs (regex)
|
|
204
|
+
*/
|
|
205
|
+
pattern(value: string): this {
|
|
206
|
+
this.state.pattern = value;
|
|
207
|
+
this._updateElement();
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
style(value: string): this {
|
|
212
|
+
this.state.style = value;
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class(value: string): this {
|
|
217
|
+
this.state.class = value;
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
onChange(handler: (value: string) => void): this {
|
|
222
|
+
this._onChange = handler;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Custom validation function
|
|
228
|
+
* Should return true if valid, or an error message string if invalid
|
|
229
|
+
*/
|
|
230
|
+
onValidate(handler: (value: string) => boolean | string): this {
|
|
231
|
+
this._onValidate = handler;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Two-way binding to state
|
|
237
|
+
*/
|
|
238
|
+
bind(stateObj: State<string>): this {
|
|
239
|
+
this._boundState = stateObj;
|
|
240
|
+
|
|
241
|
+
// Subscribe to state changes
|
|
242
|
+
stateObj.subscribe((val) => {
|
|
243
|
+
this.state.value = val;
|
|
244
|
+
this._updateElement();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Update state on input change
|
|
248
|
+
this.onChange((value) => stateObj.set(value));
|
|
249
|
+
|
|
96
250
|
return this;
|
|
97
251
|
}
|
|
98
252
|
|
|
253
|
+
/* -------------------------
|
|
254
|
+
* Validation
|
|
255
|
+
* ------------------------- */
|
|
256
|
+
|
|
257
|
+
private _validate(value: string): boolean | string {
|
|
258
|
+
const { required, type, min, max, minLength, maxLength, pattern } = this.state;
|
|
259
|
+
|
|
260
|
+
// Required check
|
|
261
|
+
if (required && !value.trim()) {
|
|
262
|
+
return 'This field is required';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Number validation
|
|
266
|
+
if (type === 'number' && value) {
|
|
267
|
+
const numValue = Number(value);
|
|
268
|
+
|
|
269
|
+
if (isNaN(numValue)) {
|
|
270
|
+
return 'Must be a valid number';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (min !== undefined && numValue < min) {
|
|
274
|
+
return `Must be at least ${min}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (max !== undefined && numValue > max) {
|
|
278
|
+
return `Must be at most ${max}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Text length validation
|
|
283
|
+
if ((type === 'text' || type === 'textarea') && value) {
|
|
284
|
+
if (minLength !== undefined && value.length < minLength) {
|
|
285
|
+
return `Must be at least ${minLength} characters`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (maxLength !== undefined && value.length > maxLength) {
|
|
289
|
+
return `Must be at most ${maxLength} characters`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Pattern validation
|
|
294
|
+
if (pattern && value) {
|
|
295
|
+
const regex = new RegExp(pattern);
|
|
296
|
+
if (!regex.test(value)) {
|
|
297
|
+
return 'Invalid format';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Custom validation
|
|
302
|
+
if (this._onValidate) {
|
|
303
|
+
const result = this._onValidate(value);
|
|
304
|
+
if (result !== true) {
|
|
305
|
+
return result || 'Invalid value';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private _showError(message: string): void {
|
|
313
|
+
const errorEl = document.getElementById(`${this._id}-error`);
|
|
314
|
+
const inputEl = document.getElementById(`${this._id}-input`);
|
|
315
|
+
|
|
316
|
+
if (errorEl) {
|
|
317
|
+
errorEl.textContent = message;
|
|
318
|
+
errorEl.style.display = 'block';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (inputEl) {
|
|
322
|
+
inputEl.classList.add('jux-input-invalid');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.state.errorMessage = message;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _clearError(): void {
|
|
329
|
+
const errorEl = document.getElementById(`${this._id}-error`);
|
|
330
|
+
const inputEl = document.getElementById(`${this._id}-input`);
|
|
331
|
+
|
|
332
|
+
if (errorEl) {
|
|
333
|
+
errorEl.textContent = '';
|
|
334
|
+
errorEl.style.display = 'none';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (inputEl) {
|
|
338
|
+
inputEl.classList.remove('jux-input-invalid');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.state.errorMessage = undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Manually validate the current value
|
|
346
|
+
*/
|
|
347
|
+
validate(): boolean {
|
|
348
|
+
const result = this._validate(this.state.value);
|
|
349
|
+
|
|
350
|
+
if (result === true) {
|
|
351
|
+
this._clearError();
|
|
352
|
+
return true;
|
|
353
|
+
} else {
|
|
354
|
+
this._showError(result as string);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* -------------------------
|
|
360
|
+
* Helpers
|
|
361
|
+
* ------------------------- */
|
|
362
|
+
|
|
363
|
+
private _updateElement(): void {
|
|
364
|
+
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement | HTMLTextAreaElement;
|
|
365
|
+
if (input) {
|
|
366
|
+
input.value = this.state.value;
|
|
367
|
+
input.disabled = this.state.disabled;
|
|
368
|
+
|
|
369
|
+
if (input instanceof HTMLInputElement) {
|
|
370
|
+
if (this.state.min !== undefined) input.min = String(this.state.min);
|
|
371
|
+
if (this.state.max !== undefined) input.max = String(this.state.max);
|
|
372
|
+
if (this.state.step !== undefined) input.step = String(this.state.step);
|
|
373
|
+
if (this.state.minLength !== undefined) input.minLength = this.state.minLength;
|
|
374
|
+
if (this.state.maxLength !== undefined) input.maxLength = this.state.maxLength;
|
|
375
|
+
if (this.state.pattern) input.pattern = this.state.pattern;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
379
|
+
if (this.state.minLength !== undefined) input.minLength = this.state.minLength;
|
|
380
|
+
if (this.state.maxLength !== undefined) input.maxLength = this.state.maxLength;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getValue(): string {
|
|
386
|
+
return this.state.value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
getNumericValue(): number | null {
|
|
390
|
+
const num = Number(this.state.value);
|
|
391
|
+
return isNaN(num) ? null : num;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
isValid(): boolean {
|
|
395
|
+
return this._validate(this.state.value) === true;
|
|
396
|
+
}
|
|
397
|
+
|
|
99
398
|
/* -------------------------
|
|
100
399
|
* Render
|
|
101
400
|
* ------------------------- */
|
|
102
401
|
|
|
103
402
|
render(targetId?: string): this {
|
|
104
403
|
let container: HTMLElement;
|
|
105
|
-
|
|
404
|
+
|
|
106
405
|
if (targetId) {
|
|
107
406
|
const target = document.querySelector(targetId);
|
|
108
407
|
if (!target || !(target instanceof HTMLElement)) {
|
|
@@ -110,66 +409,198 @@ export class Input extends Reactive {
|
|
|
110
409
|
}
|
|
111
410
|
container = target;
|
|
112
411
|
} else {
|
|
113
|
-
container = getOrCreateContainer(this.
|
|
412
|
+
container = getOrCreateContainer(this._id);
|
|
114
413
|
}
|
|
115
|
-
|
|
414
|
+
|
|
116
415
|
this.container = container;
|
|
117
|
-
const { type,
|
|
118
|
-
|
|
416
|
+
const { type, value, placeholder, label, required, disabled, name, rows, min, max, step, minLength, maxLength, pattern, style, class: className } = this.state;
|
|
417
|
+
|
|
119
418
|
const wrapper = document.createElement('div');
|
|
120
|
-
wrapper.className = 'jux-input
|
|
121
|
-
wrapper.id = this.
|
|
122
|
-
|
|
419
|
+
wrapper.className = 'jux-input';
|
|
420
|
+
wrapper.id = this._id;
|
|
421
|
+
|
|
422
|
+
if (className) {
|
|
423
|
+
wrapper.className += ` ${className}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (style) {
|
|
427
|
+
wrapper.setAttribute('style', style);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Label
|
|
123
431
|
if (label) {
|
|
124
432
|
const labelEl = document.createElement('label');
|
|
125
433
|
labelEl.className = 'jux-input-label';
|
|
434
|
+
labelEl.htmlFor = `${this._id}-input`;
|
|
126
435
|
labelEl.textContent = label;
|
|
127
|
-
|
|
436
|
+
if (required) {
|
|
437
|
+
const requiredSpan = document.createElement('span');
|
|
438
|
+
requiredSpan.className = 'jux-input-required';
|
|
439
|
+
requiredSpan.textContent = ' *';
|
|
440
|
+
labelEl.appendChild(requiredSpan);
|
|
441
|
+
}
|
|
128
442
|
wrapper.appendChild(labelEl);
|
|
129
443
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
444
|
+
|
|
445
|
+
// Input/Textarea
|
|
446
|
+
let inputEl: HTMLInputElement | HTMLTextAreaElement;
|
|
447
|
+
|
|
448
|
+
if (type === 'textarea') {
|
|
449
|
+
inputEl = document.createElement('textarea');
|
|
450
|
+
inputEl.rows = rows;
|
|
451
|
+
if (minLength !== undefined) inputEl.minLength = minLength;
|
|
452
|
+
if (maxLength !== undefined) inputEl.maxLength = maxLength;
|
|
453
|
+
} else {
|
|
454
|
+
inputEl = document.createElement('input');
|
|
455
|
+
inputEl.type = type;
|
|
456
|
+
|
|
457
|
+
// Number-specific attributes
|
|
458
|
+
if (type === 'number') {
|
|
459
|
+
if (min !== undefined) inputEl.min = String(min);
|
|
460
|
+
if (max !== undefined) inputEl.max = String(max);
|
|
461
|
+
if (step !== undefined) inputEl.step = String(step);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Text-specific attributes
|
|
465
|
+
if (type === 'text' || type === 'email' || type === 'tel' || type === 'url') {
|
|
466
|
+
if (minLength !== undefined) inputEl.minLength = minLength;
|
|
467
|
+
if (maxLength !== undefined) inputEl.maxLength = maxLength;
|
|
468
|
+
if (pattern) inputEl.pattern = pattern;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
inputEl.className = 'jux-input-element';
|
|
473
|
+
inputEl.id = `${this._id}-input`;
|
|
474
|
+
inputEl.name = name;
|
|
475
|
+
inputEl.value = value;
|
|
476
|
+
inputEl.placeholder = placeholder;
|
|
477
|
+
inputEl.required = required;
|
|
478
|
+
inputEl.disabled = disabled;
|
|
479
|
+
|
|
480
|
+
// Input event handler
|
|
481
|
+
inputEl.addEventListener('input', (e) => {
|
|
482
|
+
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
483
|
+
this.state.value = target.value;
|
|
484
|
+
|
|
485
|
+
// Clear error on input
|
|
486
|
+
this._clearError();
|
|
487
|
+
|
|
488
|
+
if (this._onChange) {
|
|
489
|
+
this._onChange(target.value);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Blur event for validation
|
|
494
|
+
inputEl.addEventListener('blur', () => {
|
|
495
|
+
this.validate();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
wrapper.appendChild(inputEl);
|
|
499
|
+
|
|
500
|
+
// Error message element
|
|
501
|
+
const errorEl = document.createElement('div');
|
|
502
|
+
errorEl.className = 'jux-input-error';
|
|
503
|
+
errorEl.id = `${this._id}-error`;
|
|
504
|
+
errorEl.style.display = 'none';
|
|
505
|
+
wrapper.appendChild(errorEl);
|
|
506
|
+
|
|
507
|
+
// Character counter for maxLength
|
|
508
|
+
if (maxLength && (type === 'text' || type === 'textarea')) {
|
|
509
|
+
const counterEl = document.createElement('div');
|
|
510
|
+
counterEl.className = 'jux-input-counter';
|
|
511
|
+
counterEl.id = `${this._id}-counter`;
|
|
512
|
+
counterEl.textContent = `${value.length}/${maxLength}`;
|
|
513
|
+
|
|
514
|
+
inputEl.addEventListener('input', () => {
|
|
515
|
+
counterEl.textContent = `${inputEl.value.length}/${maxLength}`;
|
|
148
516
|
});
|
|
517
|
+
|
|
518
|
+
wrapper.appendChild(counterEl);
|
|
149
519
|
}
|
|
150
|
-
|
|
520
|
+
|
|
521
|
+
container.appendChild(wrapper);
|
|
522
|
+
|
|
523
|
+
// Add default styles if not already present
|
|
524
|
+
this._injectDefaultStyles();
|
|
525
|
+
|
|
151
526
|
return this;
|
|
152
527
|
}
|
|
153
528
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
529
|
+
private _injectDefaultStyles(): void {
|
|
530
|
+
const styleId = 'jux-input-styles';
|
|
531
|
+
if (document.getElementById(styleId)) return;
|
|
532
|
+
|
|
533
|
+
const style = document.createElement('style');
|
|
534
|
+
style.id = styleId;
|
|
535
|
+
style.textContent = `
|
|
536
|
+
.jux-input {
|
|
537
|
+
margin-bottom: 16px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.jux-input-label {
|
|
541
|
+
display: block;
|
|
542
|
+
margin-bottom: 6px;
|
|
543
|
+
font-weight: 500;
|
|
544
|
+
color: #374151;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.jux-input-required {
|
|
548
|
+
color: #ef4444;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.jux-input-element {
|
|
552
|
+
width: 100%;
|
|
553
|
+
padding: 8px 12px;
|
|
554
|
+
border: 1px solid #d1d5db;
|
|
555
|
+
border-radius: 6px;
|
|
556
|
+
font-size: 14px;
|
|
557
|
+
transition: border-color 0.2s;
|
|
558
|
+
box-sizing: border-box;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.jux-input-element:focus {
|
|
562
|
+
outline: none;
|
|
563
|
+
border-color: #3b82f6;
|
|
564
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.jux-input-element:disabled {
|
|
568
|
+
background-color: #f3f4f6;
|
|
569
|
+
cursor: not-allowed;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.jux-input-element.jux-input-invalid {
|
|
573
|
+
border-color: #ef4444;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.jux-input-element.jux-input-invalid:focus {
|
|
577
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.jux-input-error {
|
|
581
|
+
color: #ef4444;
|
|
582
|
+
font-size: 12px;
|
|
583
|
+
margin-top: 4px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.jux-input-counter {
|
|
587
|
+
text-align: right;
|
|
588
|
+
font-size: 12px;
|
|
589
|
+
color: #6b7280;
|
|
590
|
+
margin-top: 4px;
|
|
591
|
+
}
|
|
592
|
+
`;
|
|
593
|
+
document.head.appendChild(style);
|
|
594
|
+
}
|
|
595
|
+
|
|
157
596
|
renderTo(juxComponent: any): this {
|
|
158
|
-
if (!juxComponent
|
|
159
|
-
throw new Error('Input.renderTo: Invalid component
|
|
597
|
+
if (!juxComponent?._id) {
|
|
598
|
+
throw new Error('Input.renderTo: Invalid component');
|
|
160
599
|
}
|
|
161
|
-
|
|
162
|
-
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
163
|
-
throw new Error('Input.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return this.render(`#${juxComponent._componentId}`);
|
|
600
|
+
return this.render(`#${juxComponent._id}`);
|
|
167
601
|
}
|
|
168
602
|
}
|
|
169
603
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
*/
|
|
173
|
-
export function input(componentId: string, options: InputOptions = {}): Input {
|
|
174
|
-
return new Input(componentId, options);
|
|
604
|
+
export function input(id: string, options: InputOptions = {}): Input {
|
|
605
|
+
return new Input(id, options);
|
|
175
606
|
}
|