juxscript 1.0.4 → 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 +1 -1
- package/lib/components/barchart.ts +1248 -0
- package/lib/components/checkbox.ts +8 -37
- package/lib/components/docs-data.json +242 -5
- package/lib/components/input.ts +347 -4
- package/lib/components/select.ts +73 -40
- package/lib/components/sidebar.ts +1 -1
- package/lib/jux.ts +8 -2
- package/lib/presets/notion.css +1 -1
- package/lib/themes/charts.js +126 -0
- package/package.json +1 -1
package/lib/components/input.ts
CHANGED
|
@@ -13,7 +13,14 @@ export interface InputOptions {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
name?: string;
|
|
15
15
|
rows?: number;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
minLength?: number;
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
pattern?: string;
|
|
16
22
|
onChange?: (value: string) => void;
|
|
23
|
+
onValidate?: (value: string) => boolean | string;
|
|
17
24
|
style?: string;
|
|
18
25
|
class?: string;
|
|
19
26
|
}
|
|
@@ -30,23 +37,40 @@ type InputState = {
|
|
|
30
37
|
disabled: boolean;
|
|
31
38
|
name: string;
|
|
32
39
|
rows: number;
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
step?: number;
|
|
43
|
+
minLength?: number;
|
|
44
|
+
maxLength?: number;
|
|
45
|
+
pattern?: string;
|
|
33
46
|
style: string;
|
|
34
47
|
class: string;
|
|
48
|
+
errorMessage?: string;
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
/**
|
|
38
|
-
* Input component - text input and textarea
|
|
52
|
+
* Input component - text input and textarea with validation
|
|
39
53
|
*
|
|
40
54
|
* Usage:
|
|
41
55
|
* jux.input('username')
|
|
42
56
|
* .label('Username')
|
|
43
57
|
* .placeholder('Enter username')
|
|
44
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)
|
|
45
68
|
* .render('#form');
|
|
46
69
|
*
|
|
47
70
|
* jux.input('bio')
|
|
48
71
|
* .type('textarea')
|
|
49
72
|
* .rows(5)
|
|
73
|
+
* .maxLength(500)
|
|
50
74
|
* .render('#form');
|
|
51
75
|
*/
|
|
52
76
|
export class Input {
|
|
@@ -55,12 +79,14 @@ export class Input {
|
|
|
55
79
|
_id: string;
|
|
56
80
|
id: string;
|
|
57
81
|
private _onChange?: (value: string) => void;
|
|
82
|
+
private _onValidate?: (value: string) => boolean | string;
|
|
58
83
|
private _boundState?: State<string>;
|
|
59
84
|
|
|
60
85
|
constructor(id: string, options: InputOptions = {}) {
|
|
61
86
|
this._id = id;
|
|
62
87
|
this.id = id;
|
|
63
88
|
this._onChange = options.onChange;
|
|
89
|
+
this._onValidate = options.onValidate;
|
|
64
90
|
|
|
65
91
|
this.state = {
|
|
66
92
|
type: options.type ?? 'text',
|
|
@@ -71,6 +97,12 @@ export class Input {
|
|
|
71
97
|
disabled: options.disabled ?? false,
|
|
72
98
|
name: options.name ?? id,
|
|
73
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,
|
|
74
106
|
style: options.style ?? '',
|
|
75
107
|
class: options.class ?? ''
|
|
76
108
|
};
|
|
@@ -85,8 +117,8 @@ export class Input {
|
|
|
85
117
|
return this;
|
|
86
118
|
}
|
|
87
119
|
|
|
88
|
-
value(value: string): this {
|
|
89
|
-
this.state.value = value;
|
|
120
|
+
value(value: string | number): this {
|
|
121
|
+
this.state.value = String(value);
|
|
90
122
|
this._updateElement();
|
|
91
123
|
return this;
|
|
92
124
|
}
|
|
@@ -122,6 +154,60 @@ export class Input {
|
|
|
122
154
|
return this;
|
|
123
155
|
}
|
|
124
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();
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
+
|
|
125
211
|
style(value: string): this {
|
|
126
212
|
this.state.style = value;
|
|
127
213
|
return this;
|
|
@@ -137,6 +223,15 @@ export class Input {
|
|
|
137
223
|
return this;
|
|
138
224
|
}
|
|
139
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
|
+
|
|
140
235
|
/**
|
|
141
236
|
* Two-way binding to state
|
|
142
237
|
*/
|
|
@@ -155,6 +250,112 @@ export class Input {
|
|
|
155
250
|
return this;
|
|
156
251
|
}
|
|
157
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
|
+
|
|
158
359
|
/* -------------------------
|
|
159
360
|
* Helpers
|
|
160
361
|
* ------------------------- */
|
|
@@ -164,6 +365,20 @@ export class Input {
|
|
|
164
365
|
if (input) {
|
|
165
366
|
input.value = this.state.value;
|
|
166
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
|
+
}
|
|
167
382
|
}
|
|
168
383
|
}
|
|
169
384
|
|
|
@@ -171,6 +386,15 @@ export class Input {
|
|
|
171
386
|
return this.state.value;
|
|
172
387
|
}
|
|
173
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
|
+
|
|
174
398
|
/* -------------------------
|
|
175
399
|
* Render
|
|
176
400
|
* ------------------------- */
|
|
@@ -189,7 +413,7 @@ export class Input {
|
|
|
189
413
|
}
|
|
190
414
|
|
|
191
415
|
this.container = container;
|
|
192
|
-
const { type, value, placeholder, label, required, disabled, name, rows, style, class: className } = this.state;
|
|
416
|
+
const { type, value, placeholder, label, required, disabled, name, rows, min, max, step, minLength, maxLength, pattern, style, class: className } = this.state;
|
|
193
417
|
|
|
194
418
|
const wrapper = document.createElement('div');
|
|
195
419
|
wrapper.className = 'jux-input';
|
|
@@ -224,9 +448,25 @@ export class Input {
|
|
|
224
448
|
if (type === 'textarea') {
|
|
225
449
|
inputEl = document.createElement('textarea');
|
|
226
450
|
inputEl.rows = rows;
|
|
451
|
+
if (minLength !== undefined) inputEl.minLength = minLength;
|
|
452
|
+
if (maxLength !== undefined) inputEl.maxLength = maxLength;
|
|
227
453
|
} else {
|
|
228
454
|
inputEl = document.createElement('input');
|
|
229
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
|
+
}
|
|
230
470
|
}
|
|
231
471
|
|
|
232
472
|
inputEl.className = 'jux-input-element';
|
|
@@ -237,19 +477,122 @@ export class Input {
|
|
|
237
477
|
inputEl.required = required;
|
|
238
478
|
inputEl.disabled = disabled;
|
|
239
479
|
|
|
480
|
+
// Input event handler
|
|
240
481
|
inputEl.addEventListener('input', (e) => {
|
|
241
482
|
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
242
483
|
this.state.value = target.value;
|
|
484
|
+
|
|
485
|
+
// Clear error on input
|
|
486
|
+
this._clearError();
|
|
487
|
+
|
|
243
488
|
if (this._onChange) {
|
|
244
489
|
this._onChange(target.value);
|
|
245
490
|
}
|
|
246
491
|
});
|
|
247
492
|
|
|
493
|
+
// Blur event for validation
|
|
494
|
+
inputEl.addEventListener('blur', () => {
|
|
495
|
+
this.validate();
|
|
496
|
+
});
|
|
497
|
+
|
|
248
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}`;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
wrapper.appendChild(counterEl);
|
|
519
|
+
}
|
|
520
|
+
|
|
249
521
|
container.appendChild(wrapper);
|
|
522
|
+
|
|
523
|
+
// Add default styles if not already present
|
|
524
|
+
this._injectDefaultStyles();
|
|
525
|
+
|
|
250
526
|
return this;
|
|
251
527
|
}
|
|
252
528
|
|
|
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
|
+
|
|
253
596
|
renderTo(juxComponent: any): this {
|
|
254
597
|
if (!juxComponent?._id) {
|
|
255
598
|
throw new Error('Input.renderTo: Invalid component');
|
package/lib/components/select.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
import { State } from '../reactivity/state.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Select option
|
|
6
|
-
*/
|
|
7
4
|
export interface SelectOption {
|
|
8
5
|
label: string;
|
|
9
6
|
value: string;
|
|
10
7
|
disabled?: boolean;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
/**
|
|
14
|
-
* Select component options
|
|
15
|
-
*/
|
|
16
10
|
export interface SelectOptions {
|
|
17
11
|
options?: SelectOption[];
|
|
18
12
|
value?: string;
|
|
19
13
|
placeholder?: string;
|
|
14
|
+
label?: string;
|
|
20
15
|
disabled?: boolean;
|
|
21
16
|
name?: string;
|
|
22
17
|
onChange?: (value: string) => void;
|
|
@@ -24,36 +19,17 @@ export interface SelectOptions {
|
|
|
24
19
|
class?: string;
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
/**
|
|
28
|
-
* Select component state
|
|
29
|
-
*/
|
|
30
22
|
type SelectState = {
|
|
31
23
|
options: SelectOption[];
|
|
32
24
|
value: string;
|
|
33
25
|
placeholder: string;
|
|
26
|
+
label: string;
|
|
34
27
|
disabled: boolean;
|
|
35
28
|
name: string;
|
|
36
29
|
style: string;
|
|
37
30
|
class: string;
|
|
38
31
|
};
|
|
39
32
|
|
|
40
|
-
/**
|
|
41
|
-
* Select component - Dropdown select
|
|
42
|
-
*
|
|
43
|
-
* Usage:
|
|
44
|
-
* jux.select('country', {
|
|
45
|
-
* placeholder: 'Select country',
|
|
46
|
-
* options: [
|
|
47
|
-
* { label: 'USA', value: 'us' },
|
|
48
|
-
* { label: 'Canada', value: 'ca' }
|
|
49
|
-
* ],
|
|
50
|
-
* onChange: (val) => console.log(val)
|
|
51
|
-
* }).render('#form');
|
|
52
|
-
*
|
|
53
|
-
* // Two-way binding
|
|
54
|
-
* const countryState = state('us');
|
|
55
|
-
* jux.select('country').bind(countryState).render('#form');
|
|
56
|
-
*/
|
|
57
33
|
export class Select {
|
|
58
34
|
state: SelectState;
|
|
59
35
|
container: HTMLElement | null = null;
|
|
@@ -71,6 +47,7 @@ export class Select {
|
|
|
71
47
|
options: options.options ?? [],
|
|
72
48
|
value: options.value ?? '',
|
|
73
49
|
placeholder: options.placeholder ?? 'Select...',
|
|
50
|
+
label: options.label ?? '',
|
|
74
51
|
disabled: options.disabled ?? false,
|
|
75
52
|
name: options.name ?? id,
|
|
76
53
|
style: options.style ?? '',
|
|
@@ -78,10 +55,6 @@ export class Select {
|
|
|
78
55
|
};
|
|
79
56
|
}
|
|
80
57
|
|
|
81
|
-
/* -------------------------
|
|
82
|
-
* Fluent API
|
|
83
|
-
* ------------------------- */
|
|
84
|
-
|
|
85
58
|
options(value: SelectOption[]): this {
|
|
86
59
|
this.state.options = value;
|
|
87
60
|
return this;
|
|
@@ -103,6 +76,11 @@ export class Select {
|
|
|
103
76
|
return this;
|
|
104
77
|
}
|
|
105
78
|
|
|
79
|
+
label(value: string): this {
|
|
80
|
+
this.state.label = value;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
106
84
|
disabled(value: boolean): this {
|
|
107
85
|
this.state.disabled = value;
|
|
108
86
|
this._updateElement();
|
|
@@ -142,15 +120,17 @@ export class Select {
|
|
|
142
120
|
});
|
|
143
121
|
|
|
144
122
|
// Update state when select changes
|
|
145
|
-
|
|
123
|
+
const originalOnChange = this._onChange;
|
|
124
|
+
this._onChange = (value) => {
|
|
125
|
+
stateObj.set(value);
|
|
126
|
+
if (originalOnChange) {
|
|
127
|
+
originalOnChange(value);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
146
130
|
|
|
147
131
|
return this;
|
|
148
132
|
}
|
|
149
133
|
|
|
150
|
-
/* -------------------------
|
|
151
|
-
* Helpers
|
|
152
|
-
* ------------------------- */
|
|
153
|
-
|
|
154
134
|
private _updateElement(): void {
|
|
155
135
|
const select = document.getElementById(`${this._id}-select`) as HTMLSelectElement;
|
|
156
136
|
if (select) {
|
|
@@ -163,10 +143,6 @@ export class Select {
|
|
|
163
143
|
return this.state.value;
|
|
164
144
|
}
|
|
165
145
|
|
|
166
|
-
/* -------------------------
|
|
167
|
-
* Render
|
|
168
|
-
* ------------------------- */
|
|
169
|
-
|
|
170
146
|
render(targetId?: string): this {
|
|
171
147
|
let container: HTMLElement;
|
|
172
148
|
|
|
@@ -181,7 +157,7 @@ export class Select {
|
|
|
181
157
|
}
|
|
182
158
|
|
|
183
159
|
this.container = container;
|
|
184
|
-
const { options, value, placeholder, disabled, name, style, class: className } = this.state;
|
|
160
|
+
const { options, value, placeholder, label, disabled, name, style, class: className } = this.state;
|
|
185
161
|
|
|
186
162
|
const wrapper = document.createElement('div');
|
|
187
163
|
wrapper.className = 'jux-select';
|
|
@@ -195,6 +171,15 @@ export class Select {
|
|
|
195
171
|
wrapper.setAttribute('style', style);
|
|
196
172
|
}
|
|
197
173
|
|
|
174
|
+
// Label
|
|
175
|
+
if (label) {
|
|
176
|
+
const labelEl = document.createElement('label');
|
|
177
|
+
labelEl.className = 'jux-select-label';
|
|
178
|
+
labelEl.htmlFor = `${this._id}-select`;
|
|
179
|
+
labelEl.textContent = label;
|
|
180
|
+
wrapper.appendChild(labelEl);
|
|
181
|
+
}
|
|
182
|
+
|
|
198
183
|
const select = document.createElement('select');
|
|
199
184
|
select.className = 'jux-select-element';
|
|
200
185
|
select.id = `${this._id}-select`;
|
|
@@ -231,9 +216,57 @@ export class Select {
|
|
|
231
216
|
|
|
232
217
|
wrapper.appendChild(select);
|
|
233
218
|
container.appendChild(wrapper);
|
|
219
|
+
|
|
220
|
+
// Add default styles if not already present
|
|
221
|
+
this._injectDefaultStyles();
|
|
222
|
+
|
|
234
223
|
return this;
|
|
235
224
|
}
|
|
236
225
|
|
|
226
|
+
private _injectDefaultStyles(): void {
|
|
227
|
+
const styleId = 'jux-select-styles';
|
|
228
|
+
if (document.getElementById(styleId)) return;
|
|
229
|
+
|
|
230
|
+
const style = document.createElement('style');
|
|
231
|
+
style.id = styleId;
|
|
232
|
+
style.textContent = `
|
|
233
|
+
.jux-select {
|
|
234
|
+
margin-bottom: 16px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.jux-select-label {
|
|
238
|
+
display: block;
|
|
239
|
+
margin-bottom: 6px;
|
|
240
|
+
font-weight: 500;
|
|
241
|
+
color: #374151;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.jux-select-element {
|
|
245
|
+
width: 100%;
|
|
246
|
+
padding: 8px 12px;
|
|
247
|
+
border: 1px solid #d1d5db;
|
|
248
|
+
border-radius: 6px;
|
|
249
|
+
font-size: 14px;
|
|
250
|
+
background: white;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
transition: border-color 0.2s;
|
|
253
|
+
box-sizing: border-box;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.jux-select-element:focus {
|
|
257
|
+
outline: none;
|
|
258
|
+
border-color: #3b82f6;
|
|
259
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.jux-select-element:disabled {
|
|
263
|
+
background-color: #f3f4f6;
|
|
264
|
+
cursor: not-allowed;
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
document.head.appendChild(style);
|
|
268
|
+
}
|
|
269
|
+
|
|
237
270
|
renderTo(juxComponent: any): this {
|
|
238
271
|
if (!juxComponent?._id) {
|
|
239
272
|
throw new Error('Select.renderTo: Invalid component');
|
|
@@ -49,7 +49,7 @@ export class Sidebar {
|
|
|
49
49
|
|
|
50
50
|
this.state = {
|
|
51
51
|
title: options.title ?? '',
|
|
52
|
-
width: options.width ?? '
|
|
52
|
+
width: options.width ?? '300px',
|
|
53
53
|
position: options.position ?? 'left',
|
|
54
54
|
collapsible: options.collapsible ?? false,
|
|
55
55
|
collapsed: options.collapsed ?? false,
|