vanjs-jsf 0.2.1 → 0.3.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.
- package/dist/VanJsfField.d.ts +7 -1
- package/dist/VanJsfField.js +73 -44
- package/dist/VanJsfForm.js +6 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +4 -4
- package/dist/theme.d.ts +26 -0
- package/dist/theme.js +2 -0
- package/package.json +1 -1
package/dist/VanJsfField.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { State } from "vanjs-core";
|
|
2
2
|
import { VanJSComponent } from "./VanJSComponent";
|
|
3
|
+
import { JsfTheme } from "./theme";
|
|
3
4
|
export interface Option {
|
|
4
5
|
label: string;
|
|
5
6
|
value: string;
|
|
@@ -14,15 +15,17 @@ export declare class VanJsfField extends VanJSComponent {
|
|
|
14
15
|
handleChange: (field: VanJsfField, value: MultiType) => void;
|
|
15
16
|
isVisibleState: State<boolean>;
|
|
16
17
|
errorState: State<string>;
|
|
18
|
+
theme: JsfTheme;
|
|
17
19
|
/** Used by file fields to pass file metadata to formValues */
|
|
18
20
|
fileNameValue: string;
|
|
19
21
|
fileSizeValue: string;
|
|
20
22
|
fileTypeValue: string;
|
|
21
|
-
constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void);
|
|
23
|
+
constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void, theme?: JsfTheme);
|
|
22
24
|
get inputType(): string;
|
|
23
25
|
get label(): string;
|
|
24
26
|
get class(): string;
|
|
25
27
|
get errorClass(): string;
|
|
28
|
+
get isRequired(): boolean;
|
|
26
29
|
get codemirrorExtension(): Array<any>;
|
|
27
30
|
get containerClass(): string;
|
|
28
31
|
get containerId(): string;
|
|
@@ -34,6 +37,9 @@ export declare class VanJsfField extends VanJSComponent {
|
|
|
34
37
|
set isVisible(val: boolean);
|
|
35
38
|
get error(): string;
|
|
36
39
|
set error(val: string);
|
|
40
|
+
private renderLabel;
|
|
41
|
+
private renderDescription;
|
|
42
|
+
private renderError;
|
|
37
43
|
render(): Element;
|
|
38
44
|
isVanJsfFieldArray(fields: unknown): fields is VanJsfField[];
|
|
39
45
|
}
|
package/dist/VanJsfField.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import van from "vanjs-core";
|
|
2
2
|
import { VanJSComponent } from "./VanJSComponent";
|
|
3
|
+
import { resolve } from "./theme";
|
|
3
4
|
import pikaday from "pikaday";
|
|
4
5
|
import { basicSetup, EditorView } from "codemirror";
|
|
5
6
|
import { javascript, esLint } from "@codemirror/lang-javascript";
|
|
@@ -42,16 +43,18 @@ export class VanJsfField extends VanJSComponent {
|
|
|
42
43
|
handleChange;
|
|
43
44
|
isVisibleState;
|
|
44
45
|
errorState;
|
|
46
|
+
theme;
|
|
45
47
|
/** Used by file fields to pass file metadata to formValues */
|
|
46
48
|
fileNameValue = "";
|
|
47
49
|
fileSizeValue = "";
|
|
48
50
|
fileTypeValue = "";
|
|
49
|
-
constructor(field, initVal, handleChange) {
|
|
51
|
+
constructor(field, initVal, handleChange, theme = {}) {
|
|
50
52
|
super();
|
|
51
53
|
this.field = field;
|
|
52
54
|
this.name = field.name;
|
|
53
55
|
this.iniVal = initVal;
|
|
54
56
|
this.handleChange = handleChange;
|
|
57
|
+
this.theme = theme;
|
|
55
58
|
this.isVisibleState = van.state(this.field.isVisible);
|
|
56
59
|
this.errorState = van.state("");
|
|
57
60
|
}
|
|
@@ -67,9 +70,12 @@ export class VanJsfField extends VanJSComponent {
|
|
|
67
70
|
get errorClass() {
|
|
68
71
|
return this.field.errorClass;
|
|
69
72
|
}
|
|
73
|
+
get isRequired() {
|
|
74
|
+
return this.field.required ?? false;
|
|
75
|
+
}
|
|
70
76
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
77
|
get codemirrorExtension() {
|
|
72
|
-
const
|
|
78
|
+
const cmTheme = EditorView.theme({
|
|
73
79
|
'.cm-content, .cm-gutter': {
|
|
74
80
|
"min-height": "150px",
|
|
75
81
|
},
|
|
@@ -86,7 +92,7 @@ export class VanJsfField extends VanJSComponent {
|
|
|
86
92
|
border: '1px solid silver',
|
|
87
93
|
},
|
|
88
94
|
});
|
|
89
|
-
const extensions = [
|
|
95
|
+
const extensions = [cmTheme, EditorView.updateListener.of((e) => {
|
|
90
96
|
this.field.error = null;
|
|
91
97
|
forEachDiagnostic(e.state, (diag) => {
|
|
92
98
|
if (diag.severity === "error") {
|
|
@@ -141,38 +147,52 @@ export class VanJsfField extends VanJSComponent {
|
|
|
141
147
|
set error(val) {
|
|
142
148
|
this.errorState.val = val;
|
|
143
149
|
}
|
|
150
|
+
renderLabel() {
|
|
151
|
+
const cls = resolve(this.titleClass, this.theme.label);
|
|
152
|
+
return label({ for: this.name, class: cls }, this.label, this.isRequired
|
|
153
|
+
? span({ class: this.theme.requiredIndicator || "" }, " *")
|
|
154
|
+
: null);
|
|
155
|
+
}
|
|
156
|
+
renderDescription() {
|
|
157
|
+
if (!this.description)
|
|
158
|
+
return null;
|
|
159
|
+
return div({
|
|
160
|
+
id: `${this.name}-description`,
|
|
161
|
+
class: resolve(this.descriptionClass, this.theme.description),
|
|
162
|
+
}, this.description);
|
|
163
|
+
}
|
|
164
|
+
renderError() {
|
|
165
|
+
return p({ class: resolve(this.errorClass, this.theme.error) }, () => this.error);
|
|
166
|
+
}
|
|
144
167
|
render() {
|
|
145
168
|
let el;
|
|
169
|
+
const baseContainer = resolve(this.containerClass, this.theme.container);
|
|
146
170
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
171
|
const props = {
|
|
148
|
-
|
|
149
|
-
class: this.containerClass || ''
|
|
172
|
+
class: () => this.isVisible ? baseContainer : `${baseContainer} jsf-hidden`.trim(),
|
|
150
173
|
};
|
|
151
174
|
switch (this.inputType) {
|
|
152
175
|
case FieldType.text:
|
|
153
|
-
el = div(props,
|
|
154
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
|
|
176
|
+
el = div(props, this.renderLabel(), this.renderDescription(), input({
|
|
155
177
|
id: this.name,
|
|
156
178
|
type: "text",
|
|
157
|
-
class: this.class
|
|
179
|
+
class: resolve(this.class, this.theme.input),
|
|
158
180
|
value: this.iniVal,
|
|
159
181
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
160
|
-
}),
|
|
182
|
+
}), this.renderError());
|
|
161
183
|
break;
|
|
162
184
|
case FieldType.textarea:
|
|
163
|
-
el = div(props,
|
|
164
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), textarea({
|
|
185
|
+
el = div(props, this.renderLabel(), this.renderDescription(), textarea({
|
|
165
186
|
id: this.name,
|
|
166
187
|
name: this.name,
|
|
167
|
-
class: this.class
|
|
188
|
+
class: resolve(this.class, this.theme.textarea),
|
|
168
189
|
rows: this.field.rows,
|
|
169
190
|
cols: this.field.columns,
|
|
170
191
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
171
|
-
}),
|
|
192
|
+
}), this.renderError());
|
|
172
193
|
break;
|
|
173
194
|
case FieldType.code:
|
|
174
|
-
el = div(props,
|
|
175
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description));
|
|
195
|
+
el = div(props, this.renderLabel(), this.renderDescription());
|
|
176
196
|
new EditorView({
|
|
177
197
|
doc: String(this.iniVal),
|
|
178
198
|
parent: el,
|
|
@@ -180,25 +200,23 @@ export class VanJsfField extends VanJSComponent {
|
|
|
180
200
|
});
|
|
181
201
|
break;
|
|
182
202
|
case FieldType.select:
|
|
183
|
-
el = div(props,
|
|
184
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), select({
|
|
203
|
+
el = div(props, this.renderLabel(), this.renderDescription(), select({
|
|
185
204
|
id: this.name,
|
|
186
205
|
name: this.name,
|
|
187
|
-
class: this.class
|
|
206
|
+
class: resolve(this.class, this.theme.select),
|
|
188
207
|
oninput: (e) => this.handleChange(this, e.target.value),
|
|
189
|
-
}, this.options?.map((opt) => option({ class: this.
|
|
208
|
+
}, this.options?.map((opt) => option({ class: this.theme.option || "", value: opt.value }, opt.label, opt.description))), this.renderError());
|
|
190
209
|
break;
|
|
191
210
|
case FieldType.date: {
|
|
192
211
|
const calendarInput = input({
|
|
193
212
|
id: this.name,
|
|
194
213
|
type: "text",
|
|
195
|
-
class: this.class
|
|
214
|
+
class: resolve(this.class, this.theme.input),
|
|
196
215
|
value: this.iniVal,
|
|
197
216
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
198
217
|
});
|
|
199
218
|
el =
|
|
200
|
-
div(props,
|
|
201
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), calendarInput, p({ class: this.errorClass }, () => this.error),
|
|
219
|
+
div(props, this.renderLabel(), this.renderDescription(), calendarInput, this.renderError(),
|
|
202
220
|
// External CDN dependency for Pikaday CSS — consider bundling for production
|
|
203
221
|
link({ rel: "stylesheet", type: "text/css", href: "https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css" }));
|
|
204
222
|
new pikaday({
|
|
@@ -223,36 +241,40 @@ export class VanJsfField extends VanJSComponent {
|
|
|
223
241
|
break;
|
|
224
242
|
}
|
|
225
243
|
case FieldType.number:
|
|
226
|
-
el = div(props,
|
|
227
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
|
|
244
|
+
el = div(props, this.renderLabel(), this.renderDescription(), input({
|
|
228
245
|
id: this.name,
|
|
229
246
|
type: "number",
|
|
230
|
-
class: this.class
|
|
247
|
+
class: resolve(this.class, this.theme.input),
|
|
231
248
|
value: this.iniVal,
|
|
232
249
|
oninput: (e) => {
|
|
233
250
|
const val = e.target.value;
|
|
234
251
|
this.handleChange(this, val === "" ? "" : Number(val));
|
|
235
252
|
},
|
|
236
|
-
}),
|
|
253
|
+
}), this.renderError());
|
|
237
254
|
break;
|
|
238
255
|
case FieldType.fieldset:
|
|
239
|
-
el = div(props, fieldset(legend({ class: this.titleClass ||
|
|
240
|
-
|
|
256
|
+
el = div(props, fieldset({ class: this.theme.fieldset || "" }, legend({ class: resolve(this.titleClass, this.theme.legend || this.theme.label) }, this.label), this.renderDescription(), this.isVanJsfFieldArray(this.field.fields)
|
|
257
|
+
? this.field.fields.map((field) => field.render())
|
|
258
|
+
: null));
|
|
241
259
|
break;
|
|
242
260
|
case FieldType.radio:
|
|
243
|
-
el = div(legend({ class: this.titleClass ||
|
|
261
|
+
el = div(props, legend({ class: resolve(this.titleClass, this.theme.legend || this.theme.label) }, this.label), this.renderDescription(), div({ class: this.theme.radioGroup || "" }, this.options?.map((opt) => label({ class: this.theme.radioLabel || "" }, input({
|
|
244
262
|
type: "radio",
|
|
245
263
|
name: this.name,
|
|
246
|
-
class: this.class
|
|
264
|
+
class: resolve(this.class, this.theme.radioInput),
|
|
247
265
|
value: opt.value,
|
|
248
266
|
checked: this.iniVal === opt.value,
|
|
249
267
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
250
|
-
}), opt.label, opt.description))),
|
|
268
|
+
}), opt.label, opt.description))), this.renderError());
|
|
251
269
|
break;
|
|
252
270
|
case FieldType.file: {
|
|
253
271
|
const accept = this.field.accept || "";
|
|
254
272
|
const maxSizeMB = this.field.maxSizeMB;
|
|
255
|
-
const readAs = this.field.readAs || "
|
|
273
|
+
const readAs = this.field.readAs || "auto";
|
|
274
|
+
const TEXT_EXTENSIONS = new Set([
|
|
275
|
+
"json", "csv", "tsv", "txt", "xml", "yaml", "yml",
|
|
276
|
+
"log", "md", "html", "css", "js", "ts", "sql", "env",
|
|
277
|
+
]);
|
|
256
278
|
// Reactive states
|
|
257
279
|
const fileNameState = van.state("");
|
|
258
280
|
const fileSizeState = van.state("");
|
|
@@ -283,8 +305,8 @@ export class VanJsfField extends VanJSComponent {
|
|
|
283
305
|
reader.onload = () => {
|
|
284
306
|
readingState.val = false;
|
|
285
307
|
let result = reader.result;
|
|
286
|
-
if (
|
|
287
|
-
//
|
|
308
|
+
if (reader.result instanceof ArrayBuffer) {
|
|
309
|
+
// Convert binary to base64 (applies to "arrayBuffer" and "auto" for binary files)
|
|
288
310
|
const bytes = new Uint8Array(reader.result);
|
|
289
311
|
let binary = "";
|
|
290
312
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
@@ -304,6 +326,15 @@ export class VanJsfField extends VanJSComponent {
|
|
|
304
326
|
else if (readAs === "arrayBuffer") {
|
|
305
327
|
reader.readAsArrayBuffer(file);
|
|
306
328
|
}
|
|
329
|
+
else if (readAs === "auto") {
|
|
330
|
+
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
|
331
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
332
|
+
reader.readAsText(file);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
reader.readAsArrayBuffer(file);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
307
338
|
else {
|
|
308
339
|
reader.readAsText(file);
|
|
309
340
|
}
|
|
@@ -328,11 +359,10 @@ export class VanJsfField extends VanJSComponent {
|
|
|
328
359
|
readFile(files[0]);
|
|
329
360
|
},
|
|
330
361
|
});
|
|
362
|
+
const dzBase = this.theme.dropZone || "jsf-dropzone";
|
|
363
|
+
const dzActive = this.theme.dropZoneActive || "jsf-dropzone-active";
|
|
331
364
|
const dropZone = div({
|
|
332
|
-
|
|
333
|
-
const over = dragOverState.val;
|
|
334
|
-
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"};`;
|
|
335
|
-
},
|
|
365
|
+
class: () => dragOverState.val ? `${dzBase} ${dzActive}` : dzBase,
|
|
336
366
|
ondragover: (e) => { e.preventDefault(); dragOverState.val = true; },
|
|
337
367
|
ondragleave: () => { dragOverState.val = false; },
|
|
338
368
|
ondrop: (e) => {
|
|
@@ -343,7 +373,7 @@ export class VanJsfField extends VanJSComponent {
|
|
|
343
373
|
readFile(files[0]);
|
|
344
374
|
},
|
|
345
375
|
onclick: () => fileInput.click(),
|
|
346
|
-
}, p({
|
|
376
|
+
}, p({ class: this.theme.dropZoneText || "jsf-dropzone-text" }, accept
|
|
347
377
|
? `Drop a file here or click to browse (${accept})`
|
|
348
378
|
: "Drop a file here or click to browse"));
|
|
349
379
|
const fileInfoBar = () => {
|
|
@@ -351,9 +381,9 @@ export class VanJsfField extends VanJSComponent {
|
|
|
351
381
|
const name = fileNameState.val;
|
|
352
382
|
if (!name)
|
|
353
383
|
return div();
|
|
354
|
-
return div({
|
|
384
|
+
return div({ class: this.theme.fileInfoBar || "jsf-file-info" }, strong({ class: this.theme.fileName || "" }, name), small({ class: this.theme.fileSize || "jsf-file-size" }, `(${fileSizeState.val})`), button({
|
|
355
385
|
type: "button",
|
|
356
|
-
|
|
386
|
+
class: this.theme.fileClearButton || "jsf-file-clear",
|
|
357
387
|
onclick: (e) => {
|
|
358
388
|
e.stopPropagation();
|
|
359
389
|
clearFile();
|
|
@@ -366,11 +396,10 @@ export class VanJsfField extends VanJSComponent {
|
|
|
366
396
|
return div(() => {
|
|
367
397
|
if (!readingState.val)
|
|
368
398
|
return div();
|
|
369
|
-
return div({
|
|
399
|
+
return div({ class: this.theme.fileReading || "jsf-file-reading" }, "Reading file...");
|
|
370
400
|
});
|
|
371
401
|
};
|
|
372
|
-
el = div(props,
|
|
373
|
-
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), readingIndicator(), p({ class: this.errorClass }, () => this.error));
|
|
402
|
+
el = div(props, this.renderLabel(), this.renderDescription(), fileInput, dropZone, fileInfoBar(), readingIndicator(), this.renderError());
|
|
374
403
|
break;
|
|
375
404
|
}
|
|
376
405
|
default:
|
package/dist/VanJsfForm.js
CHANGED
|
@@ -9,7 +9,8 @@ class VanJsfForm {
|
|
|
9
9
|
headlessForm;
|
|
10
10
|
formFields;
|
|
11
11
|
formValues;
|
|
12
|
-
|
|
12
|
+
theme;
|
|
13
|
+
constructor(jsonSchema, config, isValid, theme = {}) {
|
|
13
14
|
// Bind methods to instance. Needed to pass functions as props to child components
|
|
14
15
|
//this.handleSubmit = this.handleSubmit.bind(this);
|
|
15
16
|
this.handleFieldChange = this.handleFieldChange.bind(this);
|
|
@@ -17,6 +18,7 @@ class VanJsfForm {
|
|
|
17
18
|
this.schema = jsonSchema;
|
|
18
19
|
this.config = config;
|
|
19
20
|
this.isValid = isValid || undefined;
|
|
21
|
+
this.theme = theme;
|
|
20
22
|
// Working with parameters
|
|
21
23
|
const initialValues = { ...config?.initialValues };
|
|
22
24
|
this.headlessForm = createHeadlessForm(jsonSchema, config);
|
|
@@ -98,7 +100,7 @@ class VanJsfForm {
|
|
|
98
100
|
field.fields = this.processFields(field.fields, initialValues, formValues, fieldPath);
|
|
99
101
|
}
|
|
100
102
|
// Create and return a new VanJsfField instance for this field
|
|
101
|
-
return new VanJsfField(field, initVal, this.handleFieldChange);
|
|
103
|
+
return new VanJsfField(field, initVal, this.handleFieldChange, this.theme);
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
}
|
|
@@ -112,7 +114,8 @@ export function jsform(attributes, ...children) {
|
|
|
112
114
|
if (!config.formValues)
|
|
113
115
|
config.formValues = {};
|
|
114
116
|
const isValid = attributes.isValid;
|
|
115
|
-
const
|
|
117
|
+
const theme = attributes.theme ?? {};
|
|
118
|
+
const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid, theme);
|
|
116
119
|
const fields = vanJsfForm.formFields.map((field) => field.render());
|
|
117
120
|
const childrenWithFields = [...fields, ...children]; // Concatenate fields with other children
|
|
118
121
|
const originalOnSubmit = attributes.onsubmit;
|
package/dist/index.d.ts
CHANGED