@dmitryvim/form-builder 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/demo.js ADDED
@@ -0,0 +1,574 @@
1
+ // Demo Application for Form Builder
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
+ "elements": [
9
+ {
10
+ "type": "file",
11
+ "key": "cover",
12
+ "label": "Изображение товара",
13
+ "description": "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.",
14
+ "required": true,
15
+ "accept": {
16
+ "extensions": ["png", "jpg", "gif"],
17
+ "mime": ["image/png", "image/jpeg", "image/gif"]
18
+ },
19
+ "maxSizeMB": 10
20
+ },
21
+ {
22
+ "type": "files",
23
+ "key": "assets",
24
+ "label": "Другие изображения",
25
+ "description": "Additional images that provide context, show different angles, or demonstrate product usage. These will be used to enhance the infographic composition.",
26
+ "required": false,
27
+ "accept": {
28
+ "extensions": ["png", "jpg", "gif"],
29
+ "mime": ["image/png", "image/jpeg", "image/gif"]
30
+ },
31
+ "minCount": 0,
32
+ "maxCount": 10,
33
+ "maxSizeMB": 5
34
+ },
35
+ {
36
+ "type": "text",
37
+ "key": "title",
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
+ ]
119
+ };
120
+
121
+ // DOM element references for demo app
122
+ const el = {
123
+ schemaInput: document.getElementById('schemaInput'),
124
+ schemaErrors: document.getElementById('schemaErrors'),
125
+ applySchemaBtn: document.getElementById('applySchemaBtn'),
126
+ resetSchemaBtn: document.getElementById('resetSchemaBtn'),
127
+ prettySchemaBtn: document.getElementById('prettySchemaBtn'),
128
+ downloadSchemaBtn: document.getElementById('downloadSchemaBtn'),
129
+ formContainer: document.getElementById('formContainer'),
130
+ formErrors: document.getElementById('formErrors'),
131
+ submitBtn: document.getElementById('submitBtn'),
132
+ saveDraftBtn: document.getElementById('saveDraftBtn'),
133
+ clearFormBtn: document.getElementById('clearFormBtn'),
134
+ outputJson: document.getElementById('outputJson'),
135
+ copyOutputBtn: document.getElementById('copyOutputBtn'),
136
+ downloadOutputBtn: document.getElementById('downloadOutputBtn'),
137
+ shareUrlBtn: document.getElementById('shareUrlBtn'),
138
+ prefillInput: document.getElementById('prefillInput'),
139
+ loadPrefillBtn: document.getElementById('loadPrefillBtn'),
140
+ copyTemplateBtn: document.getElementById('copyTemplateBtn'),
141
+ prefillErrors: document.getElementById('prefillErrors'),
142
+ urlInfo: document.getElementById('urlInfo'),
143
+ // Readonly demo elements
144
+ readonlySchemaInput: document.getElementById('readonlySchemaInput'),
145
+ applyReadonlyBtn: document.getElementById('applyReadonlyBtn'),
146
+ clearReadonlyBtn: document.getElementById('clearReadonlyBtn'),
147
+ readonlyErrors: document.getElementById('readonlyErrors'),
148
+ readonlyDemoContainer: document.getElementById('readonlyDemoContainer')
149
+ };
150
+
151
+ // Utility functions (using FormBuilder.pretty)
152
+ function pretty(obj) {
153
+ return FormBuilder.pretty(obj);
154
+ }
155
+
156
+ function showError(element, message) {
157
+ if (element) {
158
+ element.textContent = message;
159
+ element.classList.remove('hidden');
160
+ }
161
+ }
162
+
163
+ function clearError(element) {
164
+ if (element) {
165
+ element.textContent = '';
166
+ element.classList.add('hidden');
167
+ }
168
+ }
169
+
170
+ function downloadFile(filename, content) {
171
+ const blob = new Blob([content], { type: 'text/plain' });
172
+ const url = URL.createObjectURL(blob);
173
+ const a = document.createElement('a');
174
+ a.href = url;
175
+ a.download = filename;
176
+ a.click();
177
+ URL.revokeObjectURL(url);
178
+ }
179
+
180
+ // Demo event handlers
181
+ el.applySchemaBtn.addEventListener('click', () => {
182
+ clearError(el.schemaErrors);
183
+ try {
184
+ const schema = JSON.parse(el.schemaInput.value);
185
+ const errors = FormBuilder.validateSchema(schema);
186
+
187
+ if (errors.length > 0) {
188
+ showError(el.schemaErrors, 'Schema validation errors: ' + errors.join(', '));
189
+ return;
190
+ }
191
+
192
+ FormBuilder.renderForm(schema, {});
193
+ el.outputJson.value = '';
194
+ clearError(el.formErrors);
195
+ } catch (e) {
196
+ showError(el.schemaErrors, 'JSON parse error: ' + e.message);
197
+ }
198
+ });
199
+
200
+ el.resetSchemaBtn.addEventListener('click', () => {
201
+ el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
202
+ clearError(el.schemaErrors);
203
+ FormBuilder.renderForm(EXAMPLE_SCHEMA, {});
204
+ el.outputJson.value = '';
205
+ el.prefillInput.value = '';
206
+ clearError(el.prefillErrors);
207
+ });
208
+
209
+ el.prettySchemaBtn.addEventListener('click', () => {
210
+ try {
211
+ const parsed = JSON.parse(el.schemaInput.value);
212
+ el.schemaInput.value = pretty(parsed);
213
+ } catch (e) {
214
+ showError(el.schemaErrors, 'Prettify: JSON parse error: ' + e.message);
215
+ }
216
+ });
217
+
218
+ el.downloadSchemaBtn.addEventListener('click', () => {
219
+ downloadFile('schema.json', el.schemaInput.value || pretty(EXAMPLE_SCHEMA));
220
+ });
221
+
222
+ el.clearFormBtn.addEventListener('click', () => {
223
+ FormBuilder.clearForm();
224
+ clearError(el.formErrors);
225
+ });
226
+
227
+ el.copyOutputBtn.addEventListener('click', async () => {
228
+ try {
229
+ await navigator.clipboard.writeText(el.outputJson.value || '');
230
+ el.copyOutputBtn.textContent = 'Copied!';
231
+ setTimeout(() => {
232
+ el.copyOutputBtn.textContent = 'Copy JSON';
233
+ }, 1000);
234
+ } catch (e) {
235
+ console.warn('Copy failed:', e);
236
+ }
237
+ });
238
+
239
+ el.downloadOutputBtn.addEventListener('click', () => {
240
+ downloadFile('form-data.json', el.outputJson.value || '{}');
241
+ });
242
+
243
+ el.shareUrlBtn.addEventListener('click', () => {
244
+ try {
245
+ const schema = JSON.parse(el.schemaInput.value);
246
+ const schemaBase64 = btoa(JSON.stringify(schema));
247
+ const url = `${window.location.origin}${window.location.pathname}?schema=${schemaBase64}`;
248
+ navigator.clipboard.writeText(url);
249
+ el.shareUrlBtn.textContent = 'URL Copied!';
250
+ setTimeout(() => {
251
+ el.shareUrlBtn.textContent = 'Share URL';
252
+ }, 2000);
253
+ } catch (e) {
254
+ alert('Please apply a valid schema first');
255
+ }
256
+ });
257
+
258
+ el.loadPrefillBtn.addEventListener('click', () => {
259
+ clearError(el.prefillErrors);
260
+ try {
261
+ const pre = JSON.parse(el.prefillInput.value || '{}');
262
+ const currentSchema = JSON.parse(el.schemaInput.value);
263
+ FormBuilder.renderForm(currentSchema, pre);
264
+ el.outputJson.value = '';
265
+ clearError(el.formErrors);
266
+ } catch (e) {
267
+ showError(el.prefillErrors, 'JSON parse error: ' + e.message);
268
+ }
269
+ });
270
+
271
+ el.copyTemplateBtn.addEventListener('click', () => {
272
+ try {
273
+ const schema = JSON.parse(el.schemaInput.value);
274
+ const template = {};
275
+
276
+ function processElements(elements, target) {
277
+ elements.forEach(element => {
278
+ if (element.type === 'group' && element.repeat) {
279
+ target[element.key] = [{}];
280
+ if (element.elements) {
281
+ processElements(element.elements, target[element.key][0]);
282
+ }
283
+ } else if (element.type === 'group') {
284
+ target[element.key] = {};
285
+ if (element.elements) {
286
+ processElements(element.elements, target[element.key]);
287
+ }
288
+ } else {
289
+ target[element.key] = element.default || null;
290
+ }
291
+ });
292
+ }
293
+
294
+ processElements(schema.elements, template);
295
+ el.prefillInput.value = pretty(template);
296
+ } catch (e) {
297
+ showError(el.prefillErrors, 'Schema parse error: ' + e.message);
298
+ }
299
+ });
300
+
301
+ // Form submission handlers
302
+ function submitFormEnhanced() {
303
+ clearError(el.formErrors);
304
+
305
+ const result = FormBuilder.getFormData();
306
+
307
+ if (!result.valid) {
308
+ showError(el.formErrors, 'Validation errors: ' + result.errors.join(', '));
309
+ return;
310
+ }
311
+
312
+ // Convert resource IDs to file info for demo
313
+ const processedData = JSON.parse(JSON.stringify(result.data));
314
+
315
+ function replaceResourceIds(obj) {
316
+ for (const key in obj) {
317
+ if (typeof obj[key] === 'string' && obj[key].startsWith('res_')) {
318
+ const meta = FormBuilder.state.resourceIndex.get(obj[key]);
319
+ if (meta) {
320
+ obj[key] = {
321
+ resourceId: obj[key],
322
+ name: meta.name,
323
+ type: meta.type,
324
+ size: meta.size
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
+ }
347
+ }
348
+ }
349
+
350
+ replaceResourceIds(processedData);
351
+
352
+ el.outputJson.value = pretty(processedData);
353
+ }
354
+
355
+ el.submitBtn.addEventListener('click', submitFormEnhanced);
356
+ el.saveDraftBtn.addEventListener('click', () => {
357
+ const result = FormBuilder.getFormData();
358
+ el.outputJson.value = pretty(result.data);
359
+ });
360
+
361
+ // Readonly demo handlers
362
+ el.applyReadonlyBtn.addEventListener('click', () => {
363
+ clearError(el.readonlyErrors);
364
+ try {
365
+ const schema = JSON.parse(el.readonlySchemaInput.value);
366
+ const errors = FormBuilder.validateSchema(schema);
367
+
368
+ if (errors.length > 0) {
369
+ showError(el.readonlyErrors, 'Schema validation errors: ' + errors.join(', '));
370
+ return;
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);
393
+ }
394
+ });
395
+
396
+ el.clearReadonlyBtn.addEventListener('click', () => {
397
+ el.readonlyDemoContainer.innerHTML = '<div class="text-center text-gray-500 py-8">Apply a schema to see readonly mode</div>';
398
+ clearError(el.readonlyErrors);
399
+ });
400
+
401
+ // URL schema loading
402
+ function loadSchemaFromURL() {
403
+ const urlParams = new URLSearchParams(window.location.search);
404
+ const schemaParam = urlParams.get('schema');
405
+
406
+ if (schemaParam) {
407
+ try {
408
+ const schema = JSON.parse(atob(schemaParam));
409
+ el.schemaInput.value = pretty(schema);
410
+ FormBuilder.renderForm(schema, {});
411
+ el.urlInfo.classList.remove('hidden');
412
+ } catch (e) {
413
+ console.warn('Failed to load schema from URL:', e);
414
+ }
415
+ }
416
+ }
417
+
418
+ // Read-only demo functions
419
+ function downloadDemoFile(filePath, fileName) {
420
+ console.log('downloadDemoFile called with:', filePath, fileName);
421
+
422
+ // Check if we're running on a local dev server
423
+ const isLocalDev = window.location.protocol === 'http:' && window.location.hostname === 'localhost';
424
+
425
+ if (isLocalDev) {
426
+ console.log('Running on local dev server, using fetch method');
427
+ // Force download by fetching the file and creating a blob
428
+ fetch(filePath)
429
+ .then(response => {
430
+ console.log('Fetch response:', response.ok, response.status);
431
+ return response.blob();
432
+ })
433
+ .then(blob => {
434
+ console.log('Creating download link for blob:', blob.size, 'bytes');
435
+ const link = document.createElement('a');
436
+ link.href = URL.createObjectURL(blob);
437
+ link.download = fileName;
438
+ document.body.appendChild(link);
439
+ link.click();
440
+ document.body.removeChild(link);
441
+ URL.revokeObjectURL(link.href);
442
+ console.log('Download completed successfully');
443
+ })
444
+ .catch(error => {
445
+ console.error('Fetch download failed:', error);
446
+ simpleLinkDownload(filePath, fileName);
447
+ });
448
+ } else {
449
+ console.log('Not on local dev server, using simple link method');
450
+ simpleLinkDownload(filePath, fileName);
451
+ }
452
+ }
453
+
454
+ function simpleLinkDownload(filePath, fileName) {
455
+ console.log('Using simple link download:', filePath, fileName);
456
+ const link = document.createElement('a');
457
+ link.href = filePath;
458
+ link.download = fileName;
459
+ link.style.display = 'none';
460
+ document.body.appendChild(link);
461
+ link.click();
462
+ document.body.removeChild(link);
463
+ console.log('Simple link download triggered');
464
+ }
465
+
466
+ // Configure download handler for readonly demo
467
+ function setupReadonlyDemo() {
468
+ // Set up download handler that works with local demo files
469
+ FormBuilder.setDownloadHandler((resourceId, fileName) => {
470
+ const demoFileMap = {
471
+ 'demo_infographic': 'images/infographic_draft.jpg',
472
+ 'demo_video': 'images/final_video.mp4'
473
+ };
474
+
475
+ const filePath = demoFileMap[resourceId] || `images/${fileName}`;
476
+ downloadDemoFile(filePath, fileName);
477
+ });
478
+
479
+ // Set up thumbnail handler for demo (returns URL directly, not Promise)
480
+ FormBuilder.setThumbnailHandler((resourceId) => {
481
+ const demoFileMap = {
482
+ 'demo_infographic': 'images/infographic_draft.jpg',
483
+ 'demo_video': 'images/final_video.mp4'
484
+ };
485
+
486
+ return demoFileMap[resourceId] || null;
487
+ });
488
+
489
+ // Pre-populate demo resource metadata
490
+ FormBuilder.state.resourceIndex.set('demo_infographic', {
491
+ name: 'infographic_result.jpg',
492
+ type: 'image/jpeg',
493
+ size: 150000,
494
+ file: null
495
+ });
496
+
497
+ FormBuilder.state.resourceIndex.set('demo_video', {
498
+ name: 'final_video.mp4',
499
+ type: 'video/mp4',
500
+ size: 2500000,
501
+ file: null
502
+ });
503
+ }
504
+
505
+ // Default readonly schema for demo
506
+ const DEFAULT_READONLY_SCHEMA = {
507
+ "version": "0.3",
508
+ "title": "Результаты работы",
509
+ "elements": [
510
+ {
511
+ "type": "file",
512
+ "key": "result_image",
513
+ "label": "Изображение результата",
514
+ "description": "Готовая инфографика на основе загруженных данных"
515
+ },
516
+ {
517
+ "type": "file",
518
+ "key": "result_video",
519
+ "label": "Видео результат",
520
+ "description": "Финальное видео презентации"
521
+ }
522
+ ]
523
+ };
524
+
525
+ // Default readonly data
526
+ const DEFAULT_READONLY_DATA = {
527
+ "result_image": "demo_infographic",
528
+ "result_video": "demo_video"
529
+ };
530
+
531
+ function renderReadonlyDemo() {
532
+ // Set default schema in textarea
533
+ const textarea = document.getElementById('readonlySchemaInput');
534
+ if (textarea && !textarea.value.trim()) {
535
+ textarea.value = pretty(DEFAULT_READONLY_SCHEMA);
536
+ }
537
+ }
538
+
539
+ // Initialize demo application
540
+ function initDemo() {
541
+ // Set up the form builder for main form
542
+ FormBuilder.setFormRoot(el.formContainer);
543
+
544
+ // Set up handlers for readonly demo
545
+ setupReadonlyDemo();
546
+
547
+ // Initialize with example schema
548
+ el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
549
+ FormBuilder.setMode('edit'); // Ensure main form is in edit mode
550
+ FormBuilder.renderForm(EXAMPLE_SCHEMA, {});
551
+
552
+ // Load schema from URL if present
553
+ loadSchemaFromURL();
554
+
555
+ // Initialize read-only demo
556
+ setTimeout(renderReadonlyDemo, 500);
557
+ }
558
+
559
+ // Start the demo when the page loads
560
+ document.addEventListener('DOMContentLoaded', () => {
561
+ // Wait for FormBuilder to be available
562
+ if (typeof FormBuilder !== 'undefined') {
563
+ initDemo();
564
+ } else {
565
+ // Fallback: wait a bit for scripts to load
566
+ setTimeout(() => {
567
+ if (typeof FormBuilder !== 'undefined') {
568
+ initDemo();
569
+ } else {
570
+ console.error('FormBuilder not found!');
571
+ }
572
+ }, 100);
573
+ }
574
+ });