@dmitryvim/form-builder 0.1.42 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -22
- package/dist/browser/formbuilder.min.js +179 -0
- package/dist/browser/formbuilder.v0.2.0.min.js +179 -0
- package/dist/cjs/index.cjs +3582 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.js +3534 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/form-builder.js +152 -3372
- package/dist/types/components/container.d.ts +15 -0
- package/dist/types/components/file.d.ts +26 -0
- package/dist/types/components/group.d.ts +24 -0
- package/dist/types/components/index.d.ts +11 -0
- package/dist/types/components/number.d.ts +11 -0
- package/dist/types/components/registry.d.ts +15 -0
- package/dist/types/components/select.d.ts +11 -0
- package/dist/types/components/text.d.ts +11 -0
- package/dist/types/components/textarea.d.ts +11 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/instance/FormBuilderInstance.d.ts +134 -0
- package/dist/types/instance/state.d.ts +13 -0
- package/dist/types/styles/theme.d.ts +63 -0
- package/dist/types/types/component-operations.d.ts +45 -0
- package/dist/types/types/config.d.ts +44 -0
- package/dist/types/types/index.d.ts +4 -0
- package/dist/types/types/schema.d.ts +115 -0
- package/dist/types/types/state.d.ts +11 -0
- package/dist/types/utils/helpers.d.ts +4 -0
- package/dist/types/utils/styles.d.ts +21 -0
- package/dist/types/utils/translation.d.ts +8 -0
- package/dist/types/utils/validation.d.ts +2 -0
- package/package.json +32 -15
- package/dist/demo.js +0 -861
- package/dist/elements.html +0 -1130
- package/dist/elements.js +0 -488
- package/dist/index.html +0 -315
package/dist/demo.js
DELETED
|
@@ -1,861 +0,0 @@
|
|
|
1
|
-
// Demo Application for Form Builder - Redesigned 3-Column Layout
|
|
2
|
-
// This file contains demo-specific code and should not be part of the core library
|
|
3
|
-
|
|
4
|
-
// Example schema for demonstration
|
|
5
|
-
const EXAMPLE_SCHEMA = {
|
|
6
|
-
version: "0.3",
|
|
7
|
-
title: "Asset Uploader with Slides",
|
|
8
|
-
actions: [
|
|
9
|
-
{
|
|
10
|
-
key: "test-missing",
|
|
11
|
-
label: "Другая потеря"
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
key: "save-draft",
|
|
15
|
-
label: "Сохранить черновик"
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
elements: [
|
|
19
|
-
{
|
|
20
|
-
type: "file",
|
|
21
|
-
key: "cover",
|
|
22
|
-
label: "Изображение товара",
|
|
23
|
-
description:
|
|
24
|
-
"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.",
|
|
25
|
-
required: true,
|
|
26
|
-
accept: {
|
|
27
|
-
extensions: ["png", "jpg", "gif"],
|
|
28
|
-
mime: ["image/png", "image/jpeg", "image/gif"],
|
|
29
|
-
},
|
|
30
|
-
maxSizeMB: 10,
|
|
31
|
-
actions: [
|
|
32
|
-
{ key: "retry", label: "А давай ещё разок" },
|
|
33
|
-
{ key: "enhance", label: "Улучшить качество" },
|
|
34
|
-
{ key: "crop", label: "Обрезать изображение" },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
type: "file",
|
|
39
|
-
key: "assets",
|
|
40
|
-
label: "Другие изображения",
|
|
41
|
-
description:
|
|
42
|
-
"Additional images that provide context, show different angles, or demonstrate product usage. These will be used to enhance the infographic composition.",
|
|
43
|
-
required: false,
|
|
44
|
-
multiple: true,
|
|
45
|
-
minCount: 0,
|
|
46
|
-
maxCount: 10,
|
|
47
|
-
accept: {
|
|
48
|
-
extensions: ["png", "jpg", "gif"],
|
|
49
|
-
mime: ["image/png", "image/jpeg", "image/gif"],
|
|
50
|
-
},
|
|
51
|
-
maxSizeMB: 5,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
type: "text",
|
|
55
|
-
key: "title",
|
|
56
|
-
label: "Название товара",
|
|
57
|
-
placeholder: "Введите название товара",
|
|
58
|
-
description:
|
|
59
|
-
"The exact product name as it should appear on the infographic. Use the official product name without marketing terms or decorative elements.",
|
|
60
|
-
required: true,
|
|
61
|
-
multiple: true,
|
|
62
|
-
minCount: 1,
|
|
63
|
-
maxCount: 4,
|
|
64
|
-
minLength: 1,
|
|
65
|
-
maxLength: 100,
|
|
66
|
-
pattern: "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
67
|
-
default: "",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
type: "textarea",
|
|
71
|
-
key: "description",
|
|
72
|
-
label: "Описание товара",
|
|
73
|
-
placeholder: "Введите описание товара",
|
|
74
|
-
description:
|
|
75
|
-
"Detailed product description focusing on key features, benefits, and usage scenarios. This text will be used to create compelling infographic content.",
|
|
76
|
-
required: false,
|
|
77
|
-
minLength: 0,
|
|
78
|
-
maxLength: 500,
|
|
79
|
-
pattern: "^[A-Za-zА-Яа-я0-9 ,.!?()_-]*$",
|
|
80
|
-
default: "",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
type: "select",
|
|
84
|
-
key: "theme",
|
|
85
|
-
label: "Тема",
|
|
86
|
-
placeholder: "Выберите тему",
|
|
87
|
-
description:
|
|
88
|
-
"Visual theme that determines color scheme, typography, and overall aesthetic of the generated infographic.",
|
|
89
|
-
required: true,
|
|
90
|
-
options: [
|
|
91
|
-
{ value: "", label: "Выберите тему" },
|
|
92
|
-
{ value: "modern-minimal", label: "Современный минимализм" },
|
|
93
|
-
{ value: "warm-vintage", label: "Теплый винтаж" },
|
|
94
|
-
{ value: "tech-blue", label: "Технологичный синий" },
|
|
95
|
-
{ value: "nature-green", label: "Природный зеленый" },
|
|
96
|
-
{ value: "luxury-gold", label: "Роскошный золотой" },
|
|
97
|
-
],
|
|
98
|
-
default: "",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
type: "number",
|
|
102
|
-
key: "opacity",
|
|
103
|
-
label: "Прозрачность",
|
|
104
|
-
placeholder: "0.50",
|
|
105
|
-
description:
|
|
106
|
-
"Transparency level for background elements in the infographic. Values between 0 (fully transparent) and 1 (fully opaque).",
|
|
107
|
-
required: true,
|
|
108
|
-
min: 0,
|
|
109
|
-
max: 1,
|
|
110
|
-
decimals: 2,
|
|
111
|
-
step: 0.01,
|
|
112
|
-
default: 0.5,
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
type: "text",
|
|
116
|
-
key: "tags",
|
|
117
|
-
label: "Теги товара",
|
|
118
|
-
description: "Добавьте ключевые теги для лучшего описания товара",
|
|
119
|
-
placeholder: "Введите тег",
|
|
120
|
-
required: false,
|
|
121
|
-
multiple: true,
|
|
122
|
-
minCount: 1,
|
|
123
|
-
maxCount: 5,
|
|
124
|
-
minLength: 2,
|
|
125
|
-
maxLength: 20,
|
|
126
|
-
default: "popular",
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
type: "textarea",
|
|
130
|
-
key: "features",
|
|
131
|
-
label: "Особенности товара",
|
|
132
|
-
description: "Опишите ключевые особенности и преимущества товара",
|
|
133
|
-
placeholder: "Опишите особенность...",
|
|
134
|
-
required: false,
|
|
135
|
-
multiple: true,
|
|
136
|
-
minCount: 2,
|
|
137
|
-
maxCount: 4,
|
|
138
|
-
minLength: 10,
|
|
139
|
-
maxLength: 200,
|
|
140
|
-
rows: 3,
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
type: "number",
|
|
144
|
-
key: "dimensions",
|
|
145
|
-
label: "Размеры (см)",
|
|
146
|
-
description:
|
|
147
|
-
"Укажите размеры товара в сантиметрах: длина, ширина, высота",
|
|
148
|
-
placeholder: "0",
|
|
149
|
-
required: false,
|
|
150
|
-
multiple: true,
|
|
151
|
-
minCount: 2,
|
|
152
|
-
maxCount: 3,
|
|
153
|
-
min: 0,
|
|
154
|
-
max: 500,
|
|
155
|
-
step: 0.1,
|
|
156
|
-
decimals: 1,
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
type: "select",
|
|
160
|
-
key: "colors",
|
|
161
|
-
label: "Доступные цвета",
|
|
162
|
-
description: "Выберите доступные цветовые варианты товара",
|
|
163
|
-
required: false,
|
|
164
|
-
multiple: true,
|
|
165
|
-
minCount: 1,
|
|
166
|
-
maxCount: 3,
|
|
167
|
-
options: [
|
|
168
|
-
{ value: "white", label: "Белый" },
|
|
169
|
-
{ value: "black", label: "Черный" },
|
|
170
|
-
{ value: "red", label: "Красный" },
|
|
171
|
-
{ value: "blue", label: "Синий" },
|
|
172
|
-
{ value: "green", label: "Зеленый" },
|
|
173
|
-
{ value: "gray", label: "Серый" },
|
|
174
|
-
],
|
|
175
|
-
default: "white",
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
type: "file",
|
|
179
|
-
key: "video",
|
|
180
|
-
label: "Video Content",
|
|
181
|
-
description:
|
|
182
|
-
"Optional video content to enhance the infographic. Supports MP4, WebM, and MOV formats for dynamic product demonstrations.",
|
|
183
|
-
required: false,
|
|
184
|
-
accept: {
|
|
185
|
-
extensions: ["mp4", "webm", "mov"],
|
|
186
|
-
mime: ["video/mp4", "video/webm", "video/quicktime"],
|
|
187
|
-
},
|
|
188
|
-
maxSizeMB: 50,
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
type: "container",
|
|
192
|
-
key: "slides",
|
|
193
|
-
label: "Слайды",
|
|
194
|
-
description:
|
|
195
|
-
"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.",
|
|
196
|
-
multiple: true,
|
|
197
|
-
minCount: 1,
|
|
198
|
-
maxCount: 5,
|
|
199
|
-
elements: [
|
|
200
|
-
{
|
|
201
|
-
type: "text",
|
|
202
|
-
key: "title",
|
|
203
|
-
label: "Заголовок слайда",
|
|
204
|
-
required: true,
|
|
205
|
-
minLength: 1,
|
|
206
|
-
maxLength: 80,
|
|
207
|
-
pattern: "^[A-Za-zА-Яа-я0-9 _*-]+$",
|
|
208
|
-
default: "",
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
type: "textarea",
|
|
212
|
-
key: "body",
|
|
213
|
-
label: "Текст слайда",
|
|
214
|
-
required: true,
|
|
215
|
-
minLength: 1,
|
|
216
|
-
maxLength: 1000,
|
|
217
|
-
pattern: "^[A-Za-zА-Яа-я0-9 ,.!?()_*-]*$",
|
|
218
|
-
default: "",
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
type: "text",
|
|
224
|
-
key: "session_id",
|
|
225
|
-
label: "Session ID",
|
|
226
|
-
description: "Hidden session identifier for tracking purposes",
|
|
227
|
-
hidden: true,
|
|
228
|
-
default: `session_${Math.random().toString(36).substr(2, 9)}`,
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// In-Memory File Storage System
|
|
234
|
-
class InMemoryFileStorage {
|
|
235
|
-
constructor() {
|
|
236
|
-
this.files = new Map(); // resourceId -> { blob, metadata }
|
|
237
|
-
this.resourceCounter = 0;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Generate unique resource ID
|
|
241
|
-
generateResourceId() {
|
|
242
|
-
return `res_${Date.now()}_${++this.resourceCounter}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Store file in memory and return resource ID
|
|
246
|
-
async storeFile(file) {
|
|
247
|
-
const resourceId = this.generateResourceId();
|
|
248
|
-
|
|
249
|
-
// Create blob from file
|
|
250
|
-
const blob = new Blob([file], { type: file.type });
|
|
251
|
-
|
|
252
|
-
// Store file data
|
|
253
|
-
this.files.set(resourceId, {
|
|
254
|
-
blob,
|
|
255
|
-
metadata: {
|
|
256
|
-
name: file.name,
|
|
257
|
-
type: file.type,
|
|
258
|
-
size: file.size,
|
|
259
|
-
uploadedAt: new Date().toISOString(),
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return resourceId;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Retrieve file blob by resource ID
|
|
267
|
-
getFile(resourceId) {
|
|
268
|
-
const fileData = this.files.get(resourceId);
|
|
269
|
-
return fileData ? fileData.blob : null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Get file metadata
|
|
273
|
-
getMetadata(resourceId) {
|
|
274
|
-
const fileData = this.files.get(resourceId);
|
|
275
|
-
return fileData ? fileData.metadata : null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Generate thumbnail URL for preview
|
|
279
|
-
getThumbnailUrl(resourceId) {
|
|
280
|
-
const blob = this.getFile(resourceId);
|
|
281
|
-
if (!blob) return null;
|
|
282
|
-
|
|
283
|
-
// For images and videos, return object URL for preview
|
|
284
|
-
if (blob.type.startsWith("image/") || blob.type.startsWith("video/")) {
|
|
285
|
-
return URL.createObjectURL(blob);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// For other files, return null (no thumbnail)
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Download file from memory
|
|
293
|
-
downloadFile(resourceId, fileName) {
|
|
294
|
-
const blob = this.getFile(resourceId);
|
|
295
|
-
const metadata = this.getMetadata(resourceId);
|
|
296
|
-
|
|
297
|
-
if (!blob) {
|
|
298
|
-
console.error("File not found:", resourceId);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Use provided fileName or fall back to original name
|
|
303
|
-
const downloadName = fileName || metadata.name;
|
|
304
|
-
|
|
305
|
-
// Create download link
|
|
306
|
-
const url = URL.createObjectURL(blob);
|
|
307
|
-
const link = document.createElement("a");
|
|
308
|
-
link.href = url;
|
|
309
|
-
link.download = downloadName;
|
|
310
|
-
document.body.appendChild(link);
|
|
311
|
-
link.click();
|
|
312
|
-
document.body.removeChild(link);
|
|
313
|
-
|
|
314
|
-
// Clean up object URL
|
|
315
|
-
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Clear all stored files
|
|
319
|
-
clear() {
|
|
320
|
-
// Revoke any object URLs that might be in use
|
|
321
|
-
this.files.forEach((fileData, resourceId) => {
|
|
322
|
-
const thumbnailUrl = this.getThumbnailUrl(resourceId);
|
|
323
|
-
if (thumbnailUrl) {
|
|
324
|
-
URL.revokeObjectURL(thumbnailUrl);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
this.files.clear();
|
|
329
|
-
this.resourceCounter = 0;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Initialize file storage
|
|
334
|
-
const fileStorage = new InMemoryFileStorage();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// DOM element references
|
|
338
|
-
const el = {
|
|
339
|
-
schemaInput: document.getElementById("schemaInput"),
|
|
340
|
-
schemaErrors: document.getElementById("schemaErrors"),
|
|
341
|
-
applySchemaBtn: document.getElementById("applySchemaBtn"),
|
|
342
|
-
resetSchemaBtn: document.getElementById("resetSchemaBtn"),
|
|
343
|
-
formatSchemaBtn: document.getElementById("formatSchemaBtn"),
|
|
344
|
-
readOnlyToggle: document.getElementById("readOnlyToggle"),
|
|
345
|
-
formContainer: document.getElementById("formContainer"),
|
|
346
|
-
formErrors: document.getElementById("formErrors"),
|
|
347
|
-
submitBtn: document.getElementById("submitBtn"),
|
|
348
|
-
clearFormBtn: document.getElementById("clearFormBtn"),
|
|
349
|
-
dataTextarea: document.getElementById("dataTextarea"),
|
|
350
|
-
prefillBtn: document.getElementById("prefillBtn"),
|
|
351
|
-
copyDataBtn: document.getElementById("copyDataBtn"),
|
|
352
|
-
downloadDataBtn: document.getElementById("downloadDataBtn"),
|
|
353
|
-
dataErrors: document.getElementById("dataErrors"),
|
|
354
|
-
actionsTextarea: document.getElementById("actionsTextarea"),
|
|
355
|
-
formatActionsBtn: document.getElementById("formatActionsBtn"),
|
|
356
|
-
clearActionsBtn: document.getElementById("clearActionsBtn"),
|
|
357
|
-
actionsErrors: document.getElementById("actionsErrors"),
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Utility functions
|
|
361
|
-
function pretty(obj) {
|
|
362
|
-
return FormBuilder.pretty(obj);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function showError(element, message) {
|
|
366
|
-
if (element) {
|
|
367
|
-
element.textContent = message;
|
|
368
|
-
element.classList.remove("hidden");
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function clearError(element) {
|
|
373
|
-
if (element) {
|
|
374
|
-
element.textContent = "";
|
|
375
|
-
element.classList.add("hidden");
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function downloadFile(filename, content) {
|
|
380
|
-
const blob = new Blob([content], { type: "text/plain" });
|
|
381
|
-
const url = URL.createObjectURL(blob);
|
|
382
|
-
const a = document.createElement("a");
|
|
383
|
-
a.href = url;
|
|
384
|
-
a.download = filename;
|
|
385
|
-
document.body.appendChild(a);
|
|
386
|
-
a.click();
|
|
387
|
-
document.body.removeChild(a);
|
|
388
|
-
URL.revokeObjectURL(url);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Parse and validate external actions
|
|
392
|
-
function parseActions(actionsText) {
|
|
393
|
-
if (!actionsText || actionsText.trim() === "") {
|
|
394
|
-
return [];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const actions = JSON.parse(actionsText);
|
|
399
|
-
|
|
400
|
-
if (!Array.isArray(actions)) {
|
|
401
|
-
throw new Error("Actions must be an array");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Validate each action
|
|
405
|
-
for (let i = 0; i < actions.length; i++) {
|
|
406
|
-
const action = actions[i];
|
|
407
|
-
if (!action || typeof action !== "object") {
|
|
408
|
-
throw new Error(`Action at index ${i} must be an object`);
|
|
409
|
-
}
|
|
410
|
-
if (!action.key || typeof action.key !== "string") {
|
|
411
|
-
throw new Error(
|
|
412
|
-
`Action at index ${i} missing valid 'key' property`,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
if (!action.value || typeof action.value !== "string") {
|
|
416
|
-
throw new Error(`Action at index ${i} missing valid 'value' property`);
|
|
417
|
-
}
|
|
418
|
-
// related_field is optional - if not provided, action is form-level
|
|
419
|
-
if (action.related_field && typeof action.related_field !== "string") {
|
|
420
|
-
throw new Error(`Action at index ${i} has invalid 'related_field' property type`);
|
|
421
|
-
}
|
|
422
|
-
// Label is optional - will be resolved from schema or key
|
|
423
|
-
if (action.label && typeof action.label !== "string") {
|
|
424
|
-
throw new Error(`Action at index ${i} has invalid 'label' property type`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return actions;
|
|
429
|
-
} catch (error) {
|
|
430
|
-
throw new Error(`Actions JSON error: ${error.message}`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Configure FormBuilder with in-memory handlers
|
|
435
|
-
function setupFormBuilder() {
|
|
436
|
-
// Set form container
|
|
437
|
-
FormBuilder.setFormRoot(el.formContainer);
|
|
438
|
-
|
|
439
|
-
// Upload handler - store file in memory and return resource ID
|
|
440
|
-
FormBuilder.setUploadHandler(async (file) => {
|
|
441
|
-
try {
|
|
442
|
-
console.log("Uploading file to memory:", file.name);
|
|
443
|
-
const resourceId = await fileStorage.storeFile(file);
|
|
444
|
-
console.log("File stored with resource ID:", resourceId);
|
|
445
|
-
return resourceId;
|
|
446
|
-
} catch (error) {
|
|
447
|
-
console.error("Upload failed:", error);
|
|
448
|
-
throw error;
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// Download handler - download file from memory
|
|
453
|
-
FormBuilder.setDownloadHandler((resourceId, fileName) => {
|
|
454
|
-
console.log("Downloading file from memory:", resourceId, fileName);
|
|
455
|
-
fileStorage.downloadFile(resourceId, fileName);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Thumbnail handler - get thumbnail URL for preview
|
|
459
|
-
FormBuilder.setThumbnailHandler((resourceId) => {
|
|
460
|
-
const thumbnailUrl = fileStorage.getThumbnailUrl(resourceId);
|
|
461
|
-
console.log("Getting thumbnail for:", resourceId, thumbnailUrl);
|
|
462
|
-
return thumbnailUrl;
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Action handler - display message when action button is clicked
|
|
466
|
-
// New system: value, key, related_field parameters
|
|
467
|
-
FormBuilder.setActionHandler((value, key, relatedField) => {
|
|
468
|
-
console.log("Action clicked:", {
|
|
469
|
-
value,
|
|
470
|
-
key,
|
|
471
|
-
relatedField,
|
|
472
|
-
type: relatedField ? "field-level" : "form-level",
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// Show message to user (compatible with all environments)
|
|
476
|
-
if (typeof window !== "undefined" && window.alert) {
|
|
477
|
-
if (relatedField) {
|
|
478
|
-
window.alert(
|
|
479
|
-
`Field Action: "${key}" clicked for field "${relatedField}" with value: ${value}`,
|
|
480
|
-
);
|
|
481
|
-
} else {
|
|
482
|
-
window.alert(`Form Action: "${key}" clicked with value: ${value}`);
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
console.log(
|
|
486
|
-
`Demo action: ${key} clicked with value: ${value}`,
|
|
487
|
-
relatedField ? ` for field: ${relatedField}` : " (form-level)",
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
console.log(
|
|
493
|
-
"FormBuilder configured with in-memory file handlers and action handler",
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Schema management functions
|
|
498
|
-
function applyCurrentSchema() {
|
|
499
|
-
clearError(el.schemaErrors);
|
|
500
|
-
clearError(el.formErrors);
|
|
501
|
-
clearError(el.actionsErrors);
|
|
502
|
-
|
|
503
|
-
try {
|
|
504
|
-
const schema = JSON.parse(el.schemaInput.value);
|
|
505
|
-
const errors = FormBuilder.validateSchema(schema);
|
|
506
|
-
|
|
507
|
-
if (errors.length > 0) {
|
|
508
|
-
showError(
|
|
509
|
-
el.schemaErrors,
|
|
510
|
-
`Schema validation errors: ${errors.join(", ")}`,
|
|
511
|
-
);
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Parse external actions
|
|
516
|
-
let externalActions = [];
|
|
517
|
-
try {
|
|
518
|
-
externalActions = parseActions(el.actionsTextarea.value);
|
|
519
|
-
} catch (error) {
|
|
520
|
-
showError(el.actionsErrors, error.message);
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// Set mode based on toggle
|
|
526
|
-
const isReadOnly = el.readOnlyToggle.checked;
|
|
527
|
-
FormBuilder.setMode(isReadOnly ? "readonly" : "edit");
|
|
528
|
-
|
|
529
|
-
// Get current data to preserve during re-render
|
|
530
|
-
let currentData = {};
|
|
531
|
-
try {
|
|
532
|
-
const formResult = FormBuilder.getFormData();
|
|
533
|
-
if (formResult.valid) {
|
|
534
|
-
currentData = formResult.data;
|
|
535
|
-
}
|
|
536
|
-
} catch {
|
|
537
|
-
// Ignore errors when getting current data
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Render form with current data and external actions
|
|
541
|
-
FormBuilder.renderForm(schema, currentData, externalActions);
|
|
542
|
-
|
|
543
|
-
console.log(`Form rendered in ${isReadOnly ? "readonly" : "edit"} mode`);
|
|
544
|
-
console.log(`External actions:`, externalActions);
|
|
545
|
-
|
|
546
|
-
return true;
|
|
547
|
-
} catch (e) {
|
|
548
|
-
showError(el.schemaErrors, `JSON parse error: ${e.message}`);
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Event handlers
|
|
554
|
-
el.applySchemaBtn.addEventListener("click", () => {
|
|
555
|
-
applyCurrentSchema();
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
el.resetSchemaBtn.addEventListener("click", () => {
|
|
559
|
-
el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
|
|
560
|
-
clearError(el.schemaErrors);
|
|
561
|
-
clearError(el.formErrors);
|
|
562
|
-
clearError(el.dataErrors);
|
|
563
|
-
clearError(el.actionsErrors);
|
|
564
|
-
el.dataTextarea.value = "";
|
|
565
|
-
el.actionsTextarea.value = "";
|
|
566
|
-
|
|
567
|
-
// Clear file storage
|
|
568
|
-
fileStorage.clear();
|
|
569
|
-
|
|
570
|
-
// Apply schema
|
|
571
|
-
applyCurrentSchema();
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
el.formatSchemaBtn.addEventListener("click", () => {
|
|
575
|
-
try {
|
|
576
|
-
const parsed = JSON.parse(el.schemaInput.value);
|
|
577
|
-
el.schemaInput.value = pretty(parsed);
|
|
578
|
-
clearError(el.schemaErrors);
|
|
579
|
-
} catch (e) {
|
|
580
|
-
showError(el.schemaErrors, `Format: JSON parse error: ${e.message}`);
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Read-only toggle handler
|
|
585
|
-
el.readOnlyToggle.addEventListener("change", () => {
|
|
586
|
-
applyCurrentSchema(); // Re-render form with new mode
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Form interaction handlers
|
|
590
|
-
el.submitBtn.addEventListener("click", () => {
|
|
591
|
-
clearError(el.formErrors);
|
|
592
|
-
clearError(el.dataErrors);
|
|
593
|
-
|
|
594
|
-
const result = FormBuilder.getFormData();
|
|
595
|
-
|
|
596
|
-
if (!result.valid) {
|
|
597
|
-
showError(el.formErrors, `Validation errors: ${result.errors.join(", ")}`);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// For demo purposes, we can show either raw data or enhanced data
|
|
602
|
-
// Let's provide both options - you can choose which one to use
|
|
603
|
-
|
|
604
|
-
// Option 1: Show raw data with just resource IDs (what you might want)
|
|
605
|
-
const rawData = JSON.parse(JSON.stringify(result.data));
|
|
606
|
-
|
|
607
|
-
// Option 2: Show enhanced data with file metadata (for demo visualization)
|
|
608
|
-
const enhancedData = JSON.parse(JSON.stringify(result.data));
|
|
609
|
-
|
|
610
|
-
function replaceResourceIds(obj) {
|
|
611
|
-
for (const key in obj) {
|
|
612
|
-
if (typeof obj[key] === "string" && obj[key].startsWith("res_")) {
|
|
613
|
-
const metadata = fileStorage.getMetadata(obj[key]);
|
|
614
|
-
if (metadata) {
|
|
615
|
-
obj[key] = {
|
|
616
|
-
resourceId: obj[key],
|
|
617
|
-
name: metadata.name,
|
|
618
|
-
type: metadata.type,
|
|
619
|
-
size: metadata.size,
|
|
620
|
-
uploadedAt: metadata.uploadedAt,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
} else if (Array.isArray(obj[key])) {
|
|
624
|
-
obj[key].forEach((item, index) => {
|
|
625
|
-
if (typeof item === "string" && item.startsWith("res_")) {
|
|
626
|
-
const metadata = fileStorage.getMetadata(item);
|
|
627
|
-
if (metadata) {
|
|
628
|
-
obj[key][index] = {
|
|
629
|
-
resourceId: item,
|
|
630
|
-
name: metadata.name,
|
|
631
|
-
type: metadata.type,
|
|
632
|
-
size: metadata.size,
|
|
633
|
-
uploadedAt: metadata.uploadedAt,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
} else if (typeof item === "object" && item !== null) {
|
|
637
|
-
replaceResourceIds(item);
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
641
|
-
replaceResourceIds(obj[key]);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
replaceResourceIds(enhancedData);
|
|
647
|
-
|
|
648
|
-
// Show raw data (just resource IDs) - change to enhancedData for metadata
|
|
649
|
-
el.dataTextarea.value = pretty(rawData);
|
|
650
|
-
console.log("Form submitted successfully");
|
|
651
|
-
console.log("Raw data:", rawData);
|
|
652
|
-
console.log("Enhanced data:", enhancedData);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
el.clearFormBtn.addEventListener("click", () => {
|
|
656
|
-
FormBuilder.clearForm();
|
|
657
|
-
clearError(el.formErrors);
|
|
658
|
-
console.log("Form values cleared");
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// Data management handlers
|
|
662
|
-
el.prefillBtn.addEventListener("click", () => {
|
|
663
|
-
clearError(el.dataErrors);
|
|
664
|
-
clearError(el.formErrors);
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
const prefillData = JSON.parse(el.dataTextarea.value || "{}");
|
|
668
|
-
const currentSchema = JSON.parse(el.schemaInput.value);
|
|
669
|
-
|
|
670
|
-
// Parse external actions
|
|
671
|
-
let externalActions = [];
|
|
672
|
-
try {
|
|
673
|
-
externalActions = parseActions(el.actionsTextarea.value);
|
|
674
|
-
} catch (error) {
|
|
675
|
-
showError(el.actionsErrors, error.message);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Convert enhanced data back to raw format for prefilling
|
|
680
|
-
const processedData = JSON.parse(JSON.stringify(prefillData));
|
|
681
|
-
|
|
682
|
-
function extractResourceIds(obj) {
|
|
683
|
-
for (const key in obj) {
|
|
684
|
-
// Handle enhanced file objects - extract just the resourceId
|
|
685
|
-
if (
|
|
686
|
-
obj[key] &&
|
|
687
|
-
typeof obj[key] === "object" &&
|
|
688
|
-
obj[key].resourceId &&
|
|
689
|
-
typeof obj[key].resourceId === "string" &&
|
|
690
|
-
obj[key].resourceId.startsWith("res_")
|
|
691
|
-
) {
|
|
692
|
-
obj[key] = obj[key].resourceId;
|
|
693
|
-
} else if (Array.isArray(obj[key])) {
|
|
694
|
-
obj[key].forEach((item, index) => {
|
|
695
|
-
if (
|
|
696
|
-
item &&
|
|
697
|
-
typeof item === "object" &&
|
|
698
|
-
item.resourceId &&
|
|
699
|
-
typeof item.resourceId === "string" &&
|
|
700
|
-
item.resourceId.startsWith("res_")
|
|
701
|
-
) {
|
|
702
|
-
obj[key][index] = item.resourceId;
|
|
703
|
-
} else if (typeof item === "object" && item !== null) {
|
|
704
|
-
extractResourceIds(item);
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
708
|
-
extractResourceIds(obj[key]);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
extractResourceIds(processedData);
|
|
714
|
-
|
|
715
|
-
// Set mode based on toggle
|
|
716
|
-
const isReadOnly = el.readOnlyToggle.checked;
|
|
717
|
-
FormBuilder.setMode(isReadOnly ? "readonly" : "edit");
|
|
718
|
-
|
|
719
|
-
FormBuilder.renderForm(currentSchema, processedData, externalActions);
|
|
720
|
-
console.log("Form prefilled with data");
|
|
721
|
-
console.log("Processed prefill data:", processedData);
|
|
722
|
-
console.log("External actions:", externalActions);
|
|
723
|
-
} catch (e) {
|
|
724
|
-
showError(el.dataErrors, `JSON parse error: ${e.message}`);
|
|
725
|
-
console.error("Prefill error:", e);
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
el.copyDataBtn.addEventListener("click", async () => {
|
|
730
|
-
try {
|
|
731
|
-
await navigator.clipboard.writeText(el.dataTextarea.value || "");
|
|
732
|
-
el.copyDataBtn.textContent = "Copied!";
|
|
733
|
-
setTimeout(() => {
|
|
734
|
-
el.copyDataBtn.textContent = "Copy JSON";
|
|
735
|
-
}, 1000);
|
|
736
|
-
console.log("Data copied to clipboard");
|
|
737
|
-
} catch (e) {
|
|
738
|
-
console.warn("Copy failed:", e);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
el.downloadDataBtn.addEventListener("click", () => {
|
|
743
|
-
downloadFile("form-data.json", el.dataTextarea.value || "{}");
|
|
744
|
-
console.log("Data downloaded");
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// Actions management handlers
|
|
748
|
-
el.formatActionsBtn.addEventListener("click", () => {
|
|
749
|
-
try {
|
|
750
|
-
const actions = parseActions(el.actionsTextarea.value);
|
|
751
|
-
el.actionsTextarea.value = pretty(actions);
|
|
752
|
-
clearError(el.actionsErrors);
|
|
753
|
-
console.log("Actions formatted");
|
|
754
|
-
} catch (e) {
|
|
755
|
-
showError(el.actionsErrors, e.message);
|
|
756
|
-
console.error("Format actions error:", e);
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
el.clearActionsBtn.addEventListener("click", () => {
|
|
761
|
-
el.actionsTextarea.value = "";
|
|
762
|
-
clearError(el.actionsErrors);
|
|
763
|
-
// Re-apply schema to remove external actions from the form
|
|
764
|
-
applyCurrentSchema();
|
|
765
|
-
console.log("Actions cleared");
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// Example external actions for demonstration
|
|
769
|
-
const EXAMPLE_ACTIONS = [
|
|
770
|
-
// Field-level actions using predefined labels from schema
|
|
771
|
-
{
|
|
772
|
-
related_field: "cover",
|
|
773
|
-
key: "retry",
|
|
774
|
-
value: "regenerate_cover_image", // Specific action value for handler
|
|
775
|
-
},
|
|
776
|
-
{
|
|
777
|
-
related_field: "cover",
|
|
778
|
-
key: "enhance",
|
|
779
|
-
value: "enhance_cover_quality", // Specific action value for handler
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
related_field: "cover",
|
|
783
|
-
key: "crop",
|
|
784
|
-
value: "auto_crop_cover", // Specific action value for handler
|
|
785
|
-
},
|
|
786
|
-
// Field-level actions with custom labels
|
|
787
|
-
{
|
|
788
|
-
related_field: "title[0]",
|
|
789
|
-
key: "generate",
|
|
790
|
-
value: "ai_generate_title",
|
|
791
|
-
label: "🤖 Generate Title", // Custom label overrides schema
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
related_field: "description",
|
|
795
|
-
key: "improve",
|
|
796
|
-
value: "ai_improve_description", // Key will be used as fallback label
|
|
797
|
-
},
|
|
798
|
-
{
|
|
799
|
-
related_field: "slides[0].title",
|
|
800
|
-
key: "optimize",
|
|
801
|
-
value: "ai_optimize_slide_title",
|
|
802
|
-
label: "🎯 Optimize Slide Title", // Custom label
|
|
803
|
-
},
|
|
804
|
-
// Form-level actions (no related_field)
|
|
805
|
-
{
|
|
806
|
-
key: "save-draft",
|
|
807
|
-
value: "save_form_draft",
|
|
808
|
-
label: "💾 Save Draft",
|
|
809
|
-
},
|
|
810
|
-
// Action with missing related field (should appear at bottom of form)
|
|
811
|
-
{
|
|
812
|
-
related_field: "non_existent_field",
|
|
813
|
-
key: "test-missing",
|
|
814
|
-
value: "test_missing_field_action",
|
|
815
|
-
label: "🔍 Test Missing Field Action",
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
key: "preview",
|
|
819
|
-
value: "preview_infographic",
|
|
820
|
-
label: "👁️ Preview",
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
key: "export",
|
|
824
|
-
value: "export_json_data",
|
|
825
|
-
label: "📄 Export JSON",
|
|
826
|
-
},
|
|
827
|
-
];
|
|
828
|
-
|
|
829
|
-
// Initialize demo application
|
|
830
|
-
function initDemo() {
|
|
831
|
-
// Set up FormBuilder
|
|
832
|
-
setupFormBuilder();
|
|
833
|
-
|
|
834
|
-
// Initialize with example schema
|
|
835
|
-
el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
|
|
836
|
-
|
|
837
|
-
// Initialize with example actions
|
|
838
|
-
el.actionsTextarea.value = pretty(EXAMPLE_ACTIONS);
|
|
839
|
-
|
|
840
|
-
// Apply initial schema
|
|
841
|
-
applyCurrentSchema();
|
|
842
|
-
|
|
843
|
-
console.log("Demo initialized successfully");
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Start the demo when the page loads
|
|
847
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
848
|
-
// Wait for FormBuilder to be available
|
|
849
|
-
if (typeof FormBuilder !== "undefined") {
|
|
850
|
-
initDemo();
|
|
851
|
-
} else {
|
|
852
|
-
// Fallback: wait a bit for scripts to load
|
|
853
|
-
setTimeout(() => {
|
|
854
|
-
if (typeof FormBuilder !== "undefined") {
|
|
855
|
-
initDemo();
|
|
856
|
-
} else {
|
|
857
|
-
console.error("FormBuilder not found!");
|
|
858
|
-
}
|
|
859
|
-
}, 100);
|
|
860
|
-
}
|
|
861
|
-
});
|