vanjs-jsf 0.0.19 → 0.2.0
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/LICENSE +201 -201
- package/README.md +134 -102
- package/dist/VanJsfField.d.ts +4 -3
- package/dist/VanJsfField.js +381 -216
- package/dist/VanJsfForm.js +12 -18
- package/dist/index.js.map +4 -4
- package/package.json +64 -55
package/dist/VanJsfField.js
CHANGED
|
@@ -6,23 +6,35 @@ import { javascript, esLint } from "@codemirror/lang-javascript";
|
|
|
6
6
|
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
|
7
7
|
import { lintGutter, linter, forEachDiagnostic } from "@codemirror/lint";
|
|
8
8
|
import * as eslint from "eslint-linter-browserify";
|
|
9
|
-
import { CronComponent } from "van-ui-extended";
|
|
10
|
-
import { dracula } from "thememirror";
|
|
11
|
-
import "van-ui-extended/dist/index.css";
|
|
12
|
-
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, } = van.tags;
|
|
13
9
|
import globals from "globals";
|
|
10
|
+
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, button, table, tr, th, td, strong, small } = van.tags;
|
|
14
11
|
var FieldType;
|
|
15
12
|
(function (FieldType) {
|
|
16
13
|
FieldType["text"] = "text";
|
|
17
14
|
FieldType["code"] = "code";
|
|
18
|
-
FieldType["cron"] = "cron";
|
|
19
15
|
FieldType["number"] = "number";
|
|
20
16
|
FieldType["textarea"] = "textarea";
|
|
21
17
|
FieldType["select"] = "select";
|
|
22
18
|
FieldType["radio"] = "radio";
|
|
23
19
|
FieldType["date"] = "date";
|
|
24
20
|
FieldType["fieldset"] = "fieldset";
|
|
21
|
+
FieldType["file"] = "file";
|
|
25
22
|
})(FieldType || (FieldType = {}));
|
|
23
|
+
const eslintConfig = {
|
|
24
|
+
// eslint configuration
|
|
25
|
+
languageOptions: {
|
|
26
|
+
globals: {
|
|
27
|
+
...globals.node,
|
|
28
|
+
},
|
|
29
|
+
parserOptions: {
|
|
30
|
+
ecmaVersion: 2022,
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
rules: {
|
|
35
|
+
semi: ["error", "never"],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
26
38
|
export class VanJsfField extends VanJSComponent {
|
|
27
39
|
name;
|
|
28
40
|
field;
|
|
@@ -30,6 +42,8 @@ export class VanJsfField extends VanJSComponent {
|
|
|
30
42
|
handleChange;
|
|
31
43
|
isVisibleState;
|
|
32
44
|
errorState;
|
|
45
|
+
/** Used by file fields to pass the selected arrayPath key to formValues */
|
|
46
|
+
arrayPathValue = "";
|
|
33
47
|
constructor(field, initVal, handleChange) {
|
|
34
48
|
super();
|
|
35
49
|
this.field = field;
|
|
@@ -38,7 +52,6 @@ export class VanJsfField extends VanJSComponent {
|
|
|
38
52
|
this.handleChange = handleChange;
|
|
39
53
|
this.isVisibleState = van.state(this.field.isVisible);
|
|
40
54
|
this.errorState = van.state("");
|
|
41
|
-
van.derive(() => console.log(`Field ${this.name} isVisible: ${this.isVisibleState.val}`));
|
|
42
55
|
}
|
|
43
56
|
get inputType() {
|
|
44
57
|
return this.field.inputType;
|
|
@@ -52,108 +65,26 @@ export class VanJsfField extends VanJSComponent {
|
|
|
52
65
|
get errorClass() {
|
|
53
66
|
return this.field.errorClass;
|
|
54
67
|
}
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
69
|
get codemirrorExtension() {
|
|
56
|
-
const fieldGlobals = this.field.globals && typeof this.field.globals === "object"
|
|
57
|
-
? this.field.globals
|
|
58
|
-
: {};
|
|
59
|
-
const eslintConfig = {
|
|
60
|
-
// eslint configuration
|
|
61
|
-
languageOptions: {
|
|
62
|
-
globals: {
|
|
63
|
-
...fieldGlobals,
|
|
64
|
-
...globals.node,
|
|
65
|
-
},
|
|
66
|
-
parserOptions: {
|
|
67
|
-
ecmaVersion: 2022,
|
|
68
|
-
sourceType: "module",
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
rules: {
|
|
72
|
-
"constructor-super": "error",
|
|
73
|
-
"for-direction": "error",
|
|
74
|
-
"getter-return": "error",
|
|
75
|
-
"no-async-promise-executor": "error",
|
|
76
|
-
"no-case-declarations": "error",
|
|
77
|
-
"no-class-assign": "error",
|
|
78
|
-
"no-compare-neg-zero": "error",
|
|
79
|
-
"no-cond-assign": "error",
|
|
80
|
-
"no-const-assign": "error",
|
|
81
|
-
"no-constant-binary-expression": "error",
|
|
82
|
-
"no-constant-condition": "error",
|
|
83
|
-
"no-control-regex": "error",
|
|
84
|
-
"no-debugger": "error",
|
|
85
|
-
"no-delete-var": "error",
|
|
86
|
-
"no-dupe-args": "error",
|
|
87
|
-
"no-dupe-class-members": "error",
|
|
88
|
-
"no-dupe-else-if": "error",
|
|
89
|
-
"no-dupe-keys": "error",
|
|
90
|
-
"no-duplicate-case": "error",
|
|
91
|
-
"no-empty": "error",
|
|
92
|
-
"no-empty-character-class": "error",
|
|
93
|
-
"no-empty-pattern": "error",
|
|
94
|
-
"no-empty-static-block": "error",
|
|
95
|
-
"no-ex-assign": "error",
|
|
96
|
-
"no-extra-boolean-cast": "error",
|
|
97
|
-
"no-fallthrough": "error",
|
|
98
|
-
"no-func-assign": "error",
|
|
99
|
-
"no-global-assign": "error",
|
|
100
|
-
"no-import-assign": "error",
|
|
101
|
-
"no-invalid-regexp": "error",
|
|
102
|
-
"no-irregular-whitespace": "error",
|
|
103
|
-
"no-loss-of-precision": "error",
|
|
104
|
-
"no-misleading-character-class": "error",
|
|
105
|
-
"no-new-native-nonconstructor": "error",
|
|
106
|
-
"no-nonoctal-decimal-escape": "error",
|
|
107
|
-
"no-obj-calls": "error",
|
|
108
|
-
"no-octal": "error",
|
|
109
|
-
"no-prototype-builtins": "error",
|
|
110
|
-
"no-redeclare": "error",
|
|
111
|
-
"no-regex-spaces": "error",
|
|
112
|
-
"no-self-assign": "error",
|
|
113
|
-
"no-setter-return": "error",
|
|
114
|
-
"no-shadow-restricted-names": "error",
|
|
115
|
-
"no-sparse-arrays": "error",
|
|
116
|
-
"no-this-before-super": "error",
|
|
117
|
-
"no-undef": "error",
|
|
118
|
-
"no-unexpected-multiline": "error",
|
|
119
|
-
"no-unreachable": "error",
|
|
120
|
-
"no-unsafe-finally": "error",
|
|
121
|
-
"no-unsafe-negation": "error",
|
|
122
|
-
"no-unsafe-optional-chaining": "error",
|
|
123
|
-
"no-unused-labels": "error",
|
|
124
|
-
"no-unused-private-class-members": "error",
|
|
125
|
-
"no-unused-vars": "error",
|
|
126
|
-
"no-useless-backreference": "error",
|
|
127
|
-
"no-useless-catch": "error",
|
|
128
|
-
"no-useless-escape": "error",
|
|
129
|
-
"no-with": "error",
|
|
130
|
-
"require-yield": "error",
|
|
131
|
-
"use-isnan": "error",
|
|
132
|
-
"valid-typeof": "error",
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
70
|
const theme = EditorView.theme({
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
typeof this.field["min-height"] === "string"
|
|
139
|
-
? this.field["min-height"]
|
|
140
|
-
: "150px",
|
|
71
|
+
'.cm-content, .cm-gutter': {
|
|
72
|
+
"min-height": "150px",
|
|
141
73
|
},
|
|
142
|
-
|
|
143
|
-
|
|
74
|
+
'.cm-content': {
|
|
75
|
+
"min-height": "150px",
|
|
144
76
|
},
|
|
145
|
-
|
|
146
|
-
|
|
77
|
+
'.cm-gutters': {
|
|
78
|
+
margin: '1px',
|
|
147
79
|
},
|
|
148
|
-
|
|
149
|
-
|
|
80
|
+
'.cm-scroller': {
|
|
81
|
+
overflow: 'auto',
|
|
82
|
+
},
|
|
83
|
+
'.cm-wrap': {
|
|
84
|
+
border: '1px solid silver',
|
|
150
85
|
},
|
|
151
|
-
}, {
|
|
152
|
-
dark: true,
|
|
153
86
|
});
|
|
154
|
-
const extensions = [
|
|
155
|
-
dracula,
|
|
156
|
-
EditorView.updateListener.of((e) => {
|
|
87
|
+
const extensions = [theme, EditorView.updateListener.of((e) => {
|
|
157
88
|
this.field.error = null;
|
|
158
89
|
forEachDiagnostic(e.state, (diag) => {
|
|
159
90
|
if (diag.severity === "error") {
|
|
@@ -161,10 +92,7 @@ export class VanJsfField extends VanJSComponent {
|
|
|
161
92
|
}
|
|
162
93
|
});
|
|
163
94
|
this.handleChange(this, e.state.doc.toString());
|
|
164
|
-
}),
|
|
165
|
-
basicSetup,
|
|
166
|
-
lintGutter(),
|
|
167
|
-
];
|
|
95
|
+
}), basicSetup, lintGutter()];
|
|
168
96
|
switch (this.field.codemirrorType) {
|
|
169
97
|
case "json":
|
|
170
98
|
extensions.push(json(), linter(jsonParseLinter()));
|
|
@@ -213,187 +141,424 @@ export class VanJsfField extends VanJSComponent {
|
|
|
213
141
|
}
|
|
214
142
|
render() {
|
|
215
143
|
let el;
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
145
|
const props = {
|
|
217
146
|
style: () => (this.isVisible ? "display: block" : "display: none"),
|
|
218
|
-
class: this.containerClass
|
|
147
|
+
class: this.containerClass || ''
|
|
219
148
|
};
|
|
220
149
|
switch (this.inputType) {
|
|
221
150
|
case FieldType.text:
|
|
222
|
-
el = div(props, label({
|
|
223
|
-
|
|
224
|
-
style: "margin-right: 5px;",
|
|
225
|
-
class: this.titleClass ? this.titleClass : "",
|
|
226
|
-
}, this.label), this.description &&
|
|
227
|
-
div({
|
|
228
|
-
id: `${this.name}-description`,
|
|
229
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
230
|
-
}, this.description), input({
|
|
151
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
152
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
|
|
231
153
|
id: this.name,
|
|
232
154
|
type: "text",
|
|
233
|
-
class: this.class
|
|
155
|
+
class: this.class || '',
|
|
234
156
|
value: this.iniVal,
|
|
235
157
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
236
158
|
}), p({ class: this.errorClass }, () => this.error));
|
|
237
159
|
break;
|
|
238
160
|
case FieldType.textarea:
|
|
239
|
-
el = div(props, label({
|
|
240
|
-
|
|
241
|
-
style: "margin-right: 5px;",
|
|
242
|
-
class: this.titleClass ? this.titleClass : "",
|
|
243
|
-
}, this.label), this.description &&
|
|
244
|
-
div({
|
|
245
|
-
id: `${this.name}-description`,
|
|
246
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
247
|
-
}, this.description), textarea({
|
|
161
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
162
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), textarea({
|
|
248
163
|
id: this.name,
|
|
249
164
|
name: this.name,
|
|
250
|
-
class: this.class
|
|
165
|
+
class: this.class || '',
|
|
251
166
|
rows: this.field.rows,
|
|
252
167
|
cols: this.field.columns,
|
|
253
168
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
254
169
|
}), p({ class: this.errorClass }, () => this.error));
|
|
255
170
|
break;
|
|
256
171
|
case FieldType.code:
|
|
257
|
-
el = div(props, label({
|
|
258
|
-
|
|
259
|
-
style: "margin-right: 5px;",
|
|
260
|
-
class: this.titleClass ? this.titleClass : "",
|
|
261
|
-
}, this.label), this.description &&
|
|
262
|
-
div({
|
|
263
|
-
id: `${this.name}-description`,
|
|
264
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
265
|
-
}, this.description));
|
|
172
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
173
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description));
|
|
266
174
|
new EditorView({
|
|
267
|
-
doc:
|
|
175
|
+
doc: String(this.iniVal),
|
|
268
176
|
parent: el,
|
|
269
|
-
extensions: this.codemirrorExtension
|
|
177
|
+
extensions: this.codemirrorExtension
|
|
270
178
|
});
|
|
271
179
|
break;
|
|
272
180
|
case FieldType.select:
|
|
273
|
-
el = div(props, label({
|
|
274
|
-
|
|
275
|
-
style: "margin-right: 5px;",
|
|
276
|
-
class: this.titleClass ? this.titleClass : "",
|
|
277
|
-
}, this.label), this.description &&
|
|
278
|
-
div({
|
|
279
|
-
id: `${this.name}-description`,
|
|
280
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
281
|
-
}, this.description), select({
|
|
181
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
182
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), select({
|
|
282
183
|
id: this.name,
|
|
283
184
|
name: this.name,
|
|
284
|
-
class: this.class
|
|
185
|
+
class: this.class || '',
|
|
285
186
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
286
|
-
}, this.options?.map((opt) => option({ class: this.class
|
|
187
|
+
}, this.options?.map((opt) => option({ class: this.class || '', value: opt.value }, opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
|
|
287
188
|
break;
|
|
288
|
-
case FieldType.date:
|
|
189
|
+
case FieldType.date: {
|
|
289
190
|
const calendarInput = input({
|
|
290
191
|
id: this.name,
|
|
291
192
|
type: "text",
|
|
292
|
-
class: this.class
|
|
193
|
+
class: this.class || '',
|
|
293
194
|
value: this.iniVal,
|
|
294
195
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
295
196
|
});
|
|
296
|
-
el =
|
|
297
|
-
for: this.name,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
div({
|
|
302
|
-
id: `${this.name}-description`,
|
|
303
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
304
|
-
}, this.description), calendarInput, p({ class: this.errorClass }, () => this.error), link({
|
|
305
|
-
rel: "stylesheet",
|
|
306
|
-
type: "text/css",
|
|
307
|
-
href: "https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css",
|
|
308
|
-
}));
|
|
197
|
+
el =
|
|
198
|
+
div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
199
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), calendarInput, p({ class: this.errorClass }, () => this.error),
|
|
200
|
+
// External CDN dependency for Pikaday CSS — consider bundling for production
|
|
201
|
+
link({ rel: "stylesheet", type: "text/css", href: "https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css" }));
|
|
309
202
|
new pikaday({
|
|
310
203
|
field: calendarInput,
|
|
311
|
-
format:
|
|
204
|
+
format: 'YYYY-MM-DD',
|
|
312
205
|
container: el,
|
|
313
206
|
firstDay: 1,
|
|
314
207
|
toString(date) {
|
|
315
|
-
// you should do formatting based on the passed format,
|
|
316
|
-
// but we will just return 'D/M/YYYY' for simplicity
|
|
317
208
|
const day = date.getDate();
|
|
318
209
|
const month = date.getMonth() + 1;
|
|
319
210
|
const year = date.getFullYear();
|
|
320
211
|
return `${year}-${("0" + month).slice(-2)}-${("0" + day).slice(-2)}`;
|
|
321
212
|
},
|
|
322
|
-
parse(dateString
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
const day = parseInt(parts[0], 10);
|
|
213
|
+
parse(dateString) {
|
|
214
|
+
const parts = dateString.split('-');
|
|
215
|
+
const year = parseInt(parts[0], 10);
|
|
326
216
|
const month = parseInt(parts[1], 10) - 1;
|
|
327
|
-
const
|
|
217
|
+
const day = parseInt(parts[2], 10);
|
|
328
218
|
return new Date(year, month, day);
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
break;
|
|
332
|
-
case FieldType.cron:
|
|
333
|
-
el = div(props, label({
|
|
334
|
-
for: this.name,
|
|
335
|
-
style: "margin-right: 5px;",
|
|
336
|
-
class: this.titleClass ? this.titleClass : "",
|
|
337
|
-
}, this.label), this.description &&
|
|
338
|
-
div({
|
|
339
|
-
id: `${this.name}-description`,
|
|
340
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
341
|
-
}, this.description), p({ class: this.errorClass }, () => this.error), () => {
|
|
342
|
-
let ele;
|
|
343
|
-
if (CronComponent) {
|
|
344
|
-
ele = new CronComponent() || null;
|
|
345
|
-
ele.setAttribute("color", "d58512");
|
|
346
|
-
ele.setAttribute("extraClass", this.class ? this.class : "");
|
|
347
|
-
ele.setAttribute("value", this.iniVal.toString());
|
|
348
|
-
ele.oninput = (e) => this.handleChange(this, e.detail.value);
|
|
349
219
|
}
|
|
350
|
-
return ele;
|
|
351
220
|
});
|
|
352
221
|
break;
|
|
222
|
+
}
|
|
353
223
|
case FieldType.number:
|
|
354
|
-
el = div(props, label({
|
|
355
|
-
|
|
356
|
-
style: "margin-right: 5px;",
|
|
357
|
-
class: this.titleClass ? this.titleClass : "",
|
|
358
|
-
}, this.label), this.description &&
|
|
359
|
-
div({
|
|
360
|
-
id: `${this.name}-description`,
|
|
361
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
362
|
-
}, this.description), input({
|
|
224
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
225
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
|
|
363
226
|
id: this.name,
|
|
364
227
|
type: "number",
|
|
365
|
-
class: this.class
|
|
228
|
+
class: this.class || '',
|
|
366
229
|
value: this.iniVal,
|
|
367
|
-
oninput: (e) =>
|
|
230
|
+
oninput: (e) => {
|
|
231
|
+
const val = e.target.value;
|
|
232
|
+
this.handleChange(this, val === "" ? "" : Number(val));
|
|
233
|
+
},
|
|
368
234
|
}), p({ class: this.errorClass }, () => this.error));
|
|
369
235
|
break;
|
|
370
236
|
case FieldType.fieldset:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
span({
|
|
374
|
-
id: `${this.name}-description`,
|
|
375
|
-
class: this.descriptionClass ? this.descriptionClass : "",
|
|
376
|
-
}, this.description), this.isVanJsfFieldArray(this.field.fields)
|
|
377
|
-
? this.field.fields.map((field) => field.render())
|
|
378
|
-
: null));
|
|
237
|
+
el = div(props, fieldset(legend({ class: this.titleClass || '' }, this.label), this.description &&
|
|
238
|
+
span({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), this.isVanJsfFieldArray(this.field.fields) ? this.field.fields.map((field) => field.render()) : null));
|
|
379
239
|
break;
|
|
380
240
|
case FieldType.radio:
|
|
381
|
-
el = div(legend({ class: this.titleClass
|
|
241
|
+
el = div(legend({ class: this.titleClass || '' }, this.label), this.description && div(this.description), div(this.options?.map((opt) => label(input({
|
|
382
242
|
type: "radio",
|
|
383
243
|
name: this.name,
|
|
384
|
-
class: this.class
|
|
244
|
+
class: this.class || '',
|
|
385
245
|
value: opt.value,
|
|
386
246
|
checked: this.iniVal === opt.value,
|
|
387
247
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
388
|
-
}), opt.label, opt.description), p({ class: this.errorClass }, () => this.error))
|
|
248
|
+
}), opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
|
|
249
|
+
break;
|
|
250
|
+
case FieldType.file: {
|
|
251
|
+
const accept = this.field.accept || ".json,.csv,.xlsx";
|
|
252
|
+
const maxSizeMB = this.field.maxSizeMB || 50;
|
|
253
|
+
const previewRows = this.field.previewRows || 5;
|
|
254
|
+
// Reactive states
|
|
255
|
+
const fileNameState = van.state("");
|
|
256
|
+
const fileSizeState = van.state("");
|
|
257
|
+
const parsingState = van.state(false);
|
|
258
|
+
const parsedDataState = van.state(null);
|
|
259
|
+
const arrayPathOptionsState = van.state([]);
|
|
260
|
+
const selectedArrayPathState = van.state("");
|
|
261
|
+
const dragOverState = van.state(false);
|
|
262
|
+
const formatSize = (bytes) => {
|
|
263
|
+
if (bytes < 1024)
|
|
264
|
+
return `${bytes} B`;
|
|
265
|
+
if (bytes < 1024 * 1024)
|
|
266
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
267
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
268
|
+
};
|
|
269
|
+
const formatNumber = (n) => n.toLocaleString();
|
|
270
|
+
const resolveArrayFromJson = (parsed) => {
|
|
271
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "object") {
|
|
272
|
+
return { data: parsed, paths: [] };
|
|
273
|
+
}
|
|
274
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
275
|
+
const candidates = [];
|
|
276
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
277
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object") {
|
|
278
|
+
candidates.push(key);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (candidates.length === 1) {
|
|
282
|
+
return { data: parsed[candidates[0]], paths: [] };
|
|
283
|
+
}
|
|
284
|
+
if (candidates.length > 1) {
|
|
285
|
+
return { data: null, paths: candidates };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { data: null, paths: [] };
|
|
289
|
+
};
|
|
290
|
+
const setData = (data, arrayPath) => {
|
|
291
|
+
parsedDataState.val = data;
|
|
292
|
+
this.arrayPathValue = arrayPath;
|
|
293
|
+
selectedArrayPathState.val = arrayPath;
|
|
294
|
+
parsingState.val = false;
|
|
295
|
+
this.handleChange(this, JSON.stringify(data));
|
|
296
|
+
};
|
|
297
|
+
const setError = (msg) => {
|
|
298
|
+
parsingState.val = false;
|
|
299
|
+
parsedDataState.val = null;
|
|
300
|
+
this.error = msg;
|
|
301
|
+
};
|
|
302
|
+
const processFile = async (file) => {
|
|
303
|
+
// Reset state
|
|
304
|
+
this.error = "";
|
|
305
|
+
parsedDataState.val = null;
|
|
306
|
+
arrayPathOptionsState.val = [];
|
|
307
|
+
selectedArrayPathState.val = "";
|
|
308
|
+
this.arrayPathValue = "";
|
|
309
|
+
// Validate size
|
|
310
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
311
|
+
setError(`File exceeds maximum size of ${maxSizeMB} MB`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
fileNameState.val = file.name;
|
|
315
|
+
fileSizeState.val = formatSize(file.size);
|
|
316
|
+
parsingState.val = true;
|
|
317
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
318
|
+
try {
|
|
319
|
+
if (ext === "json") {
|
|
320
|
+
const text = await file.text();
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(text);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
setError("Invalid JSON file");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const { data, paths } = resolveArrayFromJson(parsed);
|
|
330
|
+
if (data) {
|
|
331
|
+
setData(data, "");
|
|
332
|
+
}
|
|
333
|
+
else if (paths.length > 1) {
|
|
334
|
+
arrayPathOptionsState.val = paths;
|
|
335
|
+
parsingState.val = false;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
setError("JSON does not contain an array of objects");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (ext === "csv") {
|
|
342
|
+
try {
|
|
343
|
+
const Papa = await import("papaparse");
|
|
344
|
+
const text = await file.text();
|
|
345
|
+
const result = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
346
|
+
if (result.errors.length > 0) {
|
|
347
|
+
setError(`CSV parse error: ${result.errors[0].message}`);
|
|
348
|
+
}
|
|
349
|
+
else if (!result.data || result.data.length === 0) {
|
|
350
|
+
setError("CSV file is empty or has no data rows");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
setData(result.data, "");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
setError("Install papaparse to support CSV files: npm install papaparse");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (ext === "xlsx" || ext === "xls") {
|
|
361
|
+
try {
|
|
362
|
+
const XLSX = await import("xlsx");
|
|
363
|
+
const buffer = await file.arrayBuffer();
|
|
364
|
+
const workbook = XLSX.read(buffer);
|
|
365
|
+
const firstSheetName = workbook.SheetNames[0];
|
|
366
|
+
if (!firstSheetName) {
|
|
367
|
+
setError("XLSX file has no sheets");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
371
|
+
const data = XLSX.utils.sheet_to_json(sheet);
|
|
372
|
+
if (data.length === 0) {
|
|
373
|
+
setError("XLSX sheet is empty");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
setData(data, "");
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
if (e instanceof Error && e.message.includes("Failed to fetch dynamically imported module")) {
|
|
380
|
+
setError("Install xlsx to support XLSX files: npm install xlsx");
|
|
381
|
+
}
|
|
382
|
+
else if (e instanceof Error) {
|
|
383
|
+
setError(`XLSX parse error: ${e.message}`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
setError("Install xlsx to support XLSX files: npm install xlsx");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
setError(`Unsupported file extension: .${ext}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
setError(e instanceof Error ? e.message : "Error processing file");
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const clearFile = () => {
|
|
399
|
+
fileNameState.val = "";
|
|
400
|
+
fileSizeState.val = "";
|
|
401
|
+
parsedDataState.val = null;
|
|
402
|
+
parsingState.val = false;
|
|
403
|
+
arrayPathOptionsState.val = [];
|
|
404
|
+
selectedArrayPathState.val = "";
|
|
405
|
+
this.arrayPathValue = "";
|
|
406
|
+
this.error = "";
|
|
407
|
+
this.handleChange(this, "");
|
|
408
|
+
};
|
|
409
|
+
const fileInput = input({
|
|
410
|
+
type: "file",
|
|
411
|
+
accept,
|
|
412
|
+
style: "display: none;",
|
|
413
|
+
onchange: (e) => {
|
|
414
|
+
const files = e.target.files;
|
|
415
|
+
if (files && files[0])
|
|
416
|
+
processFile(files[0]);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
const dropZone = div({
|
|
420
|
+
style: () => {
|
|
421
|
+
const over = dragOverState.val;
|
|
422
|
+
return `border: 2px dashed ${over ? "#4a90d9" : "#ccc"}; border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: border-color 0.2s; background: ${over ? "#f0f7ff" : "transparent"};`;
|
|
423
|
+
},
|
|
424
|
+
ondragover: (e) => { e.preventDefault(); dragOverState.val = true; },
|
|
425
|
+
ondragleave: () => { dragOverState.val = false; },
|
|
426
|
+
ondrop: (e) => {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
dragOverState.val = false;
|
|
429
|
+
const files = e.dataTransfer?.files;
|
|
430
|
+
if (files && files[0])
|
|
431
|
+
processFile(files[0]);
|
|
432
|
+
},
|
|
433
|
+
onclick: () => fileInput.click(),
|
|
434
|
+
}, p({ style: "margin: 0; color: #666;" }, `Drop a file here or click to browse (${accept})`));
|
|
435
|
+
// We need to store the raw JSON for arrayPath selection
|
|
436
|
+
const rawJsonState = van.state(null);
|
|
437
|
+
// Override processFile's JSON branch to also store raw JSON
|
|
438
|
+
const originalProcessFile = processFile;
|
|
439
|
+
const processFileWrapped = async (file) => {
|
|
440
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
441
|
+
if (ext === "json") {
|
|
442
|
+
this.error = "";
|
|
443
|
+
parsedDataState.val = null;
|
|
444
|
+
arrayPathOptionsState.val = [];
|
|
445
|
+
selectedArrayPathState.val = "";
|
|
446
|
+
this.arrayPathValue = "";
|
|
447
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
448
|
+
setError(`File exceeds maximum size of ${maxSizeMB} MB`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
fileNameState.val = file.name;
|
|
452
|
+
fileSizeState.val = formatSize(file.size);
|
|
453
|
+
parsingState.val = true;
|
|
454
|
+
try {
|
|
455
|
+
const text = await file.text();
|
|
456
|
+
let parsed;
|
|
457
|
+
try {
|
|
458
|
+
parsed = JSON.parse(text);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
setError("Invalid JSON file");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const { data, paths } = resolveArrayFromJson(parsed);
|
|
465
|
+
if (data) {
|
|
466
|
+
setData(data, "");
|
|
467
|
+
}
|
|
468
|
+
else if (paths.length > 1) {
|
|
469
|
+
rawJsonState.val = parsed;
|
|
470
|
+
arrayPathOptionsState.val = paths;
|
|
471
|
+
parsingState.val = false;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
setError("JSON does not contain an array of objects");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
setError(e instanceof Error ? e.message : "Error processing file");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
await originalProcessFile(file);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
// Re-bind event handlers to use wrapped version
|
|
486
|
+
fileInput.onchange = (e) => {
|
|
487
|
+
const files = e.target.files;
|
|
488
|
+
if (files && files[0])
|
|
489
|
+
processFileWrapped(files[0]);
|
|
490
|
+
};
|
|
491
|
+
dropZone.ondrop = (e) => {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
dragOverState.val = false;
|
|
494
|
+
const files = e.dataTransfer?.files;
|
|
495
|
+
if (files && files[0])
|
|
496
|
+
processFileWrapped(files[0]);
|
|
497
|
+
};
|
|
498
|
+
// Preview table
|
|
499
|
+
const previewTable = () => {
|
|
500
|
+
return div(() => {
|
|
501
|
+
const data = parsedDataState.val;
|
|
502
|
+
if (!data || data.length === 0)
|
|
503
|
+
return div();
|
|
504
|
+
const columns = Object.keys(data[0]);
|
|
505
|
+
const rows = data.slice(0, previewRows);
|
|
506
|
+
return div({ style: "margin-top: 8px; overflow-x: auto;" }, div({ style: "margin-bottom: 4px; font-size: 0.9em; color: #666;" }, `Showing ${Math.min(previewRows, data.length)} of ${formatNumber(data.length)} rows`), table({ style: "border-collapse: collapse; width: 100%; font-size: 0.85em;" }, tr(...columns.map((col) => th({ style: "border: 1px solid #ddd; padding: 4px 8px; background: #f5f5f5; text-align: left; white-space: nowrap;" }, col))), ...rows.map((row) => tr(...columns.map((col) => td({ style: "border: 1px solid #ddd; padding: 4px 8px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, String(row[col] ?? "")))))));
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
// arrayPath select handler — when user picks a path, extract data
|
|
510
|
+
const arrayPathSelectorEl = div(() => {
|
|
511
|
+
const options = arrayPathOptionsState.val;
|
|
512
|
+
if (options.length === 0)
|
|
513
|
+
return div();
|
|
514
|
+
return div({ style: "margin-top: 8px;" }, label({ style: "margin-right: 5px;" }, "Select data array:"), select({
|
|
515
|
+
onchange: (e) => {
|
|
516
|
+
const key = e.target.value;
|
|
517
|
+
if (!key)
|
|
518
|
+
return;
|
|
519
|
+
const raw = rawJsonState.val;
|
|
520
|
+
if (raw && Array.isArray(raw[key])) {
|
|
521
|
+
setData(raw[key], key);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
}, option({ value: "" }, "-- choose --"), ...options.map((k) => option({ value: k }, k))));
|
|
525
|
+
});
|
|
526
|
+
// File info bar + clear button
|
|
527
|
+
const fileInfoBar = () => {
|
|
528
|
+
return div(() => {
|
|
529
|
+
const name = fileNameState.val;
|
|
530
|
+
if (!name)
|
|
531
|
+
return div();
|
|
532
|
+
return div({ style: "margin-top: 8px; display: flex; align-items: center; gap: 8px;" }, strong(name), small({ style: "color: #888;" }, `(${fileSizeState.val})`), button({
|
|
533
|
+
type: "button",
|
|
534
|
+
style: "cursor: pointer; background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; font-size: 0.85em;",
|
|
535
|
+
onclick: (e) => {
|
|
536
|
+
e.stopPropagation();
|
|
537
|
+
clearFile();
|
|
538
|
+
// Reset the file input so the same file can be re-selected
|
|
539
|
+
fileInput.value = "";
|
|
540
|
+
},
|
|
541
|
+
}, "Clear"));
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
// Parsing indicator
|
|
545
|
+
const parsingIndicator = () => {
|
|
546
|
+
return div(() => {
|
|
547
|
+
if (!parsingState.val)
|
|
548
|
+
return div();
|
|
549
|
+
return div({ style: "margin-top: 8px; color: #666;" }, "Parsing file...");
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
553
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), parsingIndicator(), arrayPathSelectorEl, previewTable(), p({ class: this.errorClass }, () => this.error));
|
|
389
554
|
break;
|
|
555
|
+
}
|
|
390
556
|
default:
|
|
391
557
|
el = div({ style: "border: 1px dashed gray; padding: 8px;" }, `Field "${this.name}" unsupported: The type "${this.inputType}" has no UI component built yet.`);
|
|
392
558
|
}
|
|
393
559
|
return el;
|
|
394
560
|
}
|
|
395
561
|
isVanJsfFieldArray(fields) {
|
|
396
|
-
return
|
|
397
|
-
fields.every((field) => field instanceof VanJsfField));
|
|
562
|
+
return Array.isArray(fields) && fields.every(field => field instanceof VanJsfField);
|
|
398
563
|
}
|
|
399
564
|
}
|