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.
@@ -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 the selected arrayPath key to formValues */
18
- arrayPathValue: string;
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;
@@ -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, table, tr, th, td, strong, small } = van.tags;
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 the selected arrayPath key to formValues */
46
- arrayPathValue = "";
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 || ".json,.csv,.xlsx";
252
- const maxSizeMB = this.field.maxSizeMB || 50;
253
- const previewRows = this.field.previewRows || 5;
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 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
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
- setError(`File exceeds maximum size of ${maxSizeMB} MB`);
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
- 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");
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
- 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
- }
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
- catch (e) {
395
- setError(e instanceof Error ? e.message : "Error processing file");
307
+ else {
308
+ reader.readAsText(file);
396
309
  }
397
310
  };
398
311
  const clearFile = () => {
399
312
  fileNameState.val = "";
400
313
  fileSizeState.val = "";
401
- parsedDataState.val = null;
402
- parsingState.val = false;
403
- arrayPathOptionsState.val = [];
404
- selectedArrayPathState.val = "";
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
- processFile(files[0]);
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
- processFile(files[0]);
343
+ readFile(files[0]);
432
344
  },
433
345
  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
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
- // Parsing indicator
545
- const parsingIndicator = () => {
365
+ const readingIndicator = () => {
546
366
  return div(() => {
547
- if (!parsingState.val)
367
+ if (!readingState.val)
548
368
  return div();
549
- return div({ style: "margin-top: 8px; color: #666;" }, "Parsing file...");
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(), parsingIndicator(), arrayPathSelectorEl, previewTable(), p({ class: this.errorClass }, () => this.error));
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:
@@ -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 the selected arrayPath key
63
+ // For file fields, also store file metadata
64
64
  if (field.inputType === "file") {
65
- this.formValues[field.name + "__arrayPath"] = field.arrayPathValue;
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);