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.
@@ -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,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 || ".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 || "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 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
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
- setError(`File exceeds maximum size of ${maxSizeMB} MB`);
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
- 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");
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
- 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
- }
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
- setError(`Unsupported file extension: .${ext}`);
317
+ reader.readAsArrayBuffer(file);
392
318
  }
393
319
  }
394
- catch (e) {
395
- setError(e instanceof Error ? e.message : "Error processing file");
320
+ else {
321
+ reader.readAsText(file);
396
322
  }
397
323
  };
398
324
  const clearFile = () => {
399
325
  fileNameState.val = "";
400
326
  fileSizeState.val = "";
401
- parsedDataState.val = null;
402
- parsingState.val = false;
403
- arrayPathOptionsState.val = [];
404
- selectedArrayPathState.val = "";
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
- processFile(files[0]);
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
- processFile(files[0]);
356
+ readFile(files[0]);
432
357
  },
433
358
  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
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
- // Parsing indicator
545
- const parsingIndicator = () => {
378
+ const readingIndicator = () => {
546
379
  return div(() => {
547
- if (!parsingState.val)
380
+ if (!readingState.val)
548
381
  return div();
549
- return div({ style: "margin-top: 8px; color: #666;" }, "Parsing file...");
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(), parsingIndicator(), arrayPathSelectorEl, previewTable(), p({ class: this.errorClass }, () => this.error));
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:
@@ -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);