@springmicro/forms 0.6.4 → 0.7.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.
Files changed (78) hide show
  1. package/.eslintrc.cjs +22 -22
  2. package/README.md +11 -11
  3. package/dist/index.d.ts +0 -0
  4. package/dist/index.js +0 -0
  5. package/dist/index.umd.cjs +0 -0
  6. package/package.json +3 -3
  7. package/src/builder/bottom-drawer.tsx +429 -429
  8. package/src/builder/form-builder.tsx +256 -256
  9. package/src/builder/modal.tsx +39 -39
  10. package/src/builder/nodes/node-base.tsx +94 -94
  11. package/src/builder/nodes/node-child-helpers.tsx +273 -273
  12. package/src/builder/nodes/node-parent.tsx +187 -187
  13. package/src/builder/nodes/node-types/array-node.tsx +134 -134
  14. package/src/builder/nodes/node-types/date-node.tsx +60 -60
  15. package/src/builder/nodes/node-types/file-node.tsx +67 -67
  16. package/src/builder/nodes/node-types/integer-node.tsx +60 -60
  17. package/src/builder/nodes/node-types/object-node.tsx +67 -67
  18. package/src/builder/nodes/node-types/text-node.tsx +66 -66
  19. package/src/fields/ArrayField.tsx +875 -875
  20. package/src/fields/BooleanField.tsx +110 -110
  21. package/src/fields/MultiSchemaField.tsx +236 -236
  22. package/src/fields/NullField.tsx +22 -22
  23. package/src/fields/NumberField.tsx +87 -87
  24. package/src/fields/ObjectField.tsx +338 -338
  25. package/src/fields/SchemaField.tsx +402 -402
  26. package/src/fields/StringField.tsx +67 -67
  27. package/src/fields/index.ts +24 -24
  28. package/src/index.tsx +26 -26
  29. package/src/interfaces/MessagesProps.interface.ts +5 -5
  30. package/src/interfaces/Option.interface.ts +4 -4
  31. package/src/styles/select.styles.ts +28 -28
  32. package/src/templates/ArrayFieldDescriptionTemplate.tsx +42 -42
  33. package/src/templates/ArrayFieldItemTemplate.tsx +78 -78
  34. package/src/templates/ArrayFieldTemplate.tsx +90 -90
  35. package/src/templates/ArrayFieldTitleTemplate.tsx +44 -44
  36. package/src/templates/BaseInputTemplate.tsx +94 -94
  37. package/src/templates/ButtonTemplates/AddButton.tsx +29 -29
  38. package/src/templates/ButtonTemplates/IconButton.tsx +49 -49
  39. package/src/templates/ButtonTemplates/SubmitButton.tsx +29 -29
  40. package/src/templates/ButtonTemplates/index.ts +16 -16
  41. package/src/templates/DescriptionField.tsx +29 -29
  42. package/src/templates/ErrorList.tsx +25 -25
  43. package/src/templates/FieldTemplate/FieldTemplate.tsx +39 -39
  44. package/src/templates/FieldTemplate/Label.tsx +29 -29
  45. package/src/templates/FieldTemplate/WrapIfAdditional.tsx +85 -85
  46. package/src/templates/FieldTemplate/index.ts +3 -3
  47. package/src/templates/ObjectFieldTemplate.tsx +79 -79
  48. package/src/templates/TitleField.tsx +20 -20
  49. package/src/templates/UnsupportedField.tsx +29 -29
  50. package/src/templates/index.ts +32 -32
  51. package/src/types/Message.type.ts +6 -6
  52. package/src/types/RawMessage.type.ts +15 -15
  53. package/src/types/form-builder.ts +135 -135
  54. package/src/types/utils.type.ts +1 -1
  55. package/src/utils/form-builder.ts +424 -424
  56. package/src/utils/processSelectValue.ts +50 -50
  57. package/src/widgets/AltDateTimeWidget.tsx +17 -17
  58. package/src/widgets/AltDateWidget.tsx +216 -216
  59. package/src/widgets/CheckboxWidget.tsx +80 -80
  60. package/src/widgets/CheckboxesWidget.tsx +74 -74
  61. package/src/widgets/ColorWidget.tsx +26 -26
  62. package/src/widgets/DateTimeWidget.tsx +28 -28
  63. package/src/widgets/DateWidget.tsx +36 -36
  64. package/src/widgets/EmailWidget.tsx +19 -19
  65. package/src/widgets/FileWidget.tsx +144 -144
  66. package/src/widgets/HiddenWidget.tsx +22 -22
  67. package/src/widgets/PasswordWidget.tsx +20 -20
  68. package/src/widgets/RadioWidget.tsx +87 -87
  69. package/src/widgets/RangeWidget.tsx +24 -24
  70. package/src/widgets/SelectWidget.tsx +99 -99
  71. package/src/widgets/TextWidget.tsx +19 -19
  72. package/src/widgets/TextareaWidget.tsx +64 -64
  73. package/src/widgets/URLWidget.tsx +19 -19
  74. package/src/widgets/UpDownWidget.tsx +20 -20
  75. package/src/widgets/index.ts +43 -43
  76. package/tsconfig.json +24 -24
  77. package/tsconfig.node.json +10 -10
  78. package/vite.config.ts +25 -25
