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.
@@ -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
- ".cm-content, .cm-gutter": {
137
- minHeight: this.field["min-height"] &&
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
- ".cm-gutters": {
143
- margin: "1px",
74
+ '.cm-content': {
75
+ "min-height": "150px",
144
76
  },
145
- ".cm-scroller": {
146
- overflow: "auto",
77
+ '.cm-gutters': {
78
+ margin: '1px',
147
79
  },
148
- ".cm-wrap": {
149
- border: "1px solid silver",
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 ? this.containerClass : "",
147
+ class: this.containerClass || ''
219
148
  };
220
149
  switch (this.inputType) {
221
150
  case FieldType.text:
222
- el = div(props, label({
223
- for: this.name,
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 ? 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
- for: this.name,
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 ? this.class : null,
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
- for: this.name,
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: new String(this.iniVal).toString(),
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
- for: this.name,
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 ? this.class : null,
185
+ class: this.class || '',
285
186
  oninput: (e) => this.handleChange(this, e.target.value),
286
- }, this.options?.map((opt) => option({ class: this.class ? this.class : null, value: opt.value }, opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
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 ? this.class : null,
193
+ class: this.class || '',
293
194
  value: this.iniVal,
294
195
  onchange: (e) => this.handleChange(this, e.target.value),
295
196
  });
296
- el = div(props, label({
297
- for: this.name,
298
- style: "margin-right: 5px;",
299
- class: this.titleClass ? this.titleClass : "",
300
- }, this.label), this.description &&
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: "YYYY/MM/DD",
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, format) {
323
- // dateString is the result of `toString` method
324
- const parts = dateString.split("/");
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 year = parseInt(parts[2], 10);
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
- for: this.name,
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 ? this.class : null,
228
+ class: this.class || '',
366
229
  value: this.iniVal,
367
- oninput: (e) => this.handleChange(this, e.target.value),
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
- console.log(this.field);
372
- el = div(props, fieldset(legend({ class: this.titleClass ? this.titleClass : "" }, this.label), this.description &&
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 ? this.titleClass : "" }, this.label), this.description && div(this.description), div(this.options?.map((opt) => label(input({
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 ? this.class : null,
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 (Array.isArray(fields) &&
397
- fields.every((field) => field instanceof VanJsfField));
562
+ return Array.isArray(fields) && fields.every(field => field instanceof VanJsfField);
398
563
  }
399
564
  }