vanjs-jsf 0.2.0 → 0.2.1
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 +54 -234
- 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,14 @@ 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 || "text";
|
|
254
256
|
// Reactive states
|
|
255
257
|
const fileNameState = van.state("");
|
|
256
258
|
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
259
|
const dragOverState = van.state(false);
|
|
260
|
+
const readingState = van.state(false);
|
|
262
261
|
const formatSize = (bytes) => {
|
|
263
262
|
if (bytes < 1024)
|
|
264
263
|
return `${bytes} B`;
|
|
@@ -266,143 +265,56 @@ export class VanJsfField extends VanJSComponent {
|
|
|
266
265
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
267
266
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
268
267
|
};
|
|
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
|
|
268
|
+
const readFile = (file) => {
|
|
304
269
|
this.error = "";
|
|
305
|
-
parsedDataState.val = null;
|
|
306
|
-
arrayPathOptionsState.val = [];
|
|
307
|
-
selectedArrayPathState.val = "";
|
|
308
|
-
this.arrayPathValue = "";
|
|
309
270
|
// Validate size
|
|
310
|
-
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
311
|
-
|
|
271
|
+
if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) {
|
|
272
|
+
this.error = `File exceeds maximum size of ${maxSizeMB} MB`;
|
|
312
273
|
return;
|
|
313
274
|
}
|
|
314
275
|
fileNameState.val = file.name;
|
|
315
276
|
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");
|
|
277
|
+
readingState.val = true;
|
|
278
|
+
// Store metadata
|
|
279
|
+
this.fileNameValue = file.name;
|
|
280
|
+
this.fileSizeValue = String(file.size);
|
|
281
|
+
this.fileTypeValue = file.type;
|
|
282
|
+
const reader = new FileReader();
|
|
283
|
+
reader.onload = () => {
|
|
284
|
+
readingState.val = false;
|
|
285
|
+
let result = reader.result;
|
|
286
|
+
if (readAs === "arrayBuffer" && reader.result instanceof ArrayBuffer) {
|
|
287
|
+
// Base64-encode the ArrayBuffer
|
|
288
|
+
const bytes = new Uint8Array(reader.result);
|
|
289
|
+
let binary = "";
|
|
290
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
291
|
+
binary += String.fromCharCode(bytes[i]);
|
|
358
292
|
}
|
|
293
|
+
result = btoa(binary);
|
|
359
294
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
}
|
|
295
|
+
this.handleChange(this, result);
|
|
296
|
+
};
|
|
297
|
+
reader.onerror = () => {
|
|
298
|
+
readingState.val = false;
|
|
299
|
+
this.error = "Error reading file";
|
|
300
|
+
};
|
|
301
|
+
if (readAs === "dataURL") {
|
|
302
|
+
reader.readAsDataURL(file);
|
|
303
|
+
}
|
|
304
|
+
else if (readAs === "arrayBuffer") {
|
|
305
|
+
reader.readAsArrayBuffer(file);
|
|
393
306
|
}
|
|
394
|
-
|
|
395
|
-
|
|
307
|
+
else {
|
|
308
|
+
reader.readAsText(file);
|
|
396
309
|
}
|
|
397
310
|
};
|
|
398
311
|
const clearFile = () => {
|
|
399
312
|
fileNameState.val = "";
|
|
400
313
|
fileSizeState.val = "";
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.arrayPathValue = "";
|
|
314
|
+
readingState.val = false;
|
|
315
|
+
this.fileNameValue = "";
|
|
316
|
+
this.fileSizeValue = "";
|
|
317
|
+
this.fileTypeValue = "";
|
|
406
318
|
this.error = "";
|
|
407
319
|
this.handleChange(this, "");
|
|
408
320
|
};
|
|
@@ -413,7 +325,7 @@ export class VanJsfField extends VanJSComponent {
|
|
|
413
325
|
onchange: (e) => {
|
|
414
326
|
const files = e.target.files;
|
|
415
327
|
if (files && files[0])
|
|
416
|
-
|
|
328
|
+
readFile(files[0]);
|
|
417
329
|
},
|
|
418
330
|
});
|
|
419
331
|
const dropZone = div({
|
|
@@ -428,102 +340,12 @@ export class VanJsfField extends VanJSComponent {
|
|
|
428
340
|
dragOverState.val = false;
|
|
429
341
|
const files = e.dataTransfer?.files;
|
|
430
342
|
if (files && files[0])
|
|
431
|
-
|
|
343
|
+
readFile(files[0]);
|
|
432
344
|
},
|
|
433
345
|
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
|
|
346
|
+
}, p({ style: "margin: 0; color: #666;" }, accept
|
|
347
|
+
? `Drop a file here or click to browse (${accept})`
|
|
348
|
+
: "Drop a file here or click to browse"));
|
|
527
349
|
const fileInfoBar = () => {
|
|
528
350
|
return div(() => {
|
|
529
351
|
const name = fileNameState.val;
|
|
@@ -535,22 +357,20 @@ export class VanJsfField extends VanJSComponent {
|
|
|
535
357
|
onclick: (e) => {
|
|
536
358
|
e.stopPropagation();
|
|
537
359
|
clearFile();
|
|
538
|
-
// Reset the file input so the same file can be re-selected
|
|
539
360
|
fileInput.value = "";
|
|
540
361
|
},
|
|
541
362
|
}, "Clear"));
|
|
542
363
|
});
|
|
543
364
|
};
|
|
544
|
-
|
|
545
|
-
const parsingIndicator = () => {
|
|
365
|
+
const readingIndicator = () => {
|
|
546
366
|
return div(() => {
|
|
547
|
-
if (!
|
|
367
|
+
if (!readingState.val)
|
|
548
368
|
return div();
|
|
549
|
-
return div({ style: "margin-top: 8px; color: #666;" }, "
|
|
369
|
+
return div({ style: "margin-top: 8px; color: #666;" }, "Reading file...");
|
|
550
370
|
});
|
|
551
371
|
};
|
|
552
372
|
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(),
|
|
373
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), readingIndicator(), p({ class: this.errorClass }, () => this.error));
|
|
554
374
|
break;
|
|
555
375
|
}
|
|
556
376
|
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);
|