@@ -1,429 +1,429 @@
1
- import {
2
- Box,
3
- Button,
4
- FormControl,
5
- FormControlLabel,
6
- FormLabel,
7
- IconButton,
8
- Radio,
9
- RadioGroup,
10
- TextField,
11
- } from "@mui/material";
12
- import React from "react";
13
- import { UseStateType } from "../types/utils.type";
14
- import Modal from "./modal";
15
- import {
16
- CountdownType,
17
- EditingStateType,
18
- FormNodeKeys,
19
- FormNodeType,
20
- FormType,
21
- } from "../types/form-builder";
22
- import AddCircleIcon from "@mui/icons-material/AddCircle";
23
- import SettingsIcon from "@mui/icons-material/Settings";
24
- import HomeIcon from "@mui/icons-material/Home";
25
- import ArrowBackIcon from "@mui/icons-material/ArrowBack";
26
- import {
27
- defaultNodes,
28
- generateNodeData,
29
- serializeFormToBuilder,
30
- } from "../utils/form-builder";
31
- import { RJSFSchema, UiSchema } from "@rjsf/utils";
32
- /**
33
- * ---------------------- BOTTOM DRAWER ----------------------
34
- */
35
-
36
- export type BottomDrawerProps = {
37
- nodesState: UseStateType<FormNodeType[]>;
38
- formState: UseStateType<FormType>;
39
- countdown: CountdownType;
40
- path: [number[], (num?: number) => void];
41
- setEditing: UseStateType<EditingStateType>[1];
42
- };
43
-
44
- export default function BottomDrawer({
45
- nodesState,
46
- countdown,
47
- formState,
48
- path: pathInfo,
49
- setEditing,
50
- }: BottomDrawerProps) {
51
- const [createModalOpen, setCreateModalOpen] = React.useState(false);
52
- const [configModalOpen, setConfigModalOpen] = React.useState(false);
53
-
54
- const [path, setPath] = pathInfo; // setPath is not from useState.
55
-
56
- function importFunction(form: RJSFSchema, ui: UiSchema) {
57
- const [, setNodes] = nodesState;
58
- const [, setForm] = formState;
59
- try {
60
- const { nodes, form: formData } = serializeFormToBuilder(form, ui);
61
- setNodes(nodes);
62
- setForm(formData);
63
- } catch {
64
- return "Could not parse data.";
65
- }
66
- }
67
-
68
- return (
69
- <>
70
- <Box
71
- sx={{
72
- width: "100%",
73
- height: 60,
74
- pt: 1,
75
- borderTop: "1px solid #777",
76
- flexShrink: 0,
77
- display: "flex",
78
- flexDirection: "row",
79
- justifyContent: "space-between",
80
- alignItems: "center",
81
- }}
82
- >
83
- <Box
84
- sx={{
85
- display: "flex",
86
- gap: 1,
87
- alignItems: "flex-end",
88
- height: "100%",
89
- }}
90
- >
91
- <DrawerIconButton
92
- icon={SettingsIcon}
93
- onClick={() => {
94
- setConfigModalOpen(true);
95
- }}
96
- />
97
- {path.length > 0 && (
98
- <>
99
- {/* PATH NAVIGATION */}
100
- <DrawerIconButton
101
- icon={HomeIcon}
102
- onClick={() => {
103
- setPath(-1);
104
- }}
105
- />
106
- <DrawerIconButton
107
- icon={ArrowBackIcon}
108
- onClick={() => {
109
- setPath();
110
- }}
111
- />
112
- </>
113
- )}
114
- </Box>
115
- <Box
116
- sx={{
117
- display: "flex",
118
- gap: 1,
119
- alignItems: "flex-end",
120
- height: "100%",
121
- }}
122
- >
123
- <p
124
- style={{
125
- color: countdown !== "Save failed." ? "#888" : "#d22",
126
- fontSize: 20,
127
- }}
128
- >
129
- <CountdownTimer countdown={countdown} />
130
- </p>
131
- <DrawerIconButton
132
- icon={AddCircleIcon}
133
- onClick={() => {
134
- setCreateModalOpen(true);
135
- }}
136
- />
137
- </Box>
138
- </Box>
139
- <CreateModal
140
- openState={[createModalOpen, setCreateModalOpen]}
141
- nodesState={nodesState}
142
- setEditing={setEditing}
143
- />
144
- <ConfigModal
145
- openState={[configModalOpen, setConfigModalOpen]}
146
- formState={formState}
147
- import={importFunction}
148
- />
149
- </>
150
- );
151
- }
152
-
153
- /**
154
- * ---------------------- COUNTDOWN ----------------------
155
- */
156
-
157
- const CountdownTimer = ({ countdown }: { countdown: CountdownType }) => {
158
- if (countdown === undefined) return <></>;
159
- if (typeof countdown === "string") return <>{countdown}</>;
160
- return <>Autosaving ({countdown})</>;
161
- };
162
-
163
- /**
164
- * ---------------------- ICON BUTTON ----------------------
165
- */
166
-
167
- type DrawerIconButtonProps = {
168
- onClick?: () => void;
169
- icon: React.ComponentType<any>;
170
- };
171
-
172
- function DrawerIconButton({
173
- // children,
174
- onClick,
175
- icon: Icon,
176
- }: DrawerIconButtonProps) {
177
- return (
178
- <Box
179
- sx={{
180
- height: "100%",
181
- aspectRatio: 1,
182
- backgroundColor: "#eaeaea",
183
- borderRadius: 1,
184
- cursor: "pointer",
185
- }}
186
- onClick={onClick}
187
- >
188
- <IconButton sx={{ height: "100%", aspectRatio: 1 }}>
189
- <Icon fontSize="large" sx={{ color: "#777" }} />
190
- </IconButton>
191
- </Box>
192
- );
193
- }
194
-
195
- /**
196
- * ---------------------- CONFIG MODAL ----------------------
197
- */
198
-
199
- type ConfigModalProps = {
200
- openState: UseStateType<boolean>;
201
- formState: UseStateType<FormType>;
202
- import: (form: RJSFSchema, ui: UiSchema) => void | string;
203
- };
204
-
205
- function ConfigModal({
206
- openState,
207
- formState,
208
- import: importFunction,
209
- }: ConfigModalProps) {
210
- const [open, setOpen] = openState;
211
- const [form, setForm] = formState;
212
-
213
- const [error, setError] = React.useState("");
214
-
215
- function closeModal() {
216
- setError("");
217
- setOpen(false);
218
- }
219
-
220
- function openImport(): void {
221
- const inputElement: HTMLInputElement = document.createElement("input");
222
- inputElement.type = "file";
223
- inputElement.accept = ".json";
224
-
225
- inputElement.onchange = (event: Event) => {
226
- const { files } = event.target as HTMLInputElement;
227
- if (!files) return;
228
- if (files.length === 0) {
229
- setError("No file selected.");
230
- return;
231
- }
232
- if (files.length !== 1) {
233
- setError("Please select only one JSON file.");
234
- return;
235
- }
236
- const file: File = files[0];
237
- if (file.type === "application/json") {
238
- const reader: FileReader = new FileReader();
239
- reader.onload = () => {
240
- try {
241
- const jsonData = JSON.parse(reader.result as string);
242
- if (!jsonData.form)
243
- throw `Invalid JSON.\nExample format: {\n\t"form": {...formData},\n\t"ui": {...uiData | undefined}\n}`;
244
- const imported = importFunction(jsonData.form, jsonData.ui ?? {});
245
- if (typeof imported === "string") throw imported;
246
-
247
- // Successful import!
248
- closeModal();
249
- } catch (err) {
250
- setError(err);
251
- }
252
- };
253
- reader.readAsText(file);
254
- } else {
255
- setError("Please select a valid JSON file.");
256
- }
257
- };
258
-
259
- inputElement.click();
260
- }
261
-
262
- return (
263
- <Modal open={open} onClose={() => setOpen(false)}>
264
- <h2>Edit Form Details:</h2>
265
- <TextField
266
- label="Title"
267
- required
268
- value={form.title}
269
- onChange={(e) => {
270
- if (!e.target.value) return;
271
- setForm((f) => ({ ...f, title: e.target.value }));
272
- }}
273
- />
274
- <TextField
275
- label="Description"
276
- value={form.description}
277
- onChange={(e) => {
278
- setForm((f) => ({ ...f, description: e.target.value }));
279
- }}
280
- />
281
- <Box
282
- sx={{
283
- display: "flex",
284
- flexDirection: "row",
285
- gap: 2,
286
- button: { flexGrow: 1 },
287
- }}
288
- >
289
- <Button variant="contained" onClick={openImport}>
290
- Import
291
- </Button>
292
- <Button variant="contained" onClick={closeModal}>
293
- Exit
294
- </Button>
295
- </Box>
296
- <p style={{ color: "red", whiteSpace: "pre-wrap" }}>{error}</p>
297
- </Modal>
298
- );
299
- }
300
-
301
- /**
302
- * ---------------------- OBJECT PRESET CHILDREN (CREATE MODAL) ----------------------
303
- */
304
-
305
- const presetChildren: Record<string, Array<FormNodeType> | undefined> = {
306
- // @ts-ignore
307
- empty: [] as FormNodeType[],
308
- image: [
309
- {
310
- ...generateNodeData("string"),
311
- format: "uri",
312
- propertyName: "src",
313
- title: undefined,
314
- },
315
- {
316
- ...generateNodeData("string"),
317
- propertyName: "alt",
318
- title: undefined,
319
- },
320
- {
321
- ...generateNodeData("string"),
322
- propertyName: "width",
323
- title: undefined,
324
- },
325
- {
326
- ...generateNodeData("string"),
327
- propertyName: "height",
328
- title: undefined,
329
- },
330
- ] as FormNodeType[],
331
- };
332
-
333
- /**
334
- * ---------------------- CREATE MODAL ----------------------
335
- */
336
-
337
- type CreateModalProps = {
338
- openState: UseStateType<boolean>;
339
- nodesState: UseStateType<FormNodeType[]>;
340
- setEditing: UseStateType<EditingStateType>[1];
341
- };
342
-
343
- function CreateModal({ openState, nodesState, setEditing }: CreateModalProps) {
344
- const [open, setOpen] = openState;
345
- const [nodes, setNodes] = nodesState;
346
- const [nodeType, setNodeType] = React.useState<FormNodeKeys>("string");
347
- const [presetType, setPresetType] = React.useState<"empty" | "image">(
348
- "empty"
349
- );
350
-
351
- return (
352
- <Modal open={open} onClose={() => setOpen(false)}>
353
- <h2>Create a new node:</h2>
354
- <FormControl>
355
- <FormLabel id="demo-radio-buttons-group-label">Field Type</FormLabel>
356
- <RadioGroup
357
- value={nodeType}
358
- onChange={(e) => {
359
- setNodeType(e.target.value as FormNodeKeys);
360
- }}
361
- >
362
- {Object.keys(defaultNodes).map((key) => {
363
- return (
364
- <FormControlLabel
365
- key={key}
366
- value={key}
367
- control={<Radio />}
368
- label={key.slice(0, 1).toUpperCase() + key.slice(1)}
369
- />
370
- );
371
- })}
372
- </RadioGroup>
373
- </FormControl>
374
- {nodeType === "object" && (
375
- // ======================= OBJECT PRESETS =======================
376
- <FormControl>
377
- <FormLabel id="demo-radio-buttons-group-label">Preset</FormLabel>
378
- <RadioGroup
379
- value={presetType}
380
- onChange={(e) => {
381
- setPresetType(e.target.value as any);
382
- }}
383
- >
384
- {Object.keys(presetChildren).map((key) => {
385
- return (
386
- <FormControlLabel
387
- key={key}
388
- value={key}
389
- control={<Radio />}
390
- label={key.slice(0, 1).toUpperCase() + key.slice(1)}
391
- />
392
- );
393
- })}
394
- </RadioGroup>
395
- </FormControl>
396
- )}
397
- <Button
398
- onClick={() => {
399
- setEditing(false);
400
- let nodeId: EditingStateType = false;
401
- setNodes((nodes) => {
402
- const newNode = generateNodeData(
403
- nodeType,
404
- nodeType === "object"
405
- ? // @ts-ignore
406
- (presetChildren[presetType] as FormNodeType[])
407
- : undefined
408
- ) as FormNodeType;
409
- nodeId = newNode.nodeId;
410
- setEditing(nodeId);
411
- return [...nodes, newNode];
412
- });
413
- setOpen(false);
414
- setTimeout(() => {
415
- const element = document.getElementById(
416
- "fb-scrollable"
417
- ) as HTMLElement;
418
- const newElement = document.getElementById(
419
- nodeId as string
420
- ) as HTMLElement;
421
- element.scrollTop = element.scrollHeight - newElement.offsetHeight;
422
- }, 0);
423
- }}
424
- >
425
- Create
426
- </Button>
427
- </Modal>
428
- );
429
- }
1
+ import {
2
+ Box,
3
+ Button,
4
+ FormControl,
5
+ FormControlLabel,
6
+ FormLabel,
7
+ IconButton,
8
+ Radio,
9
+ RadioGroup,
10
+ TextField,
11
+ } from "@mui/material";
12
+ import React from "react";
13
+ import { UseStateType } from "../types/utils.type";
14
+ import Modal from "./modal";
15
+ import {
16
+ CountdownType,
17
+ EditingStateType,
18
+ FormNodeKeys,
19
+ FormNodeType,
20
+ FormType,
21
+ } from "../types/form-builder";
22
+ import AddCircleIcon from "@mui/icons-material/AddCircle";
23
+ import SettingsIcon from "@mui/icons-material/Settings";
24
+ import HomeIcon from "@mui/icons-material/Home";
25
+ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
26
+ import {
27
+ defaultNodes,
28
+ generateNodeData,
29
+ serializeFormToBuilder,
30
+ } from "../utils/form-builder";
31
+ import { RJSFSchema, UiSchema } from "@rjsf/utils";
32
+ /**
33
+ * ---------------------- BOTTOM DRAWER ----------------------
34
+ */
35
+
36
+ export type BottomDrawerProps = {
37
+ nodesState: UseStateType<FormNodeType[]>;
38
+ formState: UseStateType<FormType>;
39
+ countdown: CountdownType;
40
+ path: [number[], (num?: number) => void];
41
+ setEditing: UseStateType<EditingStateType>[1];
42
+ };
43
+
44
+ export default function BottomDrawer({
45
+ nodesState,
46
+ countdown,
47
+ formState,
48
+ path: pathInfo,
49
+ setEditing,
50
+ }: BottomDrawerProps) {
51
+ const [createModalOpen, setCreateModalOpen] = React.useState(false);
52
+ const [configModalOpen, setConfigModalOpen] = React.useState(false);
53
+
54
+ const [path, setPath] = pathInfo; // setPath is not from useState.
55
+
56
+ function importFunction(form: RJSFSchema, ui: UiSchema) {
57
+ const [, setNodes] = nodesState;
58
+ const [, setForm] = formState;
59
+ try {
60
+ const { nodes, form: formData } = serializeFormToBuilder(form, ui);
61
+ setNodes(nodes);
62
+ setForm(formData);
63
+ } catch {
64
+ return "Could not parse data.";
65
+ }
66
+ }
67
+
68
+ return (
69
+ <>
70
+ <Box
71
+ sx={{
72
+ width: "100%",
73
+ height: 60,
74
+ pt: 1,
75
+ borderTop: "1px solid #777",
76
+ flexShrink: 0,
77
+ display: "flex",
78
+ flexDirection: "row",
79
+ justifyContent: "space-between",
80
+ alignItems: "center",
81
+ }}
82
+ >
83
+ <Box
84
+ sx={{
85
+ display: "flex",
86
+ gap: 1,
87
+ alignItems: "flex-end",
88
+ height: "100%",
89
+ }}
90
+ >
91
+ <DrawerIconButton
92
+ icon={SettingsIcon}
93
+ onClick={() => {
94
+ setConfigModalOpen(true);
95
+ }}
96
+ />
97
+ {path.length > 0 && (
98
+ <>
99
+ {/* PATH NAVIGATION */}
100
+ <DrawerIconButton
101
+ icon={HomeIcon}
102
+ onClick={() => {
103
+ setPath(-1);
104
+ }}
105
+ />
106
+ <DrawerIconButton
107
+ icon={ArrowBackIcon}
108
+ onClick={() => {
109
+ setPath();
110
+ }}
111
+ />
112
+ </>
113
+ )}
114
+ </Box>
115
+ <Box
116
+ sx={{
117
+ display: "flex",
118
+ gap: 1,
119
+ alignItems: "flex-end",
120
+ height: "100%",
121
+ }}
122
+ >
123
+ <p
124
+ style={{
125
+ color: countdown !== "Save failed." ? "#888" : "#d22",
126
+ fontSize: 20,
127
+ }}
128
+ >
129
+ <CountdownTimer countdown={countdown} />
130
+ </p>
131
+ <DrawerIconButton
132
+ icon={AddCircleIcon}
133
+ onClick={() => {
134
+ setCreateModalOpen(true);
135
+ }}
136
+ />
137
+ </Box>
138
+ </Box>
139
+ <CreateModal
140
+ openState={[createModalOpen, setCreateModalOpen]}
141
+ nodesState={nodesState}
142
+ setEditing={setEditing}
143
+ />
144
+ <ConfigModal
145
+ openState={[configModalOpen, setConfigModalOpen]}
146
+ formState={formState}
147
+ import={importFunction}
148
+ />
149
+ </>
150
+ );
151
+ }
152
+
153
+ /**
154
+ * ---------------------- COUNTDOWN ----------------------
155
+ */
156
+
157
+ const CountdownTimer = ({ countdown }: { countdown: CountdownType }) => {
158
+ if (countdown === undefined) return <></>;
159
+ if (typeof countdown === "string") return <>{countdown}</>;
160
+ return <>Autosaving ({countdown})</>;
161
+ };
162
+
163
+ /**
164
+ * ---------------------- ICON BUTTON ----------------------
165
+ */
166
+
167
+ type DrawerIconButtonProps = {
168
+ onClick?: () => void;
169
+ icon: React.ComponentType<any>;
170
+ };
171
+
172
+ function DrawerIconButton({
173
+ // children,
174
+ onClick,
175
+ icon: Icon,
176
+ }: DrawerIconButtonProps) {
177
+ return (
178
+ <Box
179
+ sx={{
180
+ height: "100%",
181
+ aspectRatio: 1,
182
+ backgroundColor: "#eaeaea",
183
+ borderRadius: 1,
184
+ cursor: "pointer",
185
+ }}
186
+ onClick={onClick}
187
+ >
188
+ <IconButton sx={{ height: "100%", aspectRatio: 1 }}>
189
+ <Icon fontSize="large" sx={{ color: "#777" }} />
190
+ </IconButton>
191
+ </Box>
192
+ );
193
+ }
194
+
195
+ /**
196
+ * ---------------------- CONFIG MODAL ----------------------
197
+ */
198
+
199
+ type ConfigModalProps = {
200
+ openState: UseStateType<boolean>;
201
+ formState: UseStateType<FormType>;
202
+ import: (form: RJSFSchema, ui: UiSchema) => void | string;
203
+ };
204
+
205
+ function ConfigModal({
206
+ openState,
207
+ formState,
208
+ import: importFunction,
209
+ }: ConfigModalProps) {
210
+ const [open, setOpen] = openState;
211
+ const [form, setForm] = formState;
212
+
213
+ const [error, setError] = React.useState("");
214
+
215
+ function closeModal() {
216
+ setError("");
217
+ setOpen(false);
218
+ }
219
+
220
+ function openImport(): void {
221
+ const inputElement: HTMLInputElement = document.createElement("input");
222
+ inputElement.type = "file";
223
+ inputElement.accept = ".json";
224
+
225
+ inputElement.onchange = (event: Event) => {
226
+ const { files } = event.target as HTMLInputElement;
227
+ if (!files) return;
228
+ if (files.length === 0) {
229
+ setError("No file selected.");
230
+ return;
231
+ }
232
+ if (files.length !== 1) {
233
+ setError("Please select only one JSON file.");
234
+ return;
235
+ }
236
+ const file: File = files[0];
237
+ if (file.type === "application/json") {
238
+ const reader: FileReader = new FileReader();
239
+ reader.onload = () => {
240
+ try {
241
+ const jsonData = JSON.parse(reader.result as string);
242
+ if (!jsonData.form)
243
+ throw `Invalid JSON.\nExample format: {\n\t"form": {...formData},\n\t"ui": {...uiData | undefined}\n}`;
244
+ const imported = importFunction(jsonData.form, jsonData.ui ?? {});
245
+ if (typeof imported === "string") throw imported;
246
+
247
+ // Successful import!
248
+ closeModal();
249
+ } catch (err) {
250
+ setError(err);
251
+ }
252
+ };
253
+ reader.readAsText(file);
254
+ } else {
255
+ setError("Please select a valid JSON file.");
256
+ }
257
+ };
258
+
259
+ inputElement.click();
260
+ }
261
+
262
+ return (
263
+ <Modal open={open} onClose={() => setOpen(false)}>
264
+ <h2>Edit Form Details:</h2>
265
+ <TextField
266
+ label="Title"
267
+ required
268
+ value={form.title}
269
+ onChange={(e) => {
270
+ if (!e.target.value) return;
271
+ setForm((f) => ({ ...f, title: e.target.value }));
272
+ }}
273
+ />
274
+ <TextField
275
+ label="Description"
276
+ value={form.description}
277
+ onChange={(e) => {
278
+ setForm((f) => ({ ...f, description: e.target.value }));
279
+ }}
280
+ />
281
+ <Box
282
+ sx={{
283
+ display: "flex",
284
+ flexDirection: "row",
285
+ gap: 2,
286
+ button: { flexGrow: 1 },
287
+ }}
288
+ >
289
+ <Button variant="contained" onClick={openImport}>
290
+ Import
291
+ </Button>
292
+ <Button variant="contained" onClick={closeModal}>
293
+ Exit
294
+ </Button>
295
+ </Box>
296
+ <p style={{ color: "red", whiteSpace: "pre-wrap" }}>{error}</p>
297
+ </Modal>
298
+ );
299
+ }
300
+
301
+ /**
302
+ * ---------------------- OBJECT PRESET CHILDREN (CREATE MODAL) ----------------------
303
+ */
304
+
305
+ const presetChildren: Record<string, Array<FormNodeType> | undefined> = {
306
+ // @ts-ignore
307
+ empty: [] as FormNodeType[],
308
+ image: [
309
+ {
310
+ ...generateNodeData("string"),
311
+ format: "uri",
312
+ propertyName: "src",
313
+ title: undefined,
314
+ },
315
+ {
316
+ ...generateNodeData("string"),
317
+ propertyName: "alt",
318
+ title: undefined,
319
+ },
320
+ {
321
+ ...generateNodeData("string"),
322
+ propertyName: "width",
323
+ title: undefined,
324
+ },
325
+ {
326
+ ...generateNodeData("string"),
327
+ propertyName: "height",
328
+ title: undefined,
329
+ },
330
+ ] as FormNodeType[],
331
+ };
332
+
333
+ /**
334
+ * ---------------------- CREATE MODAL ----------------------
335
+ */
336
+
337
+ type CreateModalProps = {
338
+ openState: UseStateType<boolean>;
339
+ nodesState: UseStateType<FormNodeType[]>;
340
+ setEditing: UseStateType<EditingStateType>[1];
341
+ };
342
+
343
+ function CreateModal({ openState, nodesState, setEditing }: CreateModalProps) {
344
+ const [open, setOpen] = openState;
345
+ const [nodes, setNodes] = nodesState;
346
+ const [nodeType, setNodeType] = React.useState<FormNodeKeys>("string");
347
+ const [presetType, setPresetType] = React.useState<"empty" | "image">(
348
+ "empty"
349
+ );
350
+
351
+ return (
352
+ <Modal open={open} onClose={() => setOpen(false)}>
353
+ <h2>Create a new node:</h2>
354
+ <FormControl>
355
+ <FormLabel id="demo-radio-buttons-group-label">Field Type</FormLabel>
356
+ <RadioGroup
357
+ value={nodeType}
358
+ onChange={(e) => {
359
+ setNodeType(e.target.value as FormNodeKeys);
360
+ }}
361
+ >
362
+ {Object.keys(defaultNodes).map((key) => {
363
+ return (
364
+ <FormControlLabel
365
+ key={key}
366
+ value={key}
367
+ control={<Radio />}
368
+ label={key.slice(0, 1).toUpperCase() + key.slice(1)}
369
+ />
370
+ );
371
+ })}
372
+ </RadioGroup>
373
+ </FormControl>
374
+ {nodeType === "object" && (
375
+ // ======================= OBJECT PRESETS =======================
376
+ <FormControl>
377
+ <FormLabel id="demo-radio-buttons-group-label">Preset</FormLabel>
378
+ <RadioGroup
379
+ value={presetType}
380
+ onChange={(e) => {
381
+ setPresetType(e.target.value as any);
382
+ }}
383
+ >
384
+ {Object.keys(presetChildren).map((key) => {
385
+ return (
386
+ <FormControlLabel
387
+ key={key}
388
+ value={key}
389
+ control={<Radio />}
390
+ label={key.slice(0, 1).toUpperCase() + key.slice(1)}
391
+ />
392
+ );
393
+ })}
394
+ </RadioGroup>
395
+ </FormControl>
396
+ )}
397
+ <Button
398
+ onClick={() => {
399
+ setEditing(false);
400
+ let nodeId: EditingStateType = false;
401
+ setNodes((nodes) => {
402
+ const newNode = generateNodeData(
403
+ nodeType,
404
+ nodeType === "object"
405
+ ? // @ts-ignore
406
+ (presetChildren[presetType] as FormNodeType[])
407
+ : undefined
408
+ ) as FormNodeType;
409
+ nodeId = newNode.nodeId;
410
+ setEditing(nodeId);
411
+ return [...nodes, newNode];
412
+ });
413
+ setOpen(false);
414
+ setTimeout(() => {
415
+ const element = document.getElementById(
416
+ "fb-scrollable"
417
+ ) as HTMLElement;
418
+ const newElement = document.getElementById(
419
+ nodeId as string
420
+ ) as HTMLElement;
421
+ element.scrollTop = element.scrollHeight - newElement.offsetHeight;
422
+ }, 0);
423
+ }}
424
+ >
425
+ Create
426
+ </Button>
427
+ </Modal>
428
+ );
429
+ }