@dmitryvim/form-builder 0.1.4 → 0.1.5
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/README.md +34 -125
- package/dist/README.md +193 -0
- package/dist/example.html +108 -0
- package/dist/form-builder.js +972 -0
- package/dist/index.html +446 -171
- package/dist/sample.html +1615 -0
- package/docs/13_form_builder.html +663 -0
- package/docs/REQUIREMENTS.md +281 -0
- package/docs/integration.md +445 -0
- package/docs/schema.md +425 -0
- package/package.json +6 -3
package/dist/index.html
CHANGED
|
@@ -4,120 +4,104 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title>Form Builder - JSON Schema to Dynamic Forms</title>
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
darkMode: 'media',
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
fontFamily: {
|
|
14
|
+
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace']
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
7
20
|
<style>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono","Courier New", monospace;
|
|
12
|
-
--sans: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
|
21
|
+
/* Custom styles for form validation states */
|
|
22
|
+
.invalid {
|
|
23
|
+
@apply border-red-500 !important;
|
|
13
24
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
.field-hint {
|
|
26
|
+
@apply text-gray-500 text-xs mt-1;
|
|
27
|
+
}
|
|
28
|
+
.error-message {
|
|
29
|
+
@apply text-red-500 text-xs mt-1;
|
|
30
|
+
}
|
|
31
|
+
.file-preview-container {
|
|
32
|
+
@apply mb-3 p-3 border border-dashed border-gray-300 rounded-lg min-h-[60px] flex items-center justify-center bg-blue-50;
|
|
33
|
+
}
|
|
34
|
+
.dark .file-preview-container {
|
|
35
|
+
@apply bg-blue-900/20 border-gray-600;
|
|
19
36
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
@media (min-width: 1400px) { .grid { grid-template-columns: 1fr 1fr 1fr 1fr; } }
|
|
26
|
-
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
|
27
|
-
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; display: flex; flex-direction: column; min-height: 320px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
28
|
-
.card h2 { margin: 0 0 12px 0; font-size: 18px; color: var(--fg); font-weight: 600; }
|
|
29
|
-
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
|
30
|
-
.btn { background: var(--card); color: var(--fg); border: 1px solid var(--border); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; }
|
|
31
|
-
.btn:hover { border-color: var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
32
|
-
.btn.good { background: var(--good); color: white; border-color: var(--good); }
|
|
33
|
-
.btn.good:hover { filter: brightness(0.9); }
|
|
34
|
-
.btn.bad { background: var(--bad); color: white; border-color: var(--bad); }
|
|
35
|
-
.btn.bad:hover { filter: brightness(0.9); }
|
|
36
|
-
.btn.ghost { background: transparent; }
|
|
37
|
-
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
38
|
-
.textarea, .json, input[type="text"], textarea, select, input[type="number"], input[type="file"] {
|
|
39
|
-
width: 100%; border: 1px solid var(--border); background: var(--card); color: var(--fg); border-radius: 8px; padding: 10px; font-family: var(--mono); font-size: 13px; transition: border-color 0.2s;
|
|
37
|
+
.resource-pill {
|
|
38
|
+
@apply inline-flex items-center gap-1.5 bg-blue-50 border border-gray-300 rounded-full px-2.5 py-1 font-mono text-xs m-0.5;
|
|
39
|
+
}
|
|
40
|
+
.dark .resource-pill {
|
|
41
|
+
@apply bg-blue-900/20 border-gray-600;
|
|
40
42
|
}
|
|
41
|
-
.json, .textarea { flex: 1; resize: vertical; min-height: 220px; }
|
|
42
|
-
input[type="text"], input[type="number"], select { font-family: var(--sans); }
|
|
43
|
-
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
|
|
44
|
-
.hint { color: var(--muted); font-size: 12px; margin: 6px 0 0; }
|
|
45
|
-
.errors { color: var(--bad); font-size: 12px; white-space: pre-wrap; margin-top: 8px; background: rgba(239, 68, 68, 0.1); padding: 8px; border-radius: 6px; border-left: 3px solid var(--bad); }
|
|
46
|
-
.field { margin-bottom: 16px; }
|
|
47
|
-
.label { font-size: 14px; margin-bottom: 8px; color: var(--fg); display: flex; align-items: center; gap: 8px; font-weight: 500; }
|
|
48
|
-
.label .req { color: var(--bad); font-weight: 600; }
|
|
49
|
-
.invalid { border-color: var(--bad) !important; }
|
|
50
|
-
.msg { font-size: 12px; color: var(--bad); margin-top: 6px; }
|
|
51
|
-
.ok { color: var(--good); }
|
|
52
|
-
.muted { color: var(--muted); }
|
|
53
|
-
.pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(59, 130, 246, 0.1); border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; font-family: var(--mono); font-size: 12px; margin: 2px; }
|
|
54
|
-
.list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
55
|
-
.groupHeader { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
|
56
|
-
.groupItem { border: 1px dashed var(--border); border-radius: 10px; padding: 12px; margin-bottom: 12px; background: rgba(59, 130, 246, 0.02); }
|
|
57
|
-
.footer { margin-top: auto; font-size: 11px; color: var(--muted); padding-top: 12px; border-top: 1px solid var(--border); }
|
|
58
|
-
.url-params { background: rgba(59, 130, 246, 0.1); border: 1px solid var(--accent); border-radius: 8px; padding: 12px; margin: 16px; font-size: 13px; }
|
|
59
|
-
.url-params code { background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 4px; font-family: var(--mono); }
|
|
60
43
|
</style>
|
|
61
44
|
</head>
|
|
62
|
-
<body>
|
|
63
|
-
<h1>🚀 Form Builder</h1>
|
|
64
|
-
<div class="
|
|
45
|
+
<body class="min-h-screen bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-200">
|
|
46
|
+
<h1 class="text-center text-2xl font-semibold m-4">🚀 Form Builder</h1>
|
|
47
|
+
<div class="text-center text-slate-600 dark:text-slate-400 text-sm mx-4 -mt-2 mb-6">JSON Schema → Dynamic Forms → Structured Output</div>
|
|
65
48
|
|
|
66
|
-
<div class="
|
|
67
|
-
<strong>💡 URL Parameters:</strong> Add <code>?schema=BASE64_ENCODED_SCHEMA</code> to auto-load a schema.
|
|
49
|
+
<div class="hidden bg-blue-50 dark:bg-blue-900/20 border border-blue-300 dark:border-blue-600 rounded-lg p-3 mx-4 mb-4 text-sm" id="urlInfo">
|
|
50
|
+
<strong>💡 URL Parameters:</strong> Add <code class="bg-black/10 dark:bg-white/10 px-1 py-0.5 rounded font-mono">?schema=BASE64_ENCODED_SCHEMA</code> to auto-load a schema.
|
|
68
51
|
</div>
|
|
69
52
|
|
|
70
|
-
<div class="grid">
|
|
71
|
-
<section class="
|
|
72
|
-
<h2>1️⃣ JSON Schema</h2>
|
|
73
|
-
<div class="
|
|
74
|
-
<button class="
|
|
75
|
-
<button class="
|
|
76
|
-
<button class="
|
|
77
|
-
<button class="
|
|
53
|
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 p-3 auto-rows-[minmax(300px,auto)]">
|
|
54
|
+
<section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="schemaCard">
|
|
55
|
+
<h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">1️⃣ JSON Schema</h2>
|
|
56
|
+
<div class="flex flex-wrap gap-2 mb-3">
|
|
57
|
+
<button class="bg-emerald-600 hover:bg-emerald-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="applySchemaBtn">Apply Schema</button>
|
|
58
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="resetSchemaBtn">Reset to Example</button>
|
|
59
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="prettySchemaBtn">Format JSON</button>
|
|
60
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="downloadSchemaBtn">Download</button>
|
|
78
61
|
</div>
|
|
79
|
-
<textarea id="schemaInput" class="
|
|
80
|
-
<div id="schemaErrors" class="
|
|
81
|
-
<div class="
|
|
62
|
+
<textarea id="schemaInput" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" spellcheck="false" placeholder="Paste your JSON schema here..."></textarea>
|
|
63
|
+
<div id="schemaErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
|
|
64
|
+
<div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">
|
|
82
65
|
Schema defines form structure. Supports: text, textarea, number, select, file, files, and nested groups.
|
|
83
66
|
</div>
|
|
84
67
|
</section>
|
|
85
68
|
|
|
86
|
-
<section class="
|
|
87
|
-
<h2>2️⃣ Generated Form</h2>
|
|
88
|
-
<div id="formContainer"
|
|
89
|
-
<div
|
|
69
|
+
<section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="formCard">
|
|
70
|
+
<h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">2️⃣ Generated Form</h2>
|
|
71
|
+
<div id="formContainer" class="max-h-[400px] overflow-y-auto">
|
|
72
|
+
<div class="text-center text-slate-500 dark:text-slate-400 py-10">
|
|
90
73
|
Apply a schema to generate the form
|
|
91
74
|
</div>
|
|
92
75
|
</div>
|
|
93
|
-
<div class="
|
|
94
|
-
<button class="
|
|
95
|
-
<button class="
|
|
76
|
+
<div class="flex flex-wrap gap-2 mb-3">
|
|
77
|
+
<button class="bg-emerald-600 hover:bg-emerald-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="submitBtn">Submit Form</button>
|
|
78
|
+
<button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="saveDraftBtn">Save Draft</button>
|
|
79
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="clearFormBtn">Clear Values</button>
|
|
96
80
|
</div>
|
|
97
|
-
<div id="formErrors" class="
|
|
98
|
-
<div class="
|
|
81
|
+
<div id="formErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
|
|
82
|
+
<div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Interactive form generated from your schema. Files are simulated with resource IDs.</div>
|
|
99
83
|
</section>
|
|
100
84
|
|
|
101
|
-
<section class="
|
|
102
|
-
<h2>3️⃣ Form Output</h2>
|
|
103
|
-
<div class="
|
|
104
|
-
<button class="
|
|
105
|
-
<button class="
|
|
106
|
-
<button class="
|
|
85
|
+
<section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="outputCard">
|
|
86
|
+
<h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">3️⃣ Form Output</h2>
|
|
87
|
+
<div class="flex flex-wrap gap-2 mb-3">
|
|
88
|
+
<button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="copyOutputBtn">Copy JSON</button>
|
|
89
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="downloadOutputBtn">Download</button>
|
|
90
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="shareUrlBtn">Share URL</button>
|
|
107
91
|
</div>
|
|
108
|
-
<textarea id="outputJson" class="
|
|
109
|
-
<div class="
|
|
92
|
+
<textarea id="outputJson" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" readonly placeholder="Submit the form to see the output JSON here..."></textarea>
|
|
93
|
+
<div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Structured output matching your schema. Ready for API consumption or processing.</div>
|
|
110
94
|
</section>
|
|
111
95
|
|
|
112
|
-
<section class="
|
|
113
|
-
<h2>4️⃣ Prefill Data</h2>
|
|
114
|
-
<div class="
|
|
115
|
-
<button class="
|
|
116
|
-
<button class="
|
|
96
|
+
<section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="prefillCard">
|
|
97
|
+
<h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">4️⃣ Prefill Data</h2>
|
|
98
|
+
<div class="flex flex-wrap gap-2 mb-3">
|
|
99
|
+
<button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="loadPrefillBtn">Load Prefill</button>
|
|
100
|
+
<button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="copyTemplateBtn">Generate Template</button>
|
|
117
101
|
</div>
|
|
118
|
-
<textarea id="prefillInput" class="
|
|
119
|
-
<div id="prefillErrors" class="
|
|
120
|
-
<div class="
|
|
102
|
+
<textarea id="prefillInput" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" spellcheck="false" placeholder='{"field1": "value1", "field2": "value2", ...}'></textarea>
|
|
103
|
+
<div id="prefillErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
|
|
104
|
+
<div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Prefill form fields with existing data. Useful for editing or setting default values.</div>
|
|
121
105
|
</section>
|
|
122
106
|
</div>
|
|
123
107
|
|
|
@@ -150,6 +134,7 @@
|
|
|
150
134
|
formContainer: document.getElementById('formContainer'),
|
|
151
135
|
formErrors: document.getElementById('formErrors'),
|
|
152
136
|
submitBtn: document.getElementById('submitBtn'),
|
|
137
|
+
saveDraftBtn: document.getElementById('saveDraftBtn'),
|
|
153
138
|
clearFormBtn: document.getElementById('clearFormBtn'),
|
|
154
139
|
outputJson: document.getElementById('outputJson'),
|
|
155
140
|
copyOutputBtn: document.getElementById('copyOutputBtn'),
|
|
@@ -162,59 +147,102 @@
|
|
|
162
147
|
urlInfo: document.getElementById('urlInfo')
|
|
163
148
|
};
|
|
164
149
|
|
|
165
|
-
// Example schema for demonstration
|
|
150
|
+
// Example schema for demonstration (from docs/13_form_builder.html)
|
|
166
151
|
const EXAMPLE_SCHEMA = {
|
|
167
152
|
"version": "0.3",
|
|
168
|
-
"title": "
|
|
153
|
+
"title": "Asset Uploader with Slides",
|
|
169
154
|
"elements": [
|
|
170
155
|
{
|
|
171
|
-
"type": "
|
|
172
|
-
"key": "
|
|
173
|
-
"label": "
|
|
156
|
+
"type": "file",
|
|
157
|
+
"key": "cover",
|
|
158
|
+
"label": "Cover image",
|
|
174
159
|
"required": true,
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
160
|
+
"accept": {
|
|
161
|
+
"extensions": ["png", "jpg", "jpeg"],
|
|
162
|
+
"mime": ["image/png", "image/jpeg"]
|
|
163
|
+
},
|
|
164
|
+
"maxSizeMB": 25
|
|
178
165
|
},
|
|
179
166
|
{
|
|
180
|
-
"type": "
|
|
181
|
-
"key": "
|
|
182
|
-
"label": "
|
|
167
|
+
"type": "files",
|
|
168
|
+
"key": "assets",
|
|
169
|
+
"label": "Additional images",
|
|
183
170
|
"required": false,
|
|
184
|
-
"
|
|
185
|
-
|
|
171
|
+
"accept": {
|
|
172
|
+
"extensions": ["png", "jpg"],
|
|
173
|
+
"mime": ["image/png", "image/jpeg"]
|
|
174
|
+
},
|
|
175
|
+
"minCount": 0,
|
|
176
|
+
"maxCount": 10,
|
|
177
|
+
"maxSizeMB": 25
|
|
186
178
|
},
|
|
187
179
|
{
|
|
188
|
-
"type": "
|
|
189
|
-
"key": "
|
|
190
|
-
"label": "
|
|
180
|
+
"type": "text",
|
|
181
|
+
"key": "title",
|
|
182
|
+
"label": "Project title",
|
|
191
183
|
"required": true,
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
184
|
+
"minLength": 1,
|
|
185
|
+
"maxLength": 120,
|
|
186
|
+
"pattern": "^[A-Za-z0-9 _-]+$",
|
|
187
|
+
"default": "My Project"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"type": "textarea",
|
|
191
|
+
"key": "description",
|
|
192
|
+
"label": "Description",
|
|
193
|
+
"required": false,
|
|
194
|
+
"minLength": 0,
|
|
195
|
+
"maxLength": 2000,
|
|
196
|
+
"pattern": null,
|
|
197
|
+
"default": ""
|
|
195
198
|
},
|
|
196
199
|
{
|
|
197
200
|
"type": "select",
|
|
198
|
-
"key": "
|
|
199
|
-
"label": "
|
|
201
|
+
"key": "theme",
|
|
202
|
+
"label": "Theme",
|
|
200
203
|
"required": true,
|
|
201
204
|
"options": [
|
|
202
|
-
{"value": "
|
|
203
|
-
{"value": "
|
|
204
|
-
{"value": "moderator", "label": "Moderator"}
|
|
205
|
+
{"value": "light", "label": "Light"},
|
|
206
|
+
{"value": "dark", "label": "Dark"}
|
|
205
207
|
],
|
|
206
|
-
"default": "
|
|
208
|
+
"default": "dark"
|
|
207
209
|
},
|
|
208
210
|
{
|
|
209
|
-
"type": "
|
|
210
|
-
"key": "
|
|
211
|
-
"label": "
|
|
212
|
-
"required":
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
211
|
+
"type": "number",
|
|
212
|
+
"key": "opacity",
|
|
213
|
+
"label": "Opacity",
|
|
214
|
+
"required": true,
|
|
215
|
+
"min": 0,
|
|
216
|
+
"max": 1,
|
|
217
|
+
"decimals": 2,
|
|
218
|
+
"step": 0.01,
|
|
219
|
+
"default": 0.85
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"type": "group",
|
|
223
|
+
"key": "slides",
|
|
224
|
+
"label": "Slides",
|
|
225
|
+
"repeat": {"min": 1, "max": 5},
|
|
226
|
+
"elements": [
|
|
227
|
+
{
|
|
228
|
+
"type": "text",
|
|
229
|
+
"key": "title",
|
|
230
|
+
"label": "Slide title",
|
|
231
|
+
"required": true,
|
|
232
|
+
"minLength": 1,
|
|
233
|
+
"maxLength": 80,
|
|
234
|
+
"default": ""
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"type": "textarea",
|
|
238
|
+
"key": "body",
|
|
239
|
+
"label": "Slide text",
|
|
240
|
+
"required": true,
|
|
241
|
+
"minLength": 1,
|
|
242
|
+
"maxLength": 1000,
|
|
243
|
+
"default": ""
|
|
244
|
+
}
|
|
245
|
+
]
|
|
218
246
|
}
|
|
219
247
|
]
|
|
220
248
|
};
|
|
@@ -253,12 +281,12 @@
|
|
|
253
281
|
}
|
|
254
282
|
|
|
255
283
|
function showError(container, message) {
|
|
256
|
-
container.
|
|
284
|
+
container.classList.remove('hidden');
|
|
257
285
|
container.textContent = message;
|
|
258
286
|
}
|
|
259
287
|
|
|
260
288
|
function clearError(container) {
|
|
261
|
-
container.
|
|
289
|
+
container.classList.add('hidden');
|
|
262
290
|
container.textContent = '';
|
|
263
291
|
}
|
|
264
292
|
|
|
@@ -362,17 +390,17 @@
|
|
|
362
390
|
|
|
363
391
|
function renderElement(element, ctx) {
|
|
364
392
|
const wrapper = document.createElement('div');
|
|
365
|
-
wrapper.className = '
|
|
393
|
+
wrapper.className = 'mb-4';
|
|
366
394
|
|
|
367
395
|
const label = document.createElement('div');
|
|
368
|
-
label.className = '
|
|
396
|
+
label.className = 'text-sm mb-2 text-slate-800 dark:text-slate-200 flex items-center gap-2 font-medium';
|
|
369
397
|
const title = document.createElement('span');
|
|
370
398
|
title.textContent = element.label || element.key;
|
|
371
399
|
label.appendChild(title);
|
|
372
400
|
|
|
373
401
|
if (element.required) {
|
|
374
402
|
const req = document.createElement('span');
|
|
375
|
-
req.className = '
|
|
403
|
+
req.className = 'text-red-500 font-semibold';
|
|
376
404
|
req.textContent = '*';
|
|
377
405
|
label.appendChild(req);
|
|
378
406
|
}
|
|
@@ -386,6 +414,7 @@
|
|
|
386
414
|
input.type = 'text';
|
|
387
415
|
input.name = pathKey;
|
|
388
416
|
input.dataset.type = 'text';
|
|
417
|
+
input.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
|
|
389
418
|
setTextValueFromPrefill(input, element, ctx.prefill, element.key);
|
|
390
419
|
input.addEventListener('input', () => markValidity(input, null));
|
|
391
420
|
wrapper.appendChild(input);
|
|
@@ -397,6 +426,7 @@
|
|
|
397
426
|
ta.name = pathKey;
|
|
398
427
|
ta.rows = 4;
|
|
399
428
|
ta.dataset.type = 'textarea';
|
|
429
|
+
ta.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500 resize-y';
|
|
400
430
|
setTextValueFromPrefill(ta, element, ctx.prefill, element.key);
|
|
401
431
|
ta.addEventListener('input', () => markValidity(ta, null));
|
|
402
432
|
wrapper.appendChild(ta);
|
|
@@ -408,6 +438,7 @@
|
|
|
408
438
|
input.type = 'number';
|
|
409
439
|
input.name = pathKey;
|
|
410
440
|
input.dataset.type = 'number';
|
|
441
|
+
input.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
|
|
411
442
|
if (element.step != null) input.step = String(element.step);
|
|
412
443
|
if (element.min != null) input.min = String(element.min);
|
|
413
444
|
if (element.max != null) input.max = String(element.max);
|
|
@@ -428,6 +459,7 @@
|
|
|
428
459
|
const sel = document.createElement('select');
|
|
429
460
|
sel.name = pathKey;
|
|
430
461
|
sel.dataset.type = 'select';
|
|
462
|
+
sel.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
|
|
431
463
|
|
|
432
464
|
if (!element.required) {
|
|
433
465
|
const opt = document.createElement('option');
|
|
@@ -517,7 +549,7 @@
|
|
|
517
549
|
container.appendChild(hid);
|
|
518
550
|
|
|
519
551
|
wrapper.appendChild(container);
|
|
520
|
-
wrapper.appendChild(makeFieldHint(element, '
|
|
552
|
+
wrapper.appendChild(makeFieldHint(element, 'Returns resource ID for download/submission'));
|
|
521
553
|
break;
|
|
522
554
|
}
|
|
523
555
|
case 'files': {
|
|
@@ -581,7 +613,7 @@
|
|
|
581
613
|
wrapper.appendChild(picker);
|
|
582
614
|
wrapper.appendChild(list);
|
|
583
615
|
wrapper.appendChild(hid);
|
|
584
|
-
wrapper.appendChild(makeFieldHint(element, 'Multiple files
|
|
616
|
+
wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
|
|
585
617
|
break;
|
|
586
618
|
}
|
|
587
619
|
case 'group': {
|
|
@@ -590,7 +622,7 @@
|
|
|
590
622
|
|
|
591
623
|
const groupWrap = document.createElement('div');
|
|
592
624
|
const header = document.createElement('div');
|
|
593
|
-
header.className = '
|
|
625
|
+
header.className = 'flex items-center justify-between my-2 pb-2 border-b border-slate-200 dark:border-slate-700';
|
|
594
626
|
|
|
595
627
|
const left = document.createElement('div');
|
|
596
628
|
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
@@ -609,7 +641,7 @@
|
|
|
609
641
|
|
|
610
642
|
const addBtn = document.createElement('button');
|
|
611
643
|
addBtn.type = 'button';
|
|
612
|
-
addBtn.className = '
|
|
644
|
+
addBtn.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors';
|
|
613
645
|
addBtn.textContent = 'Add';
|
|
614
646
|
right.appendChild(addBtn);
|
|
615
647
|
header.appendChild(right);
|
|
@@ -618,7 +650,7 @@
|
|
|
618
650
|
const refreshControls = () => {
|
|
619
651
|
const n = countItems();
|
|
620
652
|
addBtn.disabled = n >= max;
|
|
621
|
-
left.innerHTML = `<span>${element.label || element.key}</span> <span class="
|
|
653
|
+
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-slate-500 dark:text-slate-400 text-xs">[${n} / ${max === Infinity ? '∞' : max}, min=${min}]</span>`;
|
|
622
654
|
};
|
|
623
655
|
|
|
624
656
|
const addItem = (prefillObj) => {
|
|
@@ -679,7 +711,7 @@
|
|
|
679
711
|
|
|
680
712
|
function makeFieldHint(element, extra = '') {
|
|
681
713
|
const hint = document.createElement('div');
|
|
682
|
-
hint.className = '
|
|
714
|
+
hint.className = 'text-slate-500 dark:text-slate-400 text-xs mt-1';
|
|
683
715
|
const bits = [];
|
|
684
716
|
|
|
685
717
|
if (element.required) bits.push('required');
|
|
@@ -805,15 +837,17 @@
|
|
|
805
837
|
|
|
806
838
|
function renderResourcePills(container, rids, onRemove) {
|
|
807
839
|
clear(container);
|
|
840
|
+
container.className = 'flex flex-wrap gap-1.5 mt-2';
|
|
841
|
+
|
|
808
842
|
rids.forEach(rid => {
|
|
809
843
|
const meta = state.resourceIndex.get(rid);
|
|
810
844
|
const pill = document.createElement('span');
|
|
811
|
-
pill.className = 'pill';
|
|
845
|
+
pill.className = 'resource-pill';
|
|
812
846
|
pill.textContent = rid;
|
|
813
847
|
|
|
814
848
|
if (meta) {
|
|
815
849
|
const small = document.createElement('span');
|
|
816
|
-
small.className = '
|
|
850
|
+
small.className = 'text-slate-500 dark:text-slate-400';
|
|
817
851
|
small.textContent = ` (${meta.name ?? 'file'}, ${formatFileSize(meta.size ?? 0)})`;
|
|
818
852
|
pill.appendChild(small);
|
|
819
853
|
}
|
|
@@ -821,10 +855,8 @@
|
|
|
821
855
|
if (onRemove) {
|
|
822
856
|
const x = document.createElement('button');
|
|
823
857
|
x.type = 'button';
|
|
824
|
-
x.className = '
|
|
858
|
+
x.className = 'bg-red-500 hover:bg-red-600 text-white text-xs px-1.5 py-0.5 rounded ml-1.5';
|
|
825
859
|
x.textContent = '×';
|
|
826
|
-
x.style.padding = '2px 6px';
|
|
827
|
-
x.style.marginLeft = '6px';
|
|
828
860
|
x.addEventListener('click', () => onRemove(rid));
|
|
829
861
|
pill.appendChild(x);
|
|
830
862
|
}
|
|
@@ -842,14 +874,14 @@
|
|
|
842
874
|
}
|
|
843
875
|
|
|
844
876
|
function markValidity(input, msg) {
|
|
845
|
-
const prev = input?.parentElement?.querySelector?.('.
|
|
877
|
+
const prev = input?.parentElement?.querySelector?.('.error-message');
|
|
846
878
|
if (prev) prev.remove();
|
|
847
879
|
|
|
848
880
|
if (input) input.classList.toggle('invalid', !!msg);
|
|
849
881
|
|
|
850
882
|
if (msg && input?.parentElement) {
|
|
851
883
|
const m = document.createElement('div');
|
|
852
|
-
m.className = '
|
|
884
|
+
m.className = 'error-message text-red-500 text-xs mt-1';
|
|
853
885
|
m.textContent = msg;
|
|
854
886
|
input.parentElement.appendChild(m);
|
|
855
887
|
}
|
|
@@ -905,7 +937,7 @@
|
|
|
905
937
|
}
|
|
906
938
|
|
|
907
939
|
// Form data collection and validation
|
|
908
|
-
function collectAndValidate(schema) {
|
|
940
|
+
function collectAndValidate(schema, skipValidation = false) {
|
|
909
941
|
const form = document.getElementById('dynamicForm');
|
|
910
942
|
const errors = [];
|
|
911
943
|
|
|
@@ -917,10 +949,10 @@
|
|
|
917
949
|
case 'textarea': {
|
|
918
950
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
919
951
|
const val = (input?.value ?? '').trim();
|
|
920
|
-
if (element.required && val === '') {
|
|
952
|
+
if (!skipValidation && element.required && val === '') {
|
|
921
953
|
errors.push(`${key}: required`);
|
|
922
954
|
markValidity(input, 'required');
|
|
923
|
-
} else if (val !== '') {
|
|
955
|
+
} else if (!skipValidation && val !== '') {
|
|
924
956
|
if (element.minLength != null && val.length < element.minLength) {
|
|
925
957
|
errors.push(`${key}: minLength=${element.minLength}`);
|
|
926
958
|
markValidity(input, `minLength=${element.minLength}`);
|
|
@@ -941,6 +973,8 @@
|
|
|
941
973
|
markValidity(input, 'invalid pattern');
|
|
942
974
|
}
|
|
943
975
|
}
|
|
976
|
+
} else if (skipValidation) {
|
|
977
|
+
markValidity(input, null);
|
|
944
978
|
} else {
|
|
945
979
|
markValidity(input, null);
|
|
946
980
|
}
|
|
@@ -949,7 +983,7 @@
|
|
|
949
983
|
case 'number': {
|
|
950
984
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
951
985
|
const raw = input?.value ?? '';
|
|
952
|
-
if (element.required && raw === '') {
|
|
986
|
+
if (!skipValidation && element.required && raw === '') {
|
|
953
987
|
errors.push(`${key}: required`);
|
|
954
988
|
markValidity(input, 'required');
|
|
955
989
|
return null;
|
|
@@ -959,16 +993,16 @@
|
|
|
959
993
|
return null;
|
|
960
994
|
}
|
|
961
995
|
const v = parseFloat(raw);
|
|
962
|
-
if (!Number.isFinite(v)) {
|
|
996
|
+
if (!skipValidation && !Number.isFinite(v)) {
|
|
963
997
|
errors.push(`${key}: not a number`);
|
|
964
998
|
markValidity(input, 'not a number');
|
|
965
999
|
return null;
|
|
966
1000
|
}
|
|
967
|
-
if (element.min != null && v < element.min) {
|
|
1001
|
+
if (!skipValidation && element.min != null && v < element.min) {
|
|
968
1002
|
errors.push(`${key}: < min=${element.min}`);
|
|
969
1003
|
markValidity(input, `< min=${element.min}`);
|
|
970
1004
|
}
|
|
971
|
-
if (element.max != null && v > element.max) {
|
|
1005
|
+
if (!skipValidation && element.max != null && v > element.max) {
|
|
972
1006
|
errors.push(`${key}: > max=${element.max}`);
|
|
973
1007
|
markValidity(input, `> max=${element.max}`);
|
|
974
1008
|
}
|
|
@@ -982,12 +1016,12 @@
|
|
|
982
1016
|
const sel = scopeRoot.querySelector(`select[name$="${key}"]`);
|
|
983
1017
|
const val = sel?.value ?? '';
|
|
984
1018
|
const values = new Set(element.options.map(o => String(o.value)));
|
|
985
|
-
if (element.required && val === '') {
|
|
1019
|
+
if (!skipValidation && element.required && val === '') {
|
|
986
1020
|
errors.push(`${key}: required`);
|
|
987
1021
|
markValidity(sel, 'required');
|
|
988
1022
|
return '';
|
|
989
1023
|
}
|
|
990
|
-
if (val !== '' && !values.has(String(val))) {
|
|
1024
|
+
if (!skipValidation && val !== '' && !values.has(String(val))) {
|
|
991
1025
|
errors.push(`${key}: value not in options`);
|
|
992
1026
|
markValidity(sel, 'not in options');
|
|
993
1027
|
} else {
|
|
@@ -998,7 +1032,7 @@
|
|
|
998
1032
|
case 'file': {
|
|
999
1033
|
const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
|
|
1000
1034
|
const rid = hid?.value ?? '';
|
|
1001
|
-
if (element.required && !rid) {
|
|
1035
|
+
if (!skipValidation && element.required && !rid) {
|
|
1002
1036
|
errors.push(`${key}: required (file missing)`);
|
|
1003
1037
|
const picker = hid?.previousElementSibling;
|
|
1004
1038
|
if (picker) markValidity(picker, 'required');
|
|
@@ -1011,11 +1045,11 @@
|
|
|
1011
1045
|
const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
|
|
1012
1046
|
const arr = parseJSONSafe(hid?.value ?? '[]', []);
|
|
1013
1047
|
const count = Array.isArray(arr) ? arr.length : 0;
|
|
1014
|
-
if (!Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
|
|
1015
|
-
if (element.minCount != null && count < element.minCount) {
|
|
1048
|
+
if (!skipValidation && !Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
|
|
1049
|
+
if (!skipValidation && element.minCount != null && count < element.minCount) {
|
|
1016
1050
|
errors.push(`${key}: < minCount=${element.minCount}`);
|
|
1017
1051
|
}
|
|
1018
|
-
if (element.maxCount != null && count > element.maxCount) {
|
|
1052
|
+
if (!skipValidation && element.maxCount != null && count > element.maxCount) {
|
|
1019
1053
|
errors.push(`${key}: > maxCount=${element.maxCount}`);
|
|
1020
1054
|
}
|
|
1021
1055
|
if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
|
|
@@ -1039,8 +1073,8 @@
|
|
|
1039
1073
|
const n = items.length;
|
|
1040
1074
|
const min = element.repeat.min ?? 0;
|
|
1041
1075
|
const max = element.repeat.max ?? Infinity;
|
|
1042
|
-
if (n < min) errors.push(`${key}: count < min=${min}`);
|
|
1043
|
-
if (n > max) errors.push(`${key}: count > max=${max}`);
|
|
1076
|
+
if (!skipValidation && n < min) errors.push(`${key}: count < min=${min}`);
|
|
1077
|
+
if (!skipValidation && n > max) errors.push(`${key}: count > max=${max}`);
|
|
1044
1078
|
items.forEach(item => {
|
|
1045
1079
|
const obj = {};
|
|
1046
1080
|
element.elements.forEach(child => {
|
|
@@ -1077,7 +1111,7 @@
|
|
|
1077
1111
|
const schemaParam = params.get('schema');
|
|
1078
1112
|
|
|
1079
1113
|
if (schemaParam) {
|
|
1080
|
-
el.urlInfo.
|
|
1114
|
+
el.urlInfo.classList.remove('hidden');
|
|
1081
1115
|
try {
|
|
1082
1116
|
const schemaJson = atob(schemaParam);
|
|
1083
1117
|
const schema = JSON.parse(schemaJson);
|
|
@@ -1132,19 +1166,7 @@
|
|
|
1132
1166
|
downloadFile('schema.json', el.schemaInput.value || pretty(EXAMPLE_SCHEMA));
|
|
1133
1167
|
});
|
|
1134
1168
|
|
|
1135
|
-
|
|
1136
|
-
clearError(el.formErrors);
|
|
1137
|
-
if (!state.schema) {
|
|
1138
|
-
showError(el.formErrors, 'Schema not applied');
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
const { result, errors } = collectAndValidate(state.schema);
|
|
1142
|
-
if (errors.length) {
|
|
1143
|
-
showError(el.formErrors, errors.join('\n'));
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
el.outputJson.value = pretty(result);
|
|
1147
|
-
});
|
|
1169
|
+
// Submit handler is now defined as submitFormEnhanced below
|
|
1148
1170
|
|
|
1149
1171
|
el.clearFormBtn.addEventListener('click', () => {
|
|
1150
1172
|
if (!state.schema) return;
|
|
@@ -1261,12 +1283,265 @@
|
|
|
1261
1283
|
Object.assign(state.config, config);
|
|
1262
1284
|
}
|
|
1263
1285
|
};
|
|
1286
|
+
|
|
1287
|
+
// Geppetto Integration - Message Handler
|
|
1288
|
+
function setupGeppettoMessaging() {
|
|
1289
|
+
// Listen for messages from parent window (Geppetto SPA)
|
|
1290
|
+
window.addEventListener('message', (event) => {
|
|
1291
|
+
// Security check - adjust origins as needed for your deployment
|
|
1292
|
+
const allowedOrigins = [
|
|
1293
|
+
'https://your-geppetto-domain.com',
|
|
1294
|
+
'http://localhost:3000',
|
|
1295
|
+
'http://localhost:8080',
|
|
1296
|
+
window.location.origin
|
|
1297
|
+
];
|
|
1298
|
+
|
|
1299
|
+
if (!allowedOrigins.some(origin => event.origin.startsWith(origin))) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
handleGeppettoMessage(event.data);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Notify parent that form builder is ready
|
|
1307
|
+
if (window.parent !== window) {
|
|
1308
|
+
window.parent.postMessage({
|
|
1309
|
+
type: 'formBuilderReady'
|
|
1310
|
+
}, '*');
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function handleGeppettoMessage(data) {
|
|
1315
|
+
switch (data.type) {
|
|
1316
|
+
case 'configure':
|
|
1317
|
+
if (data.config) {
|
|
1318
|
+
if (data.config.uploadHandler) {
|
|
1319
|
+
state.config.uploadFile = data.config.uploadHandler;
|
|
1320
|
+
}
|
|
1321
|
+
if (data.config.downloadHandler) {
|
|
1322
|
+
state.config.downloadFile = data.config.downloadHandler;
|
|
1323
|
+
}
|
|
1324
|
+
if (data.config.thumbnailHandler) {
|
|
1325
|
+
state.config.getThumbnail = data.config.thumbnailHandler;
|
|
1326
|
+
}
|
|
1327
|
+
Object.assign(state.config, data.config);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (data.options) {
|
|
1331
|
+
if (data.options.readonly) {
|
|
1332
|
+
enableReadOnlyMode();
|
|
1333
|
+
}
|
|
1334
|
+
if (data.options.theme) {
|
|
1335
|
+
setTheme(data.options.theme);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
break;
|
|
1339
|
+
|
|
1340
|
+
case 'setSchema':
|
|
1341
|
+
if (data.schema) {
|
|
1342
|
+
el.schemaInput.value = pretty(data.schema);
|
|
1343
|
+
applySchema();
|
|
1344
|
+
}
|
|
1345
|
+
break;
|
|
1346
|
+
|
|
1347
|
+
case 'setData':
|
|
1348
|
+
if (data.data && state.formRoot) {
|
|
1349
|
+
loadFormData(data.data);
|
|
1350
|
+
}
|
|
1351
|
+
break;
|
|
1352
|
+
|
|
1353
|
+
case 'getData':
|
|
1354
|
+
const currentData = getFormData();
|
|
1355
|
+
window.parent.postMessage({
|
|
1356
|
+
type: 'currentData',
|
|
1357
|
+
data: currentData
|
|
1358
|
+
}, '*');
|
|
1359
|
+
break;
|
|
1360
|
+
|
|
1361
|
+
case 'validate':
|
|
1362
|
+
const isValid = validateForm();
|
|
1363
|
+
window.parent.postMessage({
|
|
1364
|
+
type: 'validationResult',
|
|
1365
|
+
isValid: isValid
|
|
1366
|
+
}, '*');
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function enableReadOnlyMode() {
|
|
1372
|
+
state.config.readonly = true;
|
|
1373
|
+
|
|
1374
|
+
// Hide editing controls
|
|
1375
|
+
const editingElements = [
|
|
1376
|
+
el.applySchemaBtn, el.resetSchemaBtn, el.prettySchemaBtn,
|
|
1377
|
+
el.downloadSchemaBtn, el.submitBtn, el.clearFormBtn,
|
|
1378
|
+
el.loadPrefillBtn, el.copyTemplateBtn
|
|
1379
|
+
];
|
|
1380
|
+
|
|
1381
|
+
editingElements.forEach(element => {
|
|
1382
|
+
if (element) element.style.display = 'none';
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// Make schema input readonly
|
|
1386
|
+
if (el.schemaInput) {
|
|
1387
|
+
el.schemaInput.readOnly = true;
|
|
1388
|
+
el.schemaInput.style.backgroundColor = 'var(--muted)';
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Disable form inputs
|
|
1392
|
+
if (state.formRoot) {
|
|
1393
|
+
const inputs = state.formRoot.querySelectorAll('input, textarea, select, button');
|
|
1394
|
+
inputs.forEach(input => {
|
|
1395
|
+
input.disabled = true;
|
|
1396
|
+
input.style.opacity = '0.6';
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function setTheme(theme) {
|
|
1402
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
1403
|
+
|
|
1404
|
+
if (theme === 'dark') {
|
|
1405
|
+
document.documentElement.style.colorScheme = 'dark';
|
|
1406
|
+
} else if (theme === 'light') {
|
|
1407
|
+
document.documentElement.style.colorScheme = 'light';
|
|
1408
|
+
} else {
|
|
1409
|
+
document.documentElement.style.colorScheme = 'light dark';
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function getFormData() {
|
|
1414
|
+
if (!state.formRoot) return {};
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
const { result } = collectAndValidate(state.schema);
|
|
1418
|
+
return result;
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
console.error('Error getting form data:', error);
|
|
1421
|
+
return {};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function loadFormData(data) {
|
|
1426
|
+
if (!state.formRoot || !data) return;
|
|
1427
|
+
|
|
1428
|
+
Object.keys(data).forEach(key => {
|
|
1429
|
+
const input = state.formRoot.querySelector(`[data-key="${key}"]`);
|
|
1430
|
+
if (!input) return;
|
|
1431
|
+
|
|
1432
|
+
if (input.type === 'checkbox') {
|
|
1433
|
+
input.checked = Boolean(data[key]);
|
|
1434
|
+
} else if (input.type === 'file') {
|
|
1435
|
+
// For file inputs, store the value in dataset
|
|
1436
|
+
if (data[key]) {
|
|
1437
|
+
input.dataset.fileInfo = JSON.stringify(data[key]);
|
|
1438
|
+
// Trigger preview update if file preview is available
|
|
1439
|
+
const event = new Event('change');
|
|
1440
|
+
input.dispatchEvent(event);
|
|
1441
|
+
}
|
|
1442
|
+
} else {
|
|
1443
|
+
input.value = data[key] || '';
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function validateForm() {
|
|
1449
|
+
if (!state.formRoot || !state.schema) return true;
|
|
1450
|
+
|
|
1451
|
+
try {
|
|
1452
|
+
const { errors } = collectAndValidate(state.schema);
|
|
1453
|
+
return errors.length === 0;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// Enhanced form submission to notify parent
|
|
1460
|
+
function submitFormEnhanced() {
|
|
1461
|
+
clearError(el.formErrors);
|
|
1462
|
+
if (!state.schema) {
|
|
1463
|
+
showError(el.formErrors, 'Schema not applied');
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
try {
|
|
1468
|
+
const { result, errors } = collectAndValidate(state.schema);
|
|
1469
|
+
|
|
1470
|
+
if (errors.length > 0) {
|
|
1471
|
+
showError(el.formErrors, errors.join('\n'));
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Update output display
|
|
1476
|
+
el.outputJson.value = pretty(result);
|
|
1477
|
+
|
|
1478
|
+
// Notify parent window if embedded
|
|
1479
|
+
if (window.parent !== window) {
|
|
1480
|
+
window.parent.postMessage({
|
|
1481
|
+
type: 'formSubmit',
|
|
1482
|
+
data: result,
|
|
1483
|
+
schema: state.schema
|
|
1484
|
+
}, '*');
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Dispatch custom event for direct integration
|
|
1488
|
+
document.dispatchEvent(new CustomEvent('formSubmit', {
|
|
1489
|
+
detail: { data: result, schema: state.schema }
|
|
1490
|
+
}));
|
|
1491
|
+
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
showError(el.formErrors, 'Submission error: ' + error.message);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Replace original submit handler
|
|
1498
|
+
el.submitBtn.addEventListener('click', submitFormEnhanced);
|
|
1499
|
+
|
|
1500
|
+
// Draft save handler
|
|
1501
|
+
el.saveDraftBtn.addEventListener('click', () => {
|
|
1502
|
+
clearError(el.formErrors);
|
|
1503
|
+
if (!state.schema) {
|
|
1504
|
+
showError(el.formErrors, 'Schema not applied');
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
const { result } = collectAndValidate(state.schema, true); // Skip validation for drafts
|
|
1510
|
+
|
|
1511
|
+
// Update output display
|
|
1512
|
+
el.outputJson.value = pretty(result);
|
|
1513
|
+
|
|
1514
|
+
// Notify parent window if embedded
|
|
1515
|
+
if (window.parent !== window) {
|
|
1516
|
+
window.parent.postMessage({
|
|
1517
|
+
type: 'draftSaved',
|
|
1518
|
+
data: result,
|
|
1519
|
+
schema: state.schema
|
|
1520
|
+
}, '*');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Dispatch custom event for direct integration
|
|
1524
|
+
document.dispatchEvent(new CustomEvent('draftSaved', {
|
|
1525
|
+
detail: { data: result, schema: state.schema }
|
|
1526
|
+
}));
|
|
1527
|
+
|
|
1528
|
+
// Visual feedback
|
|
1529
|
+
el.saveDraftBtn.textContent = 'Draft Saved!';
|
|
1530
|
+
setTimeout(() => {
|
|
1531
|
+
el.saveDraftBtn.textContent = 'Save Draft';
|
|
1532
|
+
}, 2000);
|
|
1533
|
+
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
showError(el.formErrors, 'Draft save error: ' + error.message);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1264
1538
|
|
|
1265
1539
|
// Initialize
|
|
1266
1540
|
function init() {
|
|
1267
1541
|
el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
|
|
1268
1542
|
renderForm(EXAMPLE_SCHEMA, {});
|
|
1269
1543
|
loadSchemaFromURL();
|
|
1544
|
+
setupGeppettoMessaging();
|
|
1270
1545
|
|
|
1271
1546
|
// Expose configuration API
|
|
1272
1547
|
window.dispatchEvent(new CustomEvent('formBuilderReady', { detail: window.FormBuilderConfig }));
|