@dmitryvim/form-builder 0.1.22 → 0.1.25
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 +13 -6
- package/dist/demo.js +488 -473
- package/dist/form-builder.js +1433 -1291
- package/dist/index.html +270 -142
- package/docs/13_form_builder.html +1217 -543
- package/docs/REQUIREMENTS.md +46 -14
- package/docs/integration.md +241 -206
- package/docs/schema.md +37 -31
- package/package.json +14 -2
package/dist/demo.js
CHANGED
|
@@ -3,572 +3,587 @@
|
|
|
3
3
|
|
|
4
4
|
// Example schema for demonstration
|
|
5
5
|
const EXAMPLE_SCHEMA = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
version: "0.3",
|
|
7
|
+
title: "Asset Uploader with Slides",
|
|
8
|
+
elements: [
|
|
9
|
+
{
|
|
10
|
+
type: "file",
|
|
11
|
+
key: "cover",
|
|
12
|
+
label: "Изображение товара",
|
|
13
|
+
description:
|
|
14
|
+
"Source of facts: Use only the description text and visible labels/markings in the photo. Target scene: Show the product in a real-life usage context, highlighting its purpose and benefits. Product integrity: Do not change geometry, proportions, color, texture, hardware/stitching, or functional elements.",
|
|
15
|
+
required: true,
|
|
16
|
+
accept: {
|
|
17
|
+
extensions: ["png", "jpg", "gif"],
|
|
18
|
+
mime: ["image/png", "image/jpeg", "image/gif"],
|
|
19
|
+
},
|
|
20
|
+
maxSizeMB: 10,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: "files",
|
|
24
|
+
key: "assets",
|
|
25
|
+
label: "Другие изображения",
|
|
26
|
+
description:
|
|
27
|
+
"Additional images that provide context, show different angles, or demonstrate product usage. These will be used to enhance the infographic composition.",
|
|
28
|
+
required: false,
|
|
29
|
+
accept: {
|
|
30
|
+
extensions: ["png", "jpg", "gif"],
|
|
31
|
+
mime: ["image/png", "image/jpeg", "image/gif"],
|
|
32
|
+
},
|
|
33
|
+
minCount: 0,
|
|
34
|
+
maxCount: 10,
|
|
35
|
+
maxSizeMB: 5,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
key: "title",
|
|
40
|
+
label: "Название товара",
|
|
41
|
+
placeholder: "Введите название товара",
|
|
42
|
+
description:
|
|
43
|
+
"The exact product name as it should appear on the infographic. Use the official product name without marketing terms or decorative elements.",
|
|
44
|
+
required: true,
|
|
45
|
+
minLength: 1,
|
|
46
|
+
maxLength: 100,
|
|
47
|
+
pattern: "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
48
|
+
default: "",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: "textarea",
|
|
52
|
+
key: "description",
|
|
53
|
+
label: "Описание товара",
|
|
54
|
+
placeholder: "Введите описание товара",
|
|
55
|
+
description:
|
|
56
|
+
"Detailed product description focusing on key features, benefits, and usage scenarios. This text will be used to create compelling infographic content.",
|
|
57
|
+
required: false,
|
|
58
|
+
minLength: 0,
|
|
59
|
+
maxLength: 500,
|
|
60
|
+
pattern: "^[A-Za-zА-Яа-я0-9 ,.!?()_-]*$",
|
|
61
|
+
default: "",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "select",
|
|
65
|
+
key: "theme",
|
|
66
|
+
label: "Тема",
|
|
67
|
+
placeholder: "Выберите тему",
|
|
68
|
+
description:
|
|
69
|
+
"Visual theme that determines color scheme, typography, and overall aesthetic of the generated infographic.",
|
|
70
|
+
required: true,
|
|
71
|
+
options: [
|
|
72
|
+
{ value: "", label: "Выберите тему" },
|
|
73
|
+
{ value: "modern-minimal", label: "Современный минимализм" },
|
|
74
|
+
{ value: "warm-vintage", label: "Теплый винтаж" },
|
|
75
|
+
{ value: "tech-blue", label: "Технологичный синий" },
|
|
76
|
+
{ value: "nature-green", label: "Природный зеленый" },
|
|
77
|
+
{ value: "luxury-gold", label: "Роскошный золотой" },
|
|
78
|
+
],
|
|
79
|
+
default: "",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "number",
|
|
83
|
+
key: "opacity",
|
|
84
|
+
label: "Прозрачность",
|
|
85
|
+
placeholder: "0.50",
|
|
86
|
+
description:
|
|
87
|
+
"Transparency level for background elements in the infographic. Values between 0 (fully transparent) and 1 (fully opaque).",
|
|
88
|
+
required: true,
|
|
89
|
+
min: 0,
|
|
90
|
+
max: 1,
|
|
91
|
+
decimals: 2,
|
|
92
|
+
step: 0.01,
|
|
93
|
+
default: 0.5,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "group",
|
|
97
|
+
key: "slides",
|
|
98
|
+
label: "Слайды",
|
|
99
|
+
description:
|
|
100
|
+
"Additional slides to showcase different aspects of your product. Each slide can have its own main image and decorative elements to create a comprehensive infographic presentation.",
|
|
101
|
+
repeat: { min: 1, max: 5 },
|
|
102
|
+
elements: [
|
|
9
103
|
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
"maxSizeMB": 10
|
|
104
|
+
type: "text",
|
|
105
|
+
key: "title",
|
|
106
|
+
label: "Заголовок слайда",
|
|
107
|
+
required: true,
|
|
108
|
+
minLength: 1,
|
|
109
|
+
maxLength: 80,
|
|
110
|
+
pattern: "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
111
|
+
default: "",
|
|
20
112
|
},
|
|
21
113
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
"minCount": 0,
|
|
32
|
-
"maxCount": 10,
|
|
33
|
-
"maxSizeMB": 5
|
|
114
|
+
type: "textarea",
|
|
115
|
+
key: "body",
|
|
116
|
+
label: "Текст слайда",
|
|
117
|
+
required: true,
|
|
118
|
+
minLength: 1,
|
|
119
|
+
maxLength: 1000,
|
|
120
|
+
pattern: "^[A-Za-zА-Яа-я0-9 ,.!?()_*-]*$",
|
|
121
|
+
default: "",
|
|
34
122
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"label": "Название товара",
|
|
39
|
-
"placeholder": "Введите название товара",
|
|
40
|
-
"description": "The exact product name as it should appear on the infographic. Use the official product name without marketing terms or decorative elements.",
|
|
41
|
-
"required": true,
|
|
42
|
-
"minLength": 1,
|
|
43
|
-
"maxLength": 100,
|
|
44
|
-
"pattern": "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
45
|
-
"default": ""
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"type": "textarea",
|
|
49
|
-
"key": "description",
|
|
50
|
-
"label": "Описание товара",
|
|
51
|
-
"placeholder": "Введите описание товара",
|
|
52
|
-
"description": "Detailed product description focusing on key features, benefits, and usage scenarios. This text will be used to create compelling infographic content.",
|
|
53
|
-
"required": false,
|
|
54
|
-
"minLength": 0,
|
|
55
|
-
"maxLength": 500,
|
|
56
|
-
"pattern": "^[A-Za-zА-Яа-я0-9 ,.!?()_-]*$",
|
|
57
|
-
"default": ""
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"type": "select",
|
|
61
|
-
"key": "theme",
|
|
62
|
-
"label": "Тема",
|
|
63
|
-
"placeholder": "Выберите тему",
|
|
64
|
-
"description": "Visual theme that determines color scheme, typography, and overall aesthetic of the generated infographic.",
|
|
65
|
-
"required": true,
|
|
66
|
-
"options": [
|
|
67
|
-
{"value": "", "label": "Выберите тему"},
|
|
68
|
-
{"value": "modern-minimal", "label": "Современный минимализм"},
|
|
69
|
-
{"value": "warm-vintage", "label": "Теплый винтаж"},
|
|
70
|
-
{"value": "tech-blue", "label": "Технологичный синий"},
|
|
71
|
-
{"value": "nature-green", "label": "Природный зеленый"},
|
|
72
|
-
{"value": "luxury-gold", "label": "Роскошный золотой"}
|
|
73
|
-
],
|
|
74
|
-
"default": ""
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"type": "number",
|
|
78
|
-
"key": "opacity",
|
|
79
|
-
"label": "Прозрачность",
|
|
80
|
-
"placeholder": "0.50",
|
|
81
|
-
"description": "Transparency level for background elements in the infographic. Values between 0 (fully transparent) and 1 (fully opaque).",
|
|
82
|
-
"required": true,
|
|
83
|
-
"min": 0,
|
|
84
|
-
"max": 1,
|
|
85
|
-
"decimals": 2,
|
|
86
|
-
"step": 0.01,
|
|
87
|
-
"default": 0.50
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"type": "group",
|
|
91
|
-
"key": "slides",
|
|
92
|
-
"label": "Слайды",
|
|
93
|
-
"description": "Additional slides to showcase different aspects of your product. Each slide can have its own main image and decorative elements to create a comprehensive infographic presentation.",
|
|
94
|
-
"repeat": {"min": 1, "max": 5},
|
|
95
|
-
"elements": [
|
|
96
|
-
{
|
|
97
|
-
"type": "text",
|
|
98
|
-
"key": "title",
|
|
99
|
-
"label": "Заголовок слайда",
|
|
100
|
-
"required": true,
|
|
101
|
-
"minLength": 1,
|
|
102
|
-
"maxLength": 80,
|
|
103
|
-
"pattern": "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
104
|
-
"default": ""
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"type": "textarea",
|
|
108
|
-
"key": "body",
|
|
109
|
-
"label": "Текст слайда",
|
|
110
|
-
"required": true,
|
|
111
|
-
"minLength": 1,
|
|
112
|
-
"maxLength": 1000,
|
|
113
|
-
"pattern": "^[A-Za-zА-Яа-я0-9 ,.!?()_*-]*$",
|
|
114
|
-
"default": ""
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
119
126
|
};
|
|
120
127
|
|
|
121
128
|
// DOM element references for demo app
|
|
122
129
|
const el = {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
schemaInput: document.getElementById("schemaInput"),
|
|
131
|
+
schemaErrors: document.getElementById("schemaErrors"),
|
|
132
|
+
applySchemaBtn: document.getElementById("applySchemaBtn"),
|
|
133
|
+
resetSchemaBtn: document.getElementById("resetSchemaBtn"),
|
|
134
|
+
prettySchemaBtn: document.getElementById("prettySchemaBtn"),
|
|
135
|
+
downloadSchemaBtn: document.getElementById("downloadSchemaBtn"),
|
|
136
|
+
formContainer: document.getElementById("formContainer"),
|
|
137
|
+
formErrors: document.getElementById("formErrors"),
|
|
138
|
+
submitBtn: document.getElementById("submitBtn"),
|
|
139
|
+
saveDraftBtn: document.getElementById("saveDraftBtn"),
|
|
140
|
+
clearFormBtn: document.getElementById("clearFormBtn"),
|
|
141
|
+
outputJson: document.getElementById("outputJson"),
|
|
142
|
+
copyOutputBtn: document.getElementById("copyOutputBtn"),
|
|
143
|
+
downloadOutputBtn: document.getElementById("downloadOutputBtn"),
|
|
144
|
+
shareUrlBtn: document.getElementById("shareUrlBtn"),
|
|
145
|
+
prefillInput: document.getElementById("prefillInput"),
|
|
146
|
+
loadPrefillBtn: document.getElementById("loadPrefillBtn"),
|
|
147
|
+
copyTemplateBtn: document.getElementById("copyTemplateBtn"),
|
|
148
|
+
prefillErrors: document.getElementById("prefillErrors"),
|
|
149
|
+
urlInfo: document.getElementById("urlInfo"),
|
|
150
|
+
// Readonly demo elements
|
|
151
|
+
readonlySchemaInput: document.getElementById("readonlySchemaInput"),
|
|
152
|
+
applyReadonlyBtn: document.getElementById("applyReadonlyBtn"),
|
|
153
|
+
clearReadonlyBtn: document.getElementById("clearReadonlyBtn"),
|
|
154
|
+
readonlyErrors: document.getElementById("readonlyErrors"),
|
|
155
|
+
readonlyDemoContainer: document.getElementById("readonlyDemoContainer"),
|
|
149
156
|
};
|
|
150
157
|
|
|
151
158
|
// Utility functions (using FormBuilder.pretty)
|
|
152
159
|
function pretty(obj) {
|
|
153
|
-
|
|
160
|
+
return FormBuilder.pretty(obj);
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
function showError(element, message) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
if (element) {
|
|
165
|
+
element.textContent = message;
|
|
166
|
+
element.classList.remove("hidden");
|
|
167
|
+
}
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
function clearError(element) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
171
|
+
if (element) {
|
|
172
|
+
element.textContent = "";
|
|
173
|
+
element.classList.add("hidden");
|
|
174
|
+
}
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
function downloadFile(filename, content) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
const blob = new Blob([content], { type: "text/plain" });
|
|
179
|
+
const url = URL.createObjectURL(blob);
|
|
180
|
+
const a = document.createElement("a");
|
|
181
|
+
a.href = url;
|
|
182
|
+
a.download = filename;
|
|
183
|
+
a.click();
|
|
184
|
+
URL.revokeObjectURL(url);
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
// Demo event handlers
|
|
181
|
-
el.applySchemaBtn.addEventListener(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
el.outputJson.value = '';
|
|
194
|
-
clearError(el.formErrors);
|
|
195
|
-
} catch (e) {
|
|
196
|
-
showError(el.schemaErrors, 'JSON parse error: ' + e.message);
|
|
188
|
+
el.applySchemaBtn.addEventListener("click", () => {
|
|
189
|
+
clearError(el.schemaErrors);
|
|
190
|
+
try {
|
|
191
|
+
const schema = JSON.parse(el.schemaInput.value);
|
|
192
|
+
const errors = FormBuilder.validateSchema(schema);
|
|
193
|
+
|
|
194
|
+
if (errors.length > 0) {
|
|
195
|
+
showError(
|
|
196
|
+
el.schemaErrors,
|
|
197
|
+
"Schema validation errors: " + errors.join(", "),
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
197
200
|
}
|
|
201
|
+
|
|
202
|
+
FormBuilder.renderForm(schema, {});
|
|
203
|
+
el.outputJson.value = "";
|
|
204
|
+
clearError(el.formErrors);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
showError(el.schemaErrors, "JSON parse error: " + e.message);
|
|
207
|
+
}
|
|
198
208
|
});
|
|
199
209
|
|
|
200
|
-
el.resetSchemaBtn.addEventListener(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
el.resetSchemaBtn.addEventListener("click", () => {
|
|
211
|
+
el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
|
|
212
|
+
clearError(el.schemaErrors);
|
|
213
|
+
FormBuilder.renderForm(EXAMPLE_SCHEMA, {});
|
|
214
|
+
el.outputJson.value = "";
|
|
215
|
+
el.prefillInput.value = "";
|
|
216
|
+
clearError(el.prefillErrors);
|
|
207
217
|
});
|
|
208
218
|
|
|
209
|
-
el.prettySchemaBtn.addEventListener(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
el.prettySchemaBtn.addEventListener("click", () => {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(el.schemaInput.value);
|
|
222
|
+
el.schemaInput.value = pretty(parsed);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
showError(el.schemaErrors, "Prettify: JSON parse error: " + e.message);
|
|
225
|
+
}
|
|
216
226
|
});
|
|
217
227
|
|
|
218
|
-
el.downloadSchemaBtn.addEventListener(
|
|
219
|
-
|
|
228
|
+
el.downloadSchemaBtn.addEventListener("click", () => {
|
|
229
|
+
downloadFile("schema.json", el.schemaInput.value || pretty(EXAMPLE_SCHEMA));
|
|
220
230
|
});
|
|
221
231
|
|
|
222
|
-
el.clearFormBtn.addEventListener(
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
el.clearFormBtn.addEventListener("click", () => {
|
|
233
|
+
FormBuilder.clearForm();
|
|
234
|
+
clearError(el.formErrors);
|
|
225
235
|
});
|
|
226
236
|
|
|
227
|
-
el.copyOutputBtn.addEventListener(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
el.copyOutputBtn.addEventListener("click", async () => {
|
|
238
|
+
try {
|
|
239
|
+
await navigator.clipboard.writeText(el.outputJson.value || "");
|
|
240
|
+
el.copyOutputBtn.textContent = "Copied!";
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
el.copyOutputBtn.textContent = "Copy JSON";
|
|
243
|
+
}, 1000);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.warn("Copy failed:", e);
|
|
246
|
+
}
|
|
237
247
|
});
|
|
238
248
|
|
|
239
|
-
el.downloadOutputBtn.addEventListener(
|
|
240
|
-
|
|
249
|
+
el.downloadOutputBtn.addEventListener("click", () => {
|
|
250
|
+
downloadFile("form-data.json", el.outputJson.value || "{}");
|
|
241
251
|
});
|
|
242
252
|
|
|
243
|
-
el.shareUrlBtn.addEventListener(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
el.shareUrlBtn.addEventListener("click", () => {
|
|
254
|
+
try {
|
|
255
|
+
const schema = JSON.parse(el.schemaInput.value);
|
|
256
|
+
const schemaBase64 = btoa(JSON.stringify(schema));
|
|
257
|
+
const url = `${window.location.origin}${window.location.pathname}?schema=${schemaBase64}`;
|
|
258
|
+
navigator.clipboard.writeText(url);
|
|
259
|
+
el.shareUrlBtn.textContent = "URL Copied!";
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
el.shareUrlBtn.textContent = "Share URL";
|
|
262
|
+
}, 2000);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
alert("Please apply a valid schema first");
|
|
265
|
+
}
|
|
256
266
|
});
|
|
257
267
|
|
|
258
|
-
el.loadPrefillBtn.addEventListener(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
el.loadPrefillBtn.addEventListener("click", () => {
|
|
269
|
+
clearError(el.prefillErrors);
|
|
270
|
+
try {
|
|
271
|
+
const pre = JSON.parse(el.prefillInput.value || "{}");
|
|
272
|
+
const currentSchema = JSON.parse(el.schemaInput.value);
|
|
273
|
+
FormBuilder.renderForm(currentSchema, pre);
|
|
274
|
+
el.outputJson.value = "";
|
|
275
|
+
clearError(el.formErrors);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
showError(el.prefillErrors, "JSON parse error: " + e.message);
|
|
278
|
+
}
|
|
269
279
|
});
|
|
270
280
|
|
|
271
|
-
el.copyTemplateBtn.addEventListener(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
});
|
|
281
|
+
el.copyTemplateBtn.addEventListener("click", () => {
|
|
282
|
+
try {
|
|
283
|
+
const schema = JSON.parse(el.schemaInput.value);
|
|
284
|
+
const template = {};
|
|
285
|
+
|
|
286
|
+
function processElements(elements, target) {
|
|
287
|
+
elements.forEach((element) => {
|
|
288
|
+
if (element.type === "group" && element.repeat) {
|
|
289
|
+
target[element.key] = [{}];
|
|
290
|
+
if (element.elements) {
|
|
291
|
+
processElements(element.elements, target[element.key][0]);
|
|
292
|
+
}
|
|
293
|
+
} else if (element.type === "group") {
|
|
294
|
+
target[element.key] = {};
|
|
295
|
+
if (element.elements) {
|
|
296
|
+
processElements(element.elements, target[element.key]);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
target[element.key] = element.default || null;
|
|
292
300
|
}
|
|
293
|
-
|
|
294
|
-
processElements(schema.elements, template);
|
|
295
|
-
el.prefillInput.value = pretty(template);
|
|
296
|
-
} catch (e) {
|
|
297
|
-
showError(el.prefillErrors, 'Schema parse error: ' + e.message);
|
|
301
|
+
});
|
|
298
302
|
}
|
|
303
|
+
|
|
304
|
+
processElements(schema.elements, template);
|
|
305
|
+
el.prefillInput.value = pretty(template);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
showError(el.prefillErrors, "Schema parse error: " + e.message);
|
|
308
|
+
}
|
|
299
309
|
});
|
|
300
310
|
|
|
301
311
|
// Form submission handlers
|
|
302
312
|
function submitFormEnhanced() {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
} else if (Array.isArray(obj[key])) {
|
|
328
|
-
obj[key].forEach(item => {
|
|
329
|
-
if (typeof item === 'string' && item.startsWith('res_')) {
|
|
330
|
-
const index = obj[key].indexOf(item);
|
|
331
|
-
const meta = FormBuilder.state.resourceIndex.get(item);
|
|
332
|
-
if (meta) {
|
|
333
|
-
obj[key][index] = {
|
|
334
|
-
resourceId: item,
|
|
335
|
-
name: meta.name,
|
|
336
|
-
type: meta.type,
|
|
337
|
-
size: meta.size
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
} else if (typeof item === 'object') {
|
|
341
|
-
replaceResourceIds(item);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
345
|
-
replaceResourceIds(obj[key]);
|
|
346
|
-
}
|
|
313
|
+
clearError(el.formErrors);
|
|
314
|
+
|
|
315
|
+
const result = FormBuilder.getFormData();
|
|
316
|
+
|
|
317
|
+
if (!result.valid) {
|
|
318
|
+
showError(el.formErrors, "Validation errors: " + result.errors.join(", "));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Convert resource IDs to file info for demo
|
|
323
|
+
const processedData = JSON.parse(JSON.stringify(result.data));
|
|
324
|
+
|
|
325
|
+
function replaceResourceIds(obj) {
|
|
326
|
+
for (const key in obj) {
|
|
327
|
+
if (typeof obj[key] === "string" && obj[key].startsWith("res_")) {
|
|
328
|
+
const meta = FormBuilder.state.resourceIndex.get(obj[key]);
|
|
329
|
+
if (meta) {
|
|
330
|
+
obj[key] = {
|
|
331
|
+
resourceId: obj[key],
|
|
332
|
+
name: meta.name,
|
|
333
|
+
type: meta.type,
|
|
334
|
+
size: meta.size,
|
|
335
|
+
};
|
|
347
336
|
}
|
|
337
|
+
} else if (Array.isArray(obj[key])) {
|
|
338
|
+
obj[key].forEach((item) => {
|
|
339
|
+
if (typeof item === "string" && item.startsWith("res_")) {
|
|
340
|
+
const index = obj[key].indexOf(item);
|
|
341
|
+
const meta = FormBuilder.state.resourceIndex.get(item);
|
|
342
|
+
if (meta) {
|
|
343
|
+
obj[key][index] = {
|
|
344
|
+
resourceId: item,
|
|
345
|
+
name: meta.name,
|
|
346
|
+
type: meta.type,
|
|
347
|
+
size: meta.size,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
} else if (typeof item === "object") {
|
|
351
|
+
replaceResourceIds(item);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
355
|
+
replaceResourceIds(obj[key]);
|
|
356
|
+
}
|
|
348
357
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
replaceResourceIds(processedData);
|
|
361
|
+
|
|
362
|
+
el.outputJson.value = pretty(processedData);
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
el.submitBtn.addEventListener(
|
|
356
|
-
el.saveDraftBtn.addEventListener(
|
|
357
|
-
|
|
358
|
-
|
|
365
|
+
el.submitBtn.addEventListener("click", submitFormEnhanced);
|
|
366
|
+
el.saveDraftBtn.addEventListener("click", () => {
|
|
367
|
+
const result = FormBuilder.getFormData();
|
|
368
|
+
el.outputJson.value = pretty(result.data);
|
|
359
369
|
});
|
|
360
370
|
|
|
361
371
|
// Readonly demo handlers
|
|
362
|
-
el.applyReadonlyBtn.addEventListener(
|
|
363
|
-
|
|
372
|
+
el.applyReadonlyBtn.addEventListener("click", () => {
|
|
373
|
+
clearError(el.readonlyErrors);
|
|
374
|
+
try {
|
|
375
|
+
const schema = JSON.parse(el.readonlySchemaInput.value);
|
|
376
|
+
const errors = FormBuilder.validateSchema(schema);
|
|
377
|
+
|
|
378
|
+
if (errors.length > 0) {
|
|
379
|
+
showError(
|
|
380
|
+
el.readonlyErrors,
|
|
381
|
+
"Schema validation errors: " + errors.join(", "),
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Clear container
|
|
387
|
+
el.readonlyDemoContainer.innerHTML = "";
|
|
388
|
+
|
|
389
|
+
// Store original form settings
|
|
390
|
+
const originalFormRoot = FormBuilder.state.formRoot;
|
|
391
|
+
const originalMode = FormBuilder.state.config.readonly;
|
|
392
|
+
|
|
364
393
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
// Clear container
|
|
374
|
-
el.readonlyDemoContainer.innerHTML = '';
|
|
375
|
-
|
|
376
|
-
// Store original form settings
|
|
377
|
-
const originalFormRoot = FormBuilder.state.formRoot;
|
|
378
|
-
const originalMode = FormBuilder.state.config.readonly;
|
|
379
|
-
|
|
380
|
-
try {
|
|
381
|
-
// Temporarily use readonly container
|
|
382
|
-
FormBuilder.setFormRoot(el.readonlyDemoContainer);
|
|
383
|
-
FormBuilder.setMode('readonly');
|
|
384
|
-
FormBuilder.renderForm(schema, DEFAULT_READONLY_DATA);
|
|
385
|
-
} finally {
|
|
386
|
-
// Restore original settings
|
|
387
|
-
FormBuilder.setFormRoot(originalFormRoot);
|
|
388
|
-
FormBuilder.state.config.readonly = originalMode;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
} catch (e) {
|
|
392
|
-
showError(el.readonlyErrors, 'JSON parse error: ' + e.message);
|
|
394
|
+
// Temporarily use readonly container
|
|
395
|
+
FormBuilder.setFormRoot(el.readonlyDemoContainer);
|
|
396
|
+
FormBuilder.setMode("readonly");
|
|
397
|
+
FormBuilder.renderForm(schema, DEFAULT_READONLY_DATA);
|
|
398
|
+
} finally {
|
|
399
|
+
// Restore original settings
|
|
400
|
+
FormBuilder.setFormRoot(originalFormRoot);
|
|
401
|
+
FormBuilder.state.config.readonly = originalMode;
|
|
393
402
|
}
|
|
403
|
+
} catch (e) {
|
|
404
|
+
showError(el.readonlyErrors, "JSON parse error: " + e.message);
|
|
405
|
+
}
|
|
394
406
|
});
|
|
395
407
|
|
|
396
|
-
el.clearReadonlyBtn.addEventListener(
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
el.clearReadonlyBtn.addEventListener("click", () => {
|
|
409
|
+
el.readonlyDemoContainer.innerHTML =
|
|
410
|
+
'<div class="text-center text-gray-500 py-8">Apply a schema to see readonly mode</div>';
|
|
411
|
+
clearError(el.readonlyErrors);
|
|
399
412
|
});
|
|
400
413
|
|
|
401
414
|
// URL schema loading
|
|
402
415
|
function loadSchemaFromURL() {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
416
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
417
|
+
const schemaParam = urlParams.get("schema");
|
|
418
|
+
|
|
419
|
+
if (schemaParam) {
|
|
420
|
+
try {
|
|
421
|
+
const schema = JSON.parse(atob(schemaParam));
|
|
422
|
+
el.schemaInput.value = pretty(schema);
|
|
423
|
+
FormBuilder.renderForm(schema, {});
|
|
424
|
+
el.urlInfo.classList.remove("hidden");
|
|
425
|
+
} catch (e) {
|
|
426
|
+
console.warn("Failed to load schema from URL:", e);
|
|
415
427
|
}
|
|
428
|
+
}
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
// Read-only demo functions
|
|
419
432
|
function downloadDemoFile(filePath, fileName) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
} else {
|
|
449
|
-
console.log('Not on local dev server, using simple link method');
|
|
433
|
+
console.log("downloadDemoFile called with:", filePath, fileName);
|
|
434
|
+
|
|
435
|
+
// Check if we're running on a local dev server
|
|
436
|
+
const isLocalDev =
|
|
437
|
+
window.location.protocol === "http:" &&
|
|
438
|
+
window.location.hostname === "localhost";
|
|
439
|
+
|
|
440
|
+
if (isLocalDev) {
|
|
441
|
+
console.log("Running on local dev server, using fetch method");
|
|
442
|
+
// Force download by fetching the file and creating a blob
|
|
443
|
+
fetch(filePath)
|
|
444
|
+
.then((response) => {
|
|
445
|
+
console.log("Fetch response:", response.ok, response.status);
|
|
446
|
+
return response.blob();
|
|
447
|
+
})
|
|
448
|
+
.then((blob) => {
|
|
449
|
+
console.log("Creating download link for blob:", blob.size, "bytes");
|
|
450
|
+
const link = document.createElement("a");
|
|
451
|
+
link.href = URL.createObjectURL(blob);
|
|
452
|
+
link.download = fileName;
|
|
453
|
+
document.body.appendChild(link);
|
|
454
|
+
link.click();
|
|
455
|
+
document.body.removeChild(link);
|
|
456
|
+
URL.revokeObjectURL(link.href);
|
|
457
|
+
console.log("Download completed successfully");
|
|
458
|
+
})
|
|
459
|
+
.catch((error) => {
|
|
460
|
+
console.error("Fetch download failed:", error);
|
|
450
461
|
simpleLinkDownload(filePath, fileName);
|
|
451
|
-
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
console.log("Not on local dev server, using simple link method");
|
|
465
|
+
simpleLinkDownload(filePath, fileName);
|
|
466
|
+
}
|
|
452
467
|
}
|
|
453
468
|
|
|
454
469
|
function simpleLinkDownload(filePath, fileName) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
console.log("Using simple link download:", filePath, fileName);
|
|
471
|
+
const link = document.createElement("a");
|
|
472
|
+
link.href = filePath;
|
|
473
|
+
link.download = fileName;
|
|
474
|
+
link.style.display = "none";
|
|
475
|
+
document.body.appendChild(link);
|
|
476
|
+
link.click();
|
|
477
|
+
document.body.removeChild(link);
|
|
478
|
+
console.log("Simple link download triggered");
|
|
464
479
|
}
|
|
465
480
|
|
|
466
481
|
// Configure download handler for readonly demo
|
|
467
482
|
function setupReadonlyDemo() {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
483
|
+
// Set up download handler that works with local demo files
|
|
484
|
+
FormBuilder.setDownloadHandler((resourceId, fileName) => {
|
|
485
|
+
const demoFileMap = {
|
|
486
|
+
demo_infographic: "images/infographic_draft.jpg",
|
|
487
|
+
demo_video: "images/final_video.mp4",
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const filePath = demoFileMap[resourceId] || `images/${fileName}`;
|
|
491
|
+
downloadDemoFile(filePath, fileName);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Set up thumbnail handler for demo (returns URL directly, not Promise)
|
|
495
|
+
FormBuilder.setThumbnailHandler((resourceId) => {
|
|
496
|
+
const demoFileMap = {
|
|
497
|
+
demo_infographic: "images/infographic_draft.jpg",
|
|
498
|
+
demo_video: "images/final_video.mp4",
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return demoFileMap[resourceId] || null;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Pre-populate demo resource metadata
|
|
505
|
+
FormBuilder.state.resourceIndex.set("demo_infographic", {
|
|
506
|
+
name: "infographic_result.jpg",
|
|
507
|
+
type: "image/jpeg",
|
|
508
|
+
size: 150000,
|
|
509
|
+
file: null,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
FormBuilder.state.resourceIndex.set("demo_video", {
|
|
513
|
+
name: "final_video.mp4",
|
|
514
|
+
type: "video/mp4",
|
|
515
|
+
size: 2500000,
|
|
516
|
+
file: null,
|
|
517
|
+
});
|
|
503
518
|
}
|
|
504
519
|
|
|
505
520
|
// Default readonly schema for demo
|
|
506
521
|
const DEFAULT_READONLY_SCHEMA = {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
522
|
+
version: "0.3",
|
|
523
|
+
title: "Результаты работы",
|
|
524
|
+
elements: [
|
|
525
|
+
{
|
|
526
|
+
type: "file",
|
|
527
|
+
key: "result_image",
|
|
528
|
+
label: "Изображение результата",
|
|
529
|
+
description: "Готовая инфографика на основе загруженных данных",
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
type: "file",
|
|
533
|
+
key: "result_video",
|
|
534
|
+
label: "Видео результат",
|
|
535
|
+
description: "Финальное видео презентации",
|
|
536
|
+
},
|
|
537
|
+
],
|
|
523
538
|
};
|
|
524
539
|
|
|
525
|
-
// Default readonly data
|
|
540
|
+
// Default readonly data
|
|
526
541
|
const DEFAULT_READONLY_DATA = {
|
|
527
|
-
|
|
528
|
-
|
|
542
|
+
result_image: "demo_infographic",
|
|
543
|
+
result_video: "demo_video",
|
|
529
544
|
};
|
|
530
545
|
|
|
531
546
|
function renderReadonlyDemo() {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
547
|
+
// Set default schema in textarea
|
|
548
|
+
const textarea = document.getElementById("readonlySchemaInput");
|
|
549
|
+
if (textarea && !textarea.value.trim()) {
|
|
550
|
+
textarea.value = pretty(DEFAULT_READONLY_SCHEMA);
|
|
551
|
+
}
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
// Initialize demo application
|
|
540
555
|
function initDemo() {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
556
|
+
// Set up the form builder for main form
|
|
557
|
+
FormBuilder.setFormRoot(el.formContainer);
|
|
558
|
+
|
|
559
|
+
// Set up handlers for readonly demo
|
|
560
|
+
setupReadonlyDemo();
|
|
561
|
+
|
|
562
|
+
// Initialize with example schema
|
|
563
|
+
el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
|
|
564
|
+
FormBuilder.setMode("edit"); // Ensure main form is in edit mode
|
|
565
|
+
FormBuilder.renderForm(EXAMPLE_SCHEMA, {});
|
|
566
|
+
|
|
567
|
+
// Load schema from URL if present
|
|
568
|
+
loadSchemaFromURL();
|
|
569
|
+
|
|
570
|
+
// Initialize read-only demo
|
|
571
|
+
setTimeout(renderReadonlyDemo, 500);
|
|
557
572
|
}
|
|
558
573
|
|
|
559
574
|
// Start the demo when the page loads
|
|
560
|
-
document.addEventListener(
|
|
561
|
-
|
|
562
|
-
|
|
575
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
576
|
+
// Wait for FormBuilder to be available
|
|
577
|
+
if (typeof FormBuilder !== "undefined") {
|
|
578
|
+
initDemo();
|
|
579
|
+
} else {
|
|
580
|
+
// Fallback: wait a bit for scripts to load
|
|
581
|
+
setTimeout(() => {
|
|
582
|
+
if (typeof FormBuilder !== "undefined") {
|
|
563
583
|
initDemo();
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
} else {
|
|
570
|
-
console.error('FormBuilder not found!');
|
|
571
|
-
}
|
|
572
|
-
}, 100);
|
|
573
|
-
}
|
|
584
|
+
} else {
|
|
585
|
+
console.error("FormBuilder not found!");
|
|
586
|
+
}
|
|
587
|
+
}, 100);
|
|
588
|
+
}
|
|
574
589
|
});
|