vanjs-jsf 0.2.0 → 0.2.2
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/dist/VanJsfField.d.ts +4 -2
- package/dist/VanJsfField.js +64 -231
- package/dist/VanJsfForm.js +4 -2
- package/dist/index.js.map +3 -3
- package/package.json +1 -9
package/dist/VanJsfField.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export declare class VanJsfField extends VanJSComponent {
|
|
|
14
14
|
handleChange: (field: VanJsfField, value: MultiType) => void;
|
|
15
15
|
isVisibleState: State<boolean>;
|
|
16
16
|
errorState: State<string>;
|
|
17
|
-
/** Used by file fields to pass
|
|
18
|
-
|
|
17
|
+
/** Used by file fields to pass file metadata to formValues */
|
|
18
|
+
fileNameValue: string;
|
|
19
|
+
fileSizeValue: string;
|
|
20
|
+
fileTypeValue: string;
|
|
19
21
|
constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void);
|
|
20
22
|
get inputType(): string;
|
|
21
23
|
get label(): string;
|
package/dist/VanJsfField.js
CHANGED
|
@@ -7,7 +7,7 @@ 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
9
|
import globals from "globals";
|
|
10
|
-
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, button,
|
|
10
|
+
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, button, strong, small } = van.tags;
|
|
11
11
|
var FieldType;
|
|
12
12
|
(function (FieldType) {
|
|
13
13
|
FieldType["text"] = "text";
|
|
@@ -42,8 +42,10 @@ export class VanJsfField extends VanJSComponent {
|
|
|
42
42
|
handleChange;
|
|
43
43
|
isVisibleState;
|
|
44
44
|
errorState;
|
|
45
|
-
/** Used by file fields to pass
|
|
46
|
-
|
|
45
|
+
/** Used by file fields to pass file metadata to formValues */
|
|
46
|
+
fileNameValue = "";
|
|
47
|
+
fileSizeValue = "";
|
|
48
|
+
fileTypeValue = "";
|
|
47
49
|
constructor(field, initVal, handleChange) {
|
|
48
50
|
super();
|
|
49
51
|
this.field = field;
|
|
@@ -248,17 +250,18 @@ export class VanJsfField extends VanJSComponent {
|
|
|
248
250
|
}), opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
|
|
249
251
|
break;
|
|
250
252
|
case FieldType.file: {
|
|
251
|
-
const accept = this.field.accept || "
|
|
252
|
-
const maxSizeMB = this.field.maxSizeMB
|
|
253
|
-
const
|
|
253
|
+
const accept = this.field.accept || "";
|
|
254
|
+
const maxSizeMB = this.field.maxSizeMB;
|
|
255
|
+
const readAs = this.field.readAs || "auto";
|
|
256
|
+
const TEXT_EXTENSIONS = new Set([
|
|
257
|
+
"json", "csv", "tsv", "txt", "xml", "yaml", "yml",
|
|
258
|
+
"log", "md", "html", "css", "js", "ts", "sql", "env",
|
|
259
|
+
]);
|
|
254
260
|
// Reactive states
|
|
255
261
|
const fileNameState = van.state("");
|
|
256
262
|
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
263
|
const dragOverState = van.state(false);
|
|
264
|
+
const readingState = van.state(false);
|
|
262
265
|
const formatSize = (bytes) => {
|
|
263
266
|
if (bytes < 1024)
|
|
264
267
|
return `${bytes} B`;
|
|
@@ -266,143 +269,65 @@ export class VanJsfField extends VanJSComponent {
|
|
|
266
269
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
267
270
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
268
271
|
};
|
|
269
|
-
const
|
|
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
|
|
272
|
+
const readFile = (file) => {
|
|
304
273
|
this.error = "";
|
|
305
|
-
parsedDataState.val = null;
|
|
306
|
-
arrayPathOptionsState.val = [];
|
|
307
|
-
selectedArrayPathState.val = "";
|
|
308
|
-
this.arrayPathValue = "";
|
|
309
274
|
// Validate size
|
|
310
|
-
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
311
|
-
|
|
275
|
+
if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) {
|
|
276
|
+
this.error = `File exceeds maximum size of ${maxSizeMB} MB`;
|
|
312
277
|
return;
|
|
313
278
|
}
|
|
314
279
|
fileNameState.val = file.name;
|
|
315
280
|
fileSizeState.val = formatSize(file.size);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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");
|
|
281
|
+
readingState.val = true;
|
|
282
|
+
// Store metadata
|
|
283
|
+
this.fileNameValue = file.name;
|
|
284
|
+
this.fileSizeValue = String(file.size);
|
|
285
|
+
this.fileTypeValue = file.type;
|
|
286
|
+
const reader = new FileReader();
|
|
287
|
+
reader.onload = () => {
|
|
288
|
+
readingState.val = false;
|
|
289
|
+
let result = reader.result;
|
|
290
|
+
if (reader.result instanceof ArrayBuffer) {
|
|
291
|
+
// Convert binary to base64 (applies to "arrayBuffer" and "auto" for binary files)
|
|
292
|
+
const bytes = new Uint8Array(reader.result);
|
|
293
|
+
let binary = "";
|
|
294
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
295
|
+
binary += String.fromCharCode(bytes[i]);
|
|
358
296
|
}
|
|
297
|
+
result = btoa(binary);
|
|
359
298
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
}
|
|
299
|
+
this.handleChange(this, result);
|
|
300
|
+
};
|
|
301
|
+
reader.onerror = () => {
|
|
302
|
+
readingState.val = false;
|
|
303
|
+
this.error = "Error reading file";
|
|
304
|
+
};
|
|
305
|
+
if (readAs === "dataURL") {
|
|
306
|
+
reader.readAsDataURL(file);
|
|
307
|
+
}
|
|
308
|
+
else if (readAs === "arrayBuffer") {
|
|
309
|
+
reader.readAsArrayBuffer(file);
|
|
310
|
+
}
|
|
311
|
+
else if (readAs === "auto") {
|
|
312
|
+
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
|
313
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
314
|
+
reader.readAsText(file);
|
|
389
315
|
}
|
|
390
316
|
else {
|
|
391
|
-
|
|
317
|
+
reader.readAsArrayBuffer(file);
|
|
392
318
|
}
|
|
393
319
|
}
|
|
394
|
-
|
|
395
|
-
|
|
320
|
+
else {
|
|
321
|
+
reader.readAsText(file);
|
|
396
322
|
}
|
|
397
323
|
};
|
|
398
324
|
const clearFile = () => {
|
|
399
325
|
fileNameState.val = "";
|
|
400
326
|
fileSizeState.val = "";
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.arrayPathValue = "";
|
|
327
|
+
readingState.val = false;
|
|
328
|
+
this.fileNameValue = "";
|
|
329
|
+
this.fileSizeValue = "";
|
|
330
|
+
this.fileTypeValue = "";
|
|
406
331
|
this.error = "";
|
|
407
332
|
this.handleChange(this, "");
|
|
408
333
|
};
|
|
@@ -413,7 +338,7 @@ export class VanJsfField extends VanJSComponent {
|
|
|
413
338
|
onchange: (e) => {
|
|
414
339
|
const files = e.target.files;
|
|
415
340
|
if (files && files[0])
|
|
416
|
-
|
|
341
|
+
readFile(files[0]);
|
|
417
342
|
},
|
|
418
343
|
});
|
|
419
344
|
const dropZone = div({
|
|
@@ -428,102 +353,12 @@ export class VanJsfField extends VanJSComponent {
|
|
|
428
353
|
dragOverState.val = false;
|
|
429
354
|
const files = e.dataTransfer?.files;
|
|
430
355
|
if (files && files[0])
|
|
431
|
-
|
|
356
|
+
readFile(files[0]);
|
|
432
357
|
},
|
|
433
358
|
onclick: () => fileInput.click(),
|
|
434
|
-
}, p({ style: "margin: 0; color: #666;" },
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
359
|
+
}, p({ style: "margin: 0; color: #666;" }, accept
|
|
360
|
+
? `Drop a file here or click to browse (${accept})`
|
|
361
|
+
: "Drop a file here or click to browse"));
|
|
527
362
|
const fileInfoBar = () => {
|
|
528
363
|
return div(() => {
|
|
529
364
|
const name = fileNameState.val;
|
|
@@ -535,22 +370,20 @@ export class VanJsfField extends VanJSComponent {
|
|
|
535
370
|
onclick: (e) => {
|
|
536
371
|
e.stopPropagation();
|
|
537
372
|
clearFile();
|
|
538
|
-
// Reset the file input so the same file can be re-selected
|
|
539
373
|
fileInput.value = "";
|
|
540
374
|
},
|
|
541
375
|
}, "Clear"));
|
|
542
376
|
});
|
|
543
377
|
};
|
|
544
|
-
|
|
545
|
-
const parsingIndicator = () => {
|
|
378
|
+
const readingIndicator = () => {
|
|
546
379
|
return div(() => {
|
|
547
|
-
if (!
|
|
380
|
+
if (!readingState.val)
|
|
548
381
|
return div();
|
|
549
|
-
return div({ style: "margin-top: 8px; color: #666;" }, "
|
|
382
|
+
return div({ style: "margin-top: 8px; color: #666;" }, "Reading file...");
|
|
550
383
|
});
|
|
551
384
|
};
|
|
552
385
|
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(),
|
|
386
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), readingIndicator(), p({ class: this.errorClass }, () => this.error));
|
|
554
387
|
break;
|
|
555
388
|
}
|
|
556
389
|
default:
|
package/dist/VanJsfForm.js
CHANGED
|
@@ -60,9 +60,11 @@ class VanJsfForm {
|
|
|
60
60
|
}
|
|
61
61
|
handleFieldChange(field, value) {
|
|
62
62
|
this.formValues[field.name] = value;
|
|
63
|
-
// For file fields, also store
|
|
63
|
+
// For file fields, also store file metadata
|
|
64
64
|
if (field.inputType === "file") {
|
|
65
|
-
this.formValues[field.name + "
|
|
65
|
+
this.formValues[field.name + "__fileName"] = field.fileNameValue;
|
|
66
|
+
this.formValues[field.name + "__fileSize"] = field.fileSizeValue;
|
|
67
|
+
this.formValues[field.name + "__fileType"] = field.fileTypeValue;
|
|
66
68
|
}
|
|
67
69
|
this.config.formValues = this.formValues;
|
|
68
70
|
const { formErrors } = this.headlessForm.handleValidation(this.formValues);
|