juxscript 1.0.19 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
|
@@ -1,79 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FormInput, FormInputState } from './base/FormInput.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
3
|
+
|
|
4
|
+
// Event definitions
|
|
5
|
+
const TRIGGER_EVENTS = [] as const;
|
|
6
|
+
const CALLBACK_EVENTS = ['change', 'filesSelected', 'clear'] as const;
|
|
2
7
|
|
|
3
|
-
/**
|
|
4
|
-
* FileUpload component options
|
|
5
|
-
*/
|
|
6
8
|
export interface FileUploadOptions {
|
|
9
|
+
label?: string;
|
|
7
10
|
accept?: string;
|
|
8
11
|
multiple?: boolean;
|
|
9
|
-
maxSize?: number;
|
|
10
12
|
disabled?: boolean;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
onError?: (error: string) => void;
|
|
13
|
+
name?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
required?: boolean;
|
|
15
16
|
style?: string;
|
|
16
17
|
class?: string;
|
|
18
|
+
onValidate?: (files: File[]) => boolean | string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
type FileUploadState = {
|
|
21
|
+
interface FileUploadState extends FormInputState {
|
|
22
|
+
files: File[];
|
|
23
23
|
accept: string;
|
|
24
24
|
multiple: boolean;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
text: string;
|
|
28
|
-
dragText: string;
|
|
29
|
-
style: string;
|
|
30
|
-
class: string;
|
|
31
|
-
files: File[];
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* FileUpload component - Drag & drop file input
|
|
36
|
-
*
|
|
37
|
-
* Usage:
|
|
38
|
-
* jux.fileupload('docs', {
|
|
39
|
-
* accept: '.pdf,.doc,.docx',
|
|
40
|
-
* multiple: true,
|
|
41
|
-
* maxSize: 5242880, // 5MB
|
|
42
|
-
* text: 'Click to upload or drag and drop',
|
|
43
|
-
* onChange: (files) => console.log(files),
|
|
44
|
-
* onError: (err) => console.error(err)
|
|
45
|
-
* }).render('#form');
|
|
46
|
-
*/
|
|
47
|
-
export class FileUpload {
|
|
48
|
-
state: FileUploadState;
|
|
49
|
-
container: HTMLElement | null = null;
|
|
50
|
-
_id: string;
|
|
51
|
-
id: string;
|
|
52
|
-
private _onChange?: (files: FileList) => void;
|
|
53
|
-
private _onError?: (error: string) => void;
|
|
25
|
+
icon: string;
|
|
26
|
+
}
|
|
54
27
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.id = id;
|
|
58
|
-
this._onChange = options.onChange;
|
|
59
|
-
this._onError = options.onError;
|
|
28
|
+
export class FileUpload extends FormInput<FileUploadState> {
|
|
29
|
+
private _fileListElement: HTMLElement | null = null;
|
|
60
30
|
|
|
61
|
-
|
|
62
|
-
|
|
31
|
+
constructor(id: string, options: FileUploadOptions = {}) {
|
|
32
|
+
super(id, {
|
|
33
|
+
files: [],
|
|
34
|
+
accept: options.accept ?? '',
|
|
63
35
|
multiple: options.multiple ?? false,
|
|
64
|
-
|
|
36
|
+
icon: options.icon ?? 'upload',
|
|
37
|
+
label: options.label ?? '',
|
|
38
|
+
required: options.required ?? false,
|
|
65
39
|
disabled: options.disabled ?? false,
|
|
66
|
-
|
|
67
|
-
dragText: options.dragText ?? 'Drop files here',
|
|
40
|
+
name: options.name ?? id,
|
|
68
41
|
style: options.style ?? '',
|
|
69
42
|
class: options.class ?? '',
|
|
70
|
-
|
|
71
|
-
};
|
|
43
|
+
errorMessage: undefined
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (options.onValidate) {
|
|
47
|
+
this._onValidate = options.onValidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected getTriggerEvents(): readonly string[] {
|
|
52
|
+
return TRIGGER_EVENTS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected getCallbackEvents(): readonly string[] {
|
|
56
|
+
return CALLBACK_EVENTS;
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
/*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
59
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
60
|
+
* FLUENT API
|
|
61
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
62
|
+
|
|
63
|
+
// ✅ Inherited from FormInput/BaseComponent:
|
|
64
|
+
// - label(), required(), name(), onValidate()
|
|
65
|
+
// - validate(), isValid()
|
|
66
|
+
// - style(), class()
|
|
67
|
+
// - bind(), sync(), renderTo()
|
|
68
|
+
// - disabled(), enable(), disable()
|
|
77
69
|
|
|
78
70
|
accept(value: string): this {
|
|
79
71
|
this.state.accept = value;
|
|
@@ -85,232 +77,297 @@ export class FileUpload {
|
|
|
85
77
|
return this;
|
|
86
78
|
}
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
this.state.
|
|
80
|
+
icon(value: string): this {
|
|
81
|
+
this.state.icon = value;
|
|
90
82
|
return this;
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
this.state.
|
|
85
|
+
clear(): this {
|
|
86
|
+
this.state.files = [];
|
|
87
|
+
if (this._inputElement) {
|
|
88
|
+
(this._inputElement as HTMLInputElement).value = '';
|
|
89
|
+
}
|
|
90
|
+
if (this._fileListElement) {
|
|
91
|
+
this._updateFileList([]);
|
|
92
|
+
}
|
|
93
|
+
// 🎯 Fire the clear callback event
|
|
94
|
+
this._triggerCallback('clear');
|
|
95
95
|
return this;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
98
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
99
|
+
* FORM INPUT IMPLEMENTATION
|
|
100
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
this.state.
|
|
105
|
-
return this;
|
|
102
|
+
getValue(): File[] {
|
|
103
|
+
return this.state.files;
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
this.state.
|
|
106
|
+
setValue(files: File[]): this {
|
|
107
|
+
this.state.files = files;
|
|
108
|
+
if (this._fileListElement) {
|
|
109
|
+
this._updateFileList(files);
|
|
110
|
+
}
|
|
110
111
|
return this;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
return this;
|
|
114
|
+
getFiles(): File[] {
|
|
115
|
+
return this.getValue();
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
118
|
+
protected _validateValue(files: File[]): boolean | string {
|
|
119
|
+
const { required } = this.state;
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
121
|
+
if (required && files.length === 0) {
|
|
122
|
+
return 'Please select at least one file';
|
|
123
|
+
}
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
private _validateFiles(files: FileList): boolean {
|
|
133
|
-
for (let i = 0; i < files.length; i++) {
|
|
134
|
-
const file = files[i];
|
|
135
|
-
if (file.size > this.state.maxSize) {
|
|
136
|
-
const maxMB = Math.round(this.state.maxSize / 1048576);
|
|
137
|
-
const error = `File "${file.name}" exceeds maximum size of ${maxMB}MB`;
|
|
138
|
-
if (this._onError) {
|
|
139
|
-
this._onError(error);
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
125
|
+
if (this._onValidate) {
|
|
126
|
+
const result = this._onValidate(files);
|
|
127
|
+
if (result !== true) {
|
|
128
|
+
return result || 'Invalid files';
|
|
142
129
|
}
|
|
143
130
|
}
|
|
131
|
+
|
|
144
132
|
return true;
|
|
145
133
|
}
|
|
146
134
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.state.files = Array.from(files);
|
|
150
|
-
this._updateFileList();
|
|
151
|
-
if (this._onChange) {
|
|
152
|
-
this._onChange(files);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
135
|
+
protected _buildInputElement(): HTMLElement {
|
|
136
|
+
const { accept, multiple, required, disabled, name } = this.state;
|
|
156
137
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const name = document.createElement('span');
|
|
166
|
-
name.className = 'jux-fileupload-file-name';
|
|
167
|
-
name.textContent = file.name;
|
|
168
|
-
|
|
169
|
-
const size = document.createElement('span');
|
|
170
|
-
size.className = 'jux-fileupload-file-size';
|
|
171
|
-
const sizeKB = Math.round(file.size / 1024);
|
|
172
|
-
size.textContent = `${sizeKB} KB`;
|
|
173
|
-
|
|
174
|
-
const removeBtn = document.createElement('button');
|
|
175
|
-
removeBtn.className = 'jux-fileupload-file-remove';
|
|
176
|
-
removeBtn.textContent = '×';
|
|
177
|
-
removeBtn.addEventListener('click', () => {
|
|
178
|
-
this.state.files.splice(index, 1);
|
|
179
|
-
this._updateFileList();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
item.appendChild(name);
|
|
183
|
-
item.appendChild(size);
|
|
184
|
-
item.appendChild(removeBtn);
|
|
185
|
-
fileList.appendChild(item);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
138
|
+
const input = document.createElement('input');
|
|
139
|
+
input.type = 'file';
|
|
140
|
+
input.className = 'jux-fileupload-input';
|
|
141
|
+
input.id = `${this._id}-input`;
|
|
142
|
+
input.name = name;
|
|
143
|
+
input.required = required;
|
|
144
|
+
input.disabled = disabled;
|
|
189
145
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
146
|
+
if (accept) input.accept = accept;
|
|
147
|
+
if (multiple) input.multiple = multiple;
|
|
193
148
|
|
|
194
|
-
|
|
195
|
-
this.state.files = [];
|
|
196
|
-
this._updateFileList();
|
|
197
|
-
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
|
|
198
|
-
if (input) {
|
|
199
|
-
input.value = '';
|
|
200
|
-
}
|
|
149
|
+
return input;
|
|
201
150
|
}
|
|
202
151
|
|
|
203
|
-
/*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
152
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
153
|
+
* RENDER
|
|
154
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
206
155
|
|
|
207
156
|
render(targetId?: string): this {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (targetId) {
|
|
211
|
-
const target = document.querySelector(targetId);
|
|
212
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
213
|
-
throw new Error(`FileUpload: Target element "${targetId}" not found`);
|
|
214
|
-
}
|
|
215
|
-
container = target;
|
|
216
|
-
} else {
|
|
217
|
-
container = getOrCreateContainer(this._id);
|
|
218
|
-
}
|
|
157
|
+
const container = this._setupContainer(targetId);
|
|
219
158
|
|
|
220
|
-
|
|
221
|
-
const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
|
|
159
|
+
const { icon, style, class: className } = this.state;
|
|
222
160
|
|
|
161
|
+
// Build wrapper
|
|
223
162
|
const wrapper = document.createElement('div');
|
|
224
|
-
wrapper.className = 'jux-fileupload';
|
|
163
|
+
wrapper.className = 'jux-input jux-fileupload';
|
|
225
164
|
wrapper.id = this._id;
|
|
165
|
+
if (className) wrapper.className += ` ${className}`;
|
|
166
|
+
if (style) wrapper.setAttribute('style', style);
|
|
226
167
|
|
|
227
|
-
|
|
228
|
-
|
|
168
|
+
// Label
|
|
169
|
+
if (this.state.label) {
|
|
170
|
+
wrapper.appendChild(this._renderLabel());
|
|
229
171
|
}
|
|
230
172
|
|
|
231
|
-
|
|
232
|
-
|
|
173
|
+
// Hidden file input
|
|
174
|
+
const inputEl = this._buildInputElement() as HTMLInputElement;
|
|
175
|
+
this._inputElement = inputEl;
|
|
176
|
+
wrapper.appendChild(inputEl);
|
|
177
|
+
|
|
178
|
+
// Button container
|
|
179
|
+
const buttonContainer = document.createElement('div');
|
|
180
|
+
buttonContainer.className = 'jux-fileupload-button-container';
|
|
181
|
+
|
|
182
|
+
if (icon) {
|
|
183
|
+
const iconEl = document.createElement('span');
|
|
184
|
+
iconEl.className = 'jux-fileupload-icon';
|
|
185
|
+
iconEl.appendChild(renderIcon(icon));
|
|
186
|
+
buttonContainer.appendChild(iconEl);
|
|
233
187
|
}
|
|
234
188
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
input.accept = accept;
|
|
241
|
-
input.multiple = multiple;
|
|
242
|
-
input.disabled = disabled;
|
|
189
|
+
const button = document.createElement('button');
|
|
190
|
+
button.type = 'button';
|
|
191
|
+
button.className = 'jux-fileupload-button';
|
|
192
|
+
button.textContent = 'Choose File(s)';
|
|
193
|
+
button.disabled = this.state.disabled;
|
|
243
194
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (target.files && target.files.length > 0) {
|
|
247
|
-
this._handleFiles(target.files);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
195
|
+
buttonContainer.appendChild(button);
|
|
196
|
+
wrapper.appendChild(buttonContainer);
|
|
250
197
|
|
|
251
|
-
//
|
|
252
|
-
const
|
|
253
|
-
|
|
198
|
+
// File list
|
|
199
|
+
const fileList = document.createElement('div');
|
|
200
|
+
fileList.className = 'jux-fileupload-list';
|
|
201
|
+
this._fileListElement = fileList;
|
|
202
|
+
wrapper.appendChild(fileList);
|
|
254
203
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
icon.textContent = '📁';
|
|
204
|
+
// Error element
|
|
205
|
+
wrapper.appendChild(this._renderError());
|
|
258
206
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
textEl.textContent = text;
|
|
207
|
+
// Button click triggers file input
|
|
208
|
+
button.addEventListener('click', () => inputEl.click());
|
|
262
209
|
|
|
263
|
-
|
|
264
|
-
|
|
210
|
+
// Wire events
|
|
211
|
+
this._wireStandardEvents(wrapper);
|
|
265
212
|
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
if (!disabled) {
|
|
269
|
-
input.click();
|
|
270
|
-
}
|
|
271
|
-
});
|
|
213
|
+
// Wire file-specific sync
|
|
214
|
+
const filesSync = this._syncBindings.find(b => b.property === 'files' || b.property === 'value');
|
|
272
215
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
e.preventDefault();
|
|
276
|
-
if (!disabled) {
|
|
277
|
-
dropzone.classList.add('jux-fileupload-dropzone-active');
|
|
278
|
-
textEl.textContent = dragText;
|
|
279
|
-
}
|
|
280
|
-
});
|
|
216
|
+
if (filesSync) {
|
|
217
|
+
const { stateObj, toState, toComponent } = filesSync;
|
|
281
218
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
textEl.textContent = text;
|
|
285
|
-
});
|
|
219
|
+
const transformToState = toState || ((v: File[]) => v);
|
|
220
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
286
221
|
|
|
287
|
-
|
|
288
|
-
e.preventDefault();
|
|
289
|
-
dropzone.classList.remove('jux-fileupload-dropzone-active');
|
|
290
|
-
textEl.textContent = text;
|
|
222
|
+
let isUpdating = false;
|
|
291
223
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
224
|
+
// State → Component
|
|
225
|
+
stateObj.subscribe((val: any) => {
|
|
226
|
+
if (isUpdating) return;
|
|
227
|
+
const transformed = transformToComponent(val);
|
|
228
|
+
this.setValue(transformed);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Component → State
|
|
232
|
+
inputEl.addEventListener('change', () => {
|
|
233
|
+
if (isUpdating) return;
|
|
234
|
+
isUpdating = true;
|
|
235
|
+
|
|
236
|
+
const files = Array.from(inputEl.files || []);
|
|
237
|
+
this.state.files = files;
|
|
238
|
+
this._updateFileList(files);
|
|
239
|
+
this._clearError();
|
|
240
|
+
|
|
241
|
+
const transformed = transformToState(files);
|
|
242
|
+
stateObj.set(transformed);
|
|
243
|
+
|
|
244
|
+
// 🎯 Fire the callback events
|
|
245
|
+
this._triggerCallback('change', files);
|
|
246
|
+
this._triggerCallback('filesSelected', files);
|
|
247
|
+
|
|
248
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
// Default behavior without sync
|
|
252
|
+
inputEl.addEventListener('change', () => {
|
|
253
|
+
const files = Array.from(inputEl.files || []);
|
|
254
|
+
this.state.files = files;
|
|
255
|
+
this._updateFileList(files);
|
|
256
|
+
this._clearError();
|
|
257
|
+
|
|
258
|
+
// 🎯 Fire the callback events
|
|
259
|
+
this._triggerCallback('change', files);
|
|
260
|
+
this._triggerCallback('filesSelected', files);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Always add blur validation
|
|
265
|
+
inputEl.addEventListener('blur', () => {
|
|
266
|
+
this.validate();
|
|
295
267
|
});
|
|
296
268
|
|
|
297
|
-
//
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
269
|
+
// Sync label changes
|
|
270
|
+
const labelSync = this._syncBindings.find(b => b.property === 'label');
|
|
271
|
+
if (labelSync) {
|
|
272
|
+
const transform = labelSync.toComponent || ((v: any) => String(v));
|
|
273
|
+
labelSync.stateObj.subscribe((val: any) => {
|
|
274
|
+
this.label(transform(val));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
301
277
|
|
|
302
|
-
wrapper.appendChild(input);
|
|
303
|
-
wrapper.appendChild(dropzone);
|
|
304
|
-
wrapper.appendChild(fileList);
|
|
305
278
|
container.appendChild(wrapper);
|
|
279
|
+
this._injectFileUploadStyles();
|
|
280
|
+
this._injectFormStyles();
|
|
281
|
+
|
|
282
|
+
requestAnimationFrame(() => {
|
|
283
|
+
if ((window as any).lucide) {
|
|
284
|
+
(window as any).lucide.createIcons();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
306
288
|
return this;
|
|
307
289
|
}
|
|
308
290
|
|
|
309
|
-
|
|
310
|
-
if (!
|
|
311
|
-
|
|
291
|
+
private _updateFileList(files: File[]): void {
|
|
292
|
+
if (!this._fileListElement) return;
|
|
293
|
+
|
|
294
|
+
this._fileListElement.innerHTML = '';
|
|
295
|
+
|
|
296
|
+
if (files.length === 0) {
|
|
297
|
+
this._fileListElement.textContent = 'No files selected';
|
|
298
|
+
return;
|
|
312
299
|
}
|
|
313
|
-
|
|
300
|
+
|
|
301
|
+
files.forEach(file => {
|
|
302
|
+
const fileItem = document.createElement('div');
|
|
303
|
+
fileItem.className = 'jux-fileupload-item';
|
|
304
|
+
fileItem.textContent = `${file.name} (${this._formatFileSize(file.size)})`;
|
|
305
|
+
this._fileListElement!.appendChild(fileItem);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private _formatFileSize(bytes: number): string {
|
|
310
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
311
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
312
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private _injectFileUploadStyles(): void {
|
|
316
|
+
const styleId = 'jux-fileupload-styles';
|
|
317
|
+
if (document.getElementById(styleId)) return;
|
|
318
|
+
|
|
319
|
+
const style = document.createElement('style');
|
|
320
|
+
style.id = styleId;
|
|
321
|
+
style.textContent = `
|
|
322
|
+
.jux-fileupload-input {
|
|
323
|
+
display: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.jux-fileupload-button-container {
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 8px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.jux-fileupload-icon svg {
|
|
333
|
+
width: 18px;
|
|
334
|
+
height: 18px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.jux-fileupload-button {
|
|
338
|
+
padding: 8px 16px;
|
|
339
|
+
background: #3b82f6;
|
|
340
|
+
color: white;
|
|
341
|
+
border: none;
|
|
342
|
+
border-radius: 6px;
|
|
343
|
+
font-size: 14px;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
transition: background 0.2s;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.jux-fileupload-button:hover:not(:disabled) {
|
|
349
|
+
background: #2563eb;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.jux-fileupload-button:disabled {
|
|
353
|
+
background: #9ca3af;
|
|
354
|
+
cursor: not-allowed;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.jux-fileupload-list {
|
|
358
|
+
margin-top: 12px;
|
|
359
|
+
font-size: 14px;
|
|
360
|
+
color: #6b7280;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.jux-fileupload-item {
|
|
364
|
+
padding: 8px;
|
|
365
|
+
background: #f3f4f6;
|
|
366
|
+
border-radius: 4px;
|
|
367
|
+
margin-bottom: 4px;
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
document.head.appendChild(style);
|
|
314
371
|
}
|
|
315
372
|
}
|
|
316
373
|
|