@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-file-preview-6b0a6e-mdmztpfy → 2.1.0-file-preview-e551b4-mdn5udxn
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/package.json +1 -1
- package/resources/ui-dynamic-form.umd.js +83 -54
- package/ui/components/UIDynamicForm.vue +441 -227
|
@@ -93,48 +93,6 @@
|
|
|
93
93
|
"
|
|
94
94
|
v-model="formData[field.id]"
|
|
95
95
|
/>
|
|
96
|
-
<!-- <component
|
|
97
|
-
:is="getFieldComponent(field).type"
|
|
98
|
-
v-if="getFieldComponent(field).innerText"
|
|
99
|
-
v-bind="getFieldComponent(field).props"
|
|
100
|
-
:ref="
|
|
101
|
-
(el) => {
|
|
102
|
-
if (index === 0) firstFormFieldRef = el;
|
|
103
|
-
}
|
|
104
|
-
"
|
|
105
|
-
v-model="formData[field.id]"
|
|
106
|
-
>
|
|
107
|
-
{{ getFieldComponent(field).innerText }}
|
|
108
|
-
</component>
|
|
109
|
-
<div v-else-if="getFieldComponent(field).type == 'v-slider'">
|
|
110
|
-
<p class="formkit-label">{{ field.label }}</p>
|
|
111
|
-
<component
|
|
112
|
-
:is="getFieldComponent(field).type"
|
|
113
|
-
v-bind="getFieldComponent(field).props"
|
|
114
|
-
:ref="
|
|
115
|
-
(el) => {
|
|
116
|
-
if (index === 0) firstFormFieldRef = el;
|
|
117
|
-
}
|
|
118
|
-
"
|
|
119
|
-
v-model="field.defaultValue"
|
|
120
|
-
/>
|
|
121
|
-
<p class="formkit-help">
|
|
122
|
-
{{
|
|
123
|
-
field.customForm ? JSON.parse(field.customForm).hint : undefined
|
|
124
|
-
}}
|
|
125
|
-
</p>
|
|
126
|
-
</div>
|
|
127
|
-
<component
|
|
128
|
-
:is="getFieldComponent(field).type"
|
|
129
|
-
v-else
|
|
130
|
-
v-bind="getFieldComponent(field).props"
|
|
131
|
-
:ref="
|
|
132
|
-
(el) => {
|
|
133
|
-
if (index === 0) firstFormFieldRef = el;
|
|
134
|
-
}
|
|
135
|
-
"
|
|
136
|
-
v-model="formData[field.id]"
|
|
137
|
-
/> -->
|
|
138
96
|
</v-col>
|
|
139
97
|
</v-row>
|
|
140
98
|
</FormKit>
|
|
@@ -181,8 +139,6 @@ import UIDynamicFormFooterAction from './FooterActions.vue';
|
|
|
181
139
|
import UIDynamicFormTitleText from './TitleText.vue';
|
|
182
140
|
|
|
183
141
|
function requiredIf({ value }, [targetField, expectedValue], node) {
|
|
184
|
-
console.debug(arguments);
|
|
185
|
-
|
|
186
142
|
const actual = node?.root?.value?.[targetField];
|
|
187
143
|
const isEmpty = value === '' || value === null || value === undefined;
|
|
188
144
|
|
|
@@ -243,9 +199,6 @@ export default {
|
|
|
243
199
|
},
|
|
244
200
|
},
|
|
245
201
|
setup(props) {
|
|
246
|
-
console.info('UIDynamicForm setup with:', props);
|
|
247
|
-
console.debug('Vue function loaded correctly', markRaw);
|
|
248
|
-
|
|
249
202
|
const instance = getCurrentInstance();
|
|
250
203
|
const app = instance.appContext.app;
|
|
251
204
|
|
|
@@ -253,7 +206,6 @@ export default {
|
|
|
253
206
|
theme: 'genesis',
|
|
254
207
|
locales: { de },
|
|
255
208
|
locale: 'de',
|
|
256
|
-
// eslint-disable-next-line object-shorthand
|
|
257
209
|
rules: { requiredIf: requiredIf },
|
|
258
210
|
});
|
|
259
211
|
app.use(plugin, formkitConfig);
|
|
@@ -269,7 +221,8 @@ export default {
|
|
|
269
221
|
msg: null,
|
|
270
222
|
collapsed: false,
|
|
271
223
|
firstFormFieldRef: null,
|
|
272
|
-
|
|
224
|
+
intersectionObserver: null,
|
|
225
|
+
visibleFileFields: new Set(),
|
|
273
226
|
};
|
|
274
227
|
},
|
|
275
228
|
computed: {
|
|
@@ -290,7 +243,7 @@ export default {
|
|
|
290
243
|
);
|
|
291
244
|
},
|
|
292
245
|
isConfirmDialog() {
|
|
293
|
-
return this.userTask
|
|
246
|
+
return this.userTask?.userTaskConfig?.formFields?.some((field) => field.type === 'confirm') || false;
|
|
294
247
|
},
|
|
295
248
|
effectiveTitle() {
|
|
296
249
|
if (this.props.title_text_type === 'str') {
|
|
@@ -301,6 +254,29 @@ export default {
|
|
|
301
254
|
return '';
|
|
302
255
|
}
|
|
303
256
|
},
|
|
257
|
+
// Optimized computed property for field components
|
|
258
|
+
fieldComponents() {
|
|
259
|
+
if (!this.userTask?.userTaskConfig?.formFields) {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const components = {};
|
|
264
|
+
const aFields = this.userTask.userTaskConfig.formFields;
|
|
265
|
+
|
|
266
|
+
aFields.forEach((field) => {
|
|
267
|
+
components[field.id] = this.createComponent(field);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return components;
|
|
271
|
+
},
|
|
272
|
+
// Optimized computed property for fields
|
|
273
|
+
computedFields() {
|
|
274
|
+
const aFields = this.userTask?.userTaskConfig?.formFields ?? [];
|
|
275
|
+
return aFields.map((field) => ({
|
|
276
|
+
...field,
|
|
277
|
+
items: mapItems(field.type, field),
|
|
278
|
+
}));
|
|
279
|
+
},
|
|
304
280
|
},
|
|
305
281
|
watch: {
|
|
306
282
|
formData: {
|
|
@@ -354,6 +330,9 @@ export default {
|
|
|
354
330
|
element.classList.add('test');
|
|
355
331
|
});
|
|
356
332
|
|
|
333
|
+
// Initialize Intersection Observer for lazy loading
|
|
334
|
+
this.initLazyLoading();
|
|
335
|
+
|
|
357
336
|
this.$socket.on('widget-load:' + this.id, (msg) => {
|
|
358
337
|
this.init(msg);
|
|
359
338
|
});
|
|
@@ -368,114 +347,161 @@ export default {
|
|
|
368
347
|
/* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
|
|
369
348
|
this.$socket?.off('widget-load' + this.id);
|
|
370
349
|
this.$socket?.off('msg-input:' + this.id);
|
|
350
|
+
|
|
351
|
+
// Clean up Intersection Observer
|
|
352
|
+
if (this.intersectionObserver) {
|
|
353
|
+
this.intersectionObserver.disconnect();
|
|
354
|
+
}
|
|
371
355
|
},
|
|
372
356
|
methods: {
|
|
373
|
-
//
|
|
357
|
+
// Simplified component getter - now just returns from computed cache
|
|
374
358
|
getFieldComponent(field) {
|
|
375
|
-
|
|
376
|
-
this.theme
|
|
377
|
-
}`;
|
|
378
|
-
|
|
379
|
-
if (this.componentCache.has(cacheKey)) {
|
|
380
|
-
return this.componentCache.get(cacheKey);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const component = this.createComponent(field);
|
|
384
|
-
this.componentCache.set(cacheKey, component);
|
|
385
|
-
return component;
|
|
359
|
+
return this.fieldComponents[field.id] || this.createComponent(field);
|
|
386
360
|
},
|
|
387
361
|
|
|
388
362
|
// Clear cache when form data changes
|
|
389
363
|
clearComponentCache() {
|
|
390
|
-
|
|
364
|
+
// This is now handled by computed properties automatically
|
|
391
365
|
},
|
|
392
366
|
|
|
393
367
|
createComponent(field) {
|
|
394
|
-
console.debug('Creating component for field:', field);
|
|
395
368
|
const customForm = field.customForm ? JSON.parse(field.customForm) : {};
|
|
396
|
-
const hint = customForm
|
|
397
|
-
const placeholder = customForm.placeholder;
|
|
398
|
-
const validation = customForm.validation;
|
|
369
|
+
const { hint, placeholder, validation, customProperties = [] } = customForm;
|
|
399
370
|
const name = field.id;
|
|
400
|
-
const customProperties = customForm.customProperties ?? [];
|
|
401
371
|
const isReadOnly =
|
|
402
372
|
this.props.readonly ||
|
|
403
373
|
this.formIsFinished ||
|
|
404
|
-
customProperties.
|
|
374
|
+
customProperties.some(
|
|
405
375
|
(entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'
|
|
406
376
|
)
|
|
407
377
|
? 'true'
|
|
408
378
|
: undefined;
|
|
379
|
+
|
|
380
|
+
const commonFormKitProps = {
|
|
381
|
+
id: field.id,
|
|
382
|
+
name,
|
|
383
|
+
label: field.label,
|
|
384
|
+
required: field.required,
|
|
385
|
+
value: this.formData[field.id],
|
|
386
|
+
help: hint,
|
|
387
|
+
wrapperClass: '$remove:formkit-wrapper',
|
|
388
|
+
labelClass: 'ui-dynamic-form-input-label',
|
|
389
|
+
inputClass: `input-${this.theme}`,
|
|
390
|
+
innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
|
|
391
|
+
readonly: isReadOnly,
|
|
392
|
+
validation,
|
|
393
|
+
validationVisibility: 'live',
|
|
394
|
+
};
|
|
395
|
+
|
|
409
396
|
switch (field.type) {
|
|
410
397
|
case 'long':
|
|
411
398
|
return {
|
|
412
399
|
type: 'FormKit',
|
|
413
400
|
props: {
|
|
401
|
+
...commonFormKitProps,
|
|
414
402
|
type: 'number',
|
|
415
|
-
id: field.id,
|
|
416
|
-
name,
|
|
417
|
-
label: field.label,
|
|
418
|
-
required: field.required,
|
|
419
|
-
value: this.formData[field.id],
|
|
420
403
|
number: 'integer',
|
|
421
404
|
min: 0,
|
|
422
405
|
validation: validation ? `${validation}|number` : 'number',
|
|
423
|
-
help: hint,
|
|
424
|
-
wrapperClass: '$remove:formkit-wrapper',
|
|
425
|
-
labelClass: 'ui-dynamic-form-input-label',
|
|
426
|
-
inputClass: `input-${this.theme}`,
|
|
427
|
-
innerClass: `ui-dynamic-form-input-outlines ${
|
|
428
|
-
this.theme === 'dark' ? '$remove:formkit-inner' : ''
|
|
429
|
-
}`,
|
|
430
|
-
readonly: isReadOnly,
|
|
431
|
-
validationVisibility: 'live',
|
|
432
406
|
},
|
|
433
407
|
};
|
|
434
408
|
case 'number':
|
|
435
|
-
const step =
|
|
409
|
+
const step = customForm.step;
|
|
436
410
|
return {
|
|
437
411
|
type: 'FormKit',
|
|
438
412
|
props: {
|
|
413
|
+
...commonFormKitProps,
|
|
439
414
|
type: 'number',
|
|
440
|
-
id: field.id,
|
|
441
|
-
name,
|
|
442
|
-
label: field.label,
|
|
443
|
-
required: field.required,
|
|
444
|
-
value: this.formData[field.id],
|
|
445
415
|
step,
|
|
446
416
|
number: 'float',
|
|
447
417
|
validation: validation ? `${validation}|number` : 'number',
|
|
448
|
-
help: hint,
|
|
449
|
-
wrapperClass: '$remove:formkit-wrapper',
|
|
450
|
-
labelClass: 'ui-dynamic-form-input-label',
|
|
451
|
-
inputClass: `input-${this.theme}`,
|
|
452
|
-
innerClass: `ui-dynamic-form-input-outlines ${
|
|
453
|
-
this.theme === 'dark' ? '$remove:formkit-inner' : ''
|
|
454
|
-
}`,
|
|
455
|
-
readonly: isReadOnly,
|
|
456
|
-
validationVisibility: 'live',
|
|
457
418
|
},
|
|
458
419
|
};
|
|
459
420
|
case 'date':
|
|
460
421
|
return {
|
|
461
422
|
type: 'FormKit',
|
|
462
423
|
props: {
|
|
424
|
+
...commonFormKitProps,
|
|
463
425
|
type: 'date',
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
case 'string':
|
|
429
|
+
return {
|
|
430
|
+
type: 'FormKit',
|
|
431
|
+
props: {
|
|
432
|
+
...commonFormKitProps,
|
|
433
|
+
type: 'text',
|
|
434
|
+
placeholder,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
case 'email':
|
|
438
|
+
return {
|
|
439
|
+
type: 'FormKit',
|
|
440
|
+
props: {
|
|
441
|
+
...commonFormKitProps,
|
|
442
|
+
type: 'email',
|
|
443
|
+
placeholder,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
case 'password':
|
|
447
|
+
return {
|
|
448
|
+
type: 'FormKit',
|
|
449
|
+
props: {
|
|
450
|
+
...commonFormKitProps,
|
|
451
|
+
type: 'password',
|
|
452
|
+
placeholder,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
case 'tel':
|
|
456
|
+
return {
|
|
457
|
+
type: 'FormKit',
|
|
458
|
+
props: {
|
|
459
|
+
...commonFormKitProps,
|
|
460
|
+
type: 'tel',
|
|
461
|
+
placeholder,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
case 'url':
|
|
465
|
+
return {
|
|
466
|
+
type: 'FormKit',
|
|
467
|
+
props: {
|
|
468
|
+
...commonFormKitProps,
|
|
469
|
+
type: 'url',
|
|
470
|
+
placeholder,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
case 'time':
|
|
474
|
+
return {
|
|
475
|
+
type: 'FormKit',
|
|
476
|
+
props: {
|
|
477
|
+
...commonFormKitProps,
|
|
478
|
+
type: 'time',
|
|
479
|
+
placeholder,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
case 'week':
|
|
483
|
+
return {
|
|
484
|
+
type: 'FormKit',
|
|
485
|
+
props: {
|
|
486
|
+
...commonFormKitProps,
|
|
487
|
+
type: 'week',
|
|
488
|
+
placeholder,
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
case 'month':
|
|
492
|
+
return {
|
|
493
|
+
type: 'FormKit',
|
|
494
|
+
props: {
|
|
495
|
+
...commonFormKitProps,
|
|
496
|
+
type: 'month',
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
case 'datetime-local':
|
|
500
|
+
return {
|
|
501
|
+
type: 'FormKit',
|
|
502
|
+
props: {
|
|
503
|
+
...commonFormKitProps,
|
|
504
|
+
type: 'datetime-local',
|
|
479
505
|
},
|
|
480
506
|
};
|
|
481
507
|
case 'enum':
|
|
@@ -591,106 +617,59 @@ export default {
|
|
|
591
617
|
? this.formData[originalFieldId]
|
|
592
618
|
: [this.formData[originalFieldId]];
|
|
593
619
|
|
|
594
|
-
//
|
|
595
|
-
const
|
|
596
|
-
const otherFiles = [];
|
|
597
|
-
|
|
598
|
-
fileDataArray.forEach((fileData) => {
|
|
599
|
-
const fileName = fileData.name || '';
|
|
600
|
-
const isImage = fileName.toLowerCase().match(/\.(png|jpg|jpeg|gif|webp)$/);
|
|
601
|
-
|
|
602
|
-
if (isImage && fileData.file && fileData.file.data) {
|
|
603
|
-
// Convert buffer to base64 data URL for image display - safe for large files
|
|
604
|
-
const uint8Array = new Uint8Array(fileData.file.data);
|
|
605
|
-
let binaryString = '';
|
|
620
|
+
// Create unique container ID for this field
|
|
621
|
+
const containerId = `file-preview-${field.id}`;
|
|
606
622
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
? 'image/gif'
|
|
619
|
-
: 'image/jpeg';
|
|
620
|
-
const dataURL = `data:${mimeType};base64,${base64String}`;
|
|
621
|
-
|
|
622
|
-
images.push({ fileName, dataURL, fileData });
|
|
623
|
-
} else {
|
|
624
|
-
otherFiles.push({ fileName, fileData });
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
let content = `<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
629
|
-
field.required ? ' *' : ''
|
|
630
|
-
}</label>`;
|
|
631
|
-
|
|
632
|
-
// Display images
|
|
633
|
-
if (images.length > 0) {
|
|
634
|
-
content += '<div style="margin-top: 8px;">';
|
|
635
|
-
content += '<div style="font-weight: bold; margin-bottom: 8px;">Bilder:</div>';
|
|
636
|
-
images.forEach((img, index) => {
|
|
637
|
-
const downloadId = `download-img-${field.id}-${index}`;
|
|
638
|
-
content += `
|
|
639
|
-
<div style="display: inline-block; margin: 8px; text-align: center; vertical-align: top;">
|
|
640
|
-
<img src="${img.dataURL}" alt="${img.fileName}"
|
|
641
|
-
style="max-width: 300px; max-height: 200px; border: 1px solid #ccc; display: block; cursor: pointer;"
|
|
642
|
-
onclick="document.getElementById('${downloadId}').click();" />
|
|
643
|
-
<div style="margin-top: 4px; font-size: 0.9em; color: #666; max-width: 300px; word-break: break-word;">
|
|
644
|
-
${img.fileName}
|
|
645
|
-
</div>
|
|
646
|
-
<a id="${downloadId}" href="${img.dataURL}" download="${img.fileName}" style="display: none;"></a>
|
|
623
|
+
// Check if this field is already visible (for immediate processing)
|
|
624
|
+
if (this.visibleFileFields.has(field.id)) {
|
|
625
|
+
// Return loading state initially
|
|
626
|
+
const loadingContent = `
|
|
627
|
+
<div id="${containerId}" data-lazy-field="${field.id}">
|
|
628
|
+
<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
629
|
+
field.required ? ' *' : ''
|
|
630
|
+
}</label>
|
|
631
|
+
<div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
|
|
632
|
+
<div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
|
|
633
|
+
<div>Dateien werden geladen...</div>
|
|
647
634
|
</div>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
content += '</div>';
|
|
651
|
-
}
|
|
635
|
+
</div>
|
|
636
|
+
`;
|
|
652
637
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
content += '<div style="font-weight: bold; margin-bottom: 8px;">Weitere Dateien:</div>';
|
|
658
|
-
otherFiles.forEach((file, index) => {
|
|
659
|
-
const downloadId = `download-file-${field.id}-${index}`;
|
|
660
|
-
const uint8Array = new Uint8Array(file.fileData.file.data);
|
|
661
|
-
let binaryString = '';
|
|
662
|
-
|
|
663
|
-
// Process in chunks for download
|
|
664
|
-
const chunkSize = 1024;
|
|
665
|
-
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
|
666
|
-
const chunk = uint8Array.slice(i, i + chunkSize);
|
|
667
|
-
binaryString += String.fromCharCode.apply(null, chunk);
|
|
668
|
-
}
|
|
638
|
+
// Process files asynchronously
|
|
639
|
+
setTimeout(() => {
|
|
640
|
+
this.processFilePreview(containerId, fileDataArray, field);
|
|
641
|
+
}, 0);
|
|
669
642
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
643
|
+
return {
|
|
644
|
+
type: 'div',
|
|
645
|
+
props: {
|
|
646
|
+
innerHTML: loadingContent,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
} else {
|
|
650
|
+
// Return lazy loading placeholder
|
|
651
|
+
const lazyContent = `
|
|
652
|
+
<div id="${containerId}" data-lazy-field="${field.id}" class="lazy-file-preview">
|
|
653
|
+
<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
654
|
+
field.required ? ' *' : ''
|
|
655
|
+
}</label>
|
|
656
|
+
<div style="margin-top: 8px; padding: 40px; text-align: center; color: #999; border: 1px dashed #ddd; border-radius: 4px;">
|
|
657
|
+
<div style="font-size: 1.5em; margin-bottom: 12px;">📁</div>
|
|
658
|
+
<div>Dateien werden geladen, wenn sie sichtbar werden...</div>
|
|
659
|
+
<div style="margin-top: 8px; font-size: 0.9em;">${
|
|
660
|
+
fileDataArray.length
|
|
661
|
+
} Datei(en)</div>
|
|
682
662
|
</div>
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
content += '</div>';
|
|
686
|
-
}
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
687
665
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
666
|
+
return {
|
|
667
|
+
type: 'div',
|
|
668
|
+
props: {
|
|
669
|
+
innerHTML: lazyContent,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
694
673
|
}
|
|
695
674
|
// If no files to preview, return empty div
|
|
696
675
|
return {
|
|
@@ -1075,6 +1054,220 @@ export default {
|
|
|
1075
1054
|
};
|
|
1076
1055
|
}
|
|
1077
1056
|
},
|
|
1057
|
+
initLazyLoading() {
|
|
1058
|
+
// Initialize Intersection Observer for lazy loading of file previews
|
|
1059
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
1060
|
+
this.intersectionObserver = new IntersectionObserver(
|
|
1061
|
+
(entries) => {
|
|
1062
|
+
entries.forEach((entry) => {
|
|
1063
|
+
if (entry.isIntersecting) {
|
|
1064
|
+
const element = entry.target;
|
|
1065
|
+
const fieldId = element.getAttribute('data-lazy-field');
|
|
1066
|
+
|
|
1067
|
+
if (fieldId && !this.visibleFileFields.has(fieldId)) {
|
|
1068
|
+
this.visibleFileFields.add(fieldId);
|
|
1069
|
+
this.loadFilePreview(fieldId);
|
|
1070
|
+
this.intersectionObserver.unobserve(element);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
root: null,
|
|
1077
|
+
rootMargin: '50px', // Start loading 50px before element becomes visible
|
|
1078
|
+
threshold: 0.1,
|
|
1079
|
+
}
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
// Observe all lazy file preview elements
|
|
1083
|
+
this.$nextTick(() => {
|
|
1084
|
+
this.observeLazyElements();
|
|
1085
|
+
});
|
|
1086
|
+
} else {
|
|
1087
|
+
// Fallback for browsers without Intersection Observer
|
|
1088
|
+
// Load all file previews immediately
|
|
1089
|
+
this.loadAllFilePreviews();
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
observeLazyElements() {
|
|
1093
|
+
const lazyElements = document.querySelectorAll('.lazy-file-preview[data-lazy-field]');
|
|
1094
|
+
lazyElements.forEach((element) => {
|
|
1095
|
+
if (this.intersectionObserver) {
|
|
1096
|
+
this.intersectionObserver.observe(element);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
},
|
|
1100
|
+
loadFilePreview(fieldId) {
|
|
1101
|
+
// Find the field configuration
|
|
1102
|
+
const field = this.userTask?.userTaskConfig?.formFields?.find((f) => f.id === fieldId);
|
|
1103
|
+
if (!field) return;
|
|
1104
|
+
|
|
1105
|
+
const originalFieldId = fieldId.replace('_preview', '');
|
|
1106
|
+
const fileDataArray = this.formData[originalFieldId];
|
|
1107
|
+
|
|
1108
|
+
if (!fileDataArray || fileDataArray.length === 0) return;
|
|
1109
|
+
|
|
1110
|
+
const containerId = `file-preview-${fieldId}`;
|
|
1111
|
+
const container = document.getElementById(containerId);
|
|
1112
|
+
|
|
1113
|
+
if (container) {
|
|
1114
|
+
// Show loading state
|
|
1115
|
+
container.innerHTML = `
|
|
1116
|
+
<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
1117
|
+
field.required ? ' *' : ''
|
|
1118
|
+
}</label>
|
|
1119
|
+
<div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
|
|
1120
|
+
<div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
|
|
1121
|
+
<div>Dateien werden geladen...</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
`;
|
|
1124
|
+
|
|
1125
|
+
// Process files
|
|
1126
|
+
setTimeout(() => {
|
|
1127
|
+
this.processFilePreview(containerId, fileDataArray, field);
|
|
1128
|
+
}, 0);
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
loadAllFilePreviews() {
|
|
1132
|
+
// Fallback method - load all file previews immediately
|
|
1133
|
+
const fileFields =
|
|
1134
|
+
this.userTask?.userTaskConfig?.formFields?.filter((f) => f.type === 'file-preview') || [];
|
|
1135
|
+
fileFields.forEach((field) => {
|
|
1136
|
+
if (!this.visibleFileFields.has(field.id)) {
|
|
1137
|
+
this.visibleFileFields.add(field.id);
|
|
1138
|
+
this.loadFilePreview(field.id);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
},
|
|
1142
|
+
processFilePreview(containerId, fileDataArray, field) {
|
|
1143
|
+
// Process files in chunks to avoid blocking the UI
|
|
1144
|
+
const processInChunks = async () => {
|
|
1145
|
+
const images = [];
|
|
1146
|
+
const otherFiles = [];
|
|
1147
|
+
|
|
1148
|
+
// Process files in batches to avoid UI blocking
|
|
1149
|
+
const batchSize = 3;
|
|
1150
|
+
|
|
1151
|
+
for (let i = 0; i < fileDataArray.length; i += batchSize) {
|
|
1152
|
+
const batch = fileDataArray.slice(i, i + batchSize);
|
|
1153
|
+
|
|
1154
|
+
for (const fileData of batch) {
|
|
1155
|
+
const fileName = fileData.name || '';
|
|
1156
|
+
const isImage = fileName.toLowerCase().match(/\.(png|jpg|jpeg|gif|webp)$/);
|
|
1157
|
+
|
|
1158
|
+
if (isImage && fileData.file && fileData.file.data) {
|
|
1159
|
+
// Convert buffer to base64 data URL for image display
|
|
1160
|
+
const uint8Array = new Uint8Array(fileData.file.data);
|
|
1161
|
+
let binaryString = '';
|
|
1162
|
+
|
|
1163
|
+
// Process in chunks to avoid call stack overflow
|
|
1164
|
+
const chunkSize = 1024;
|
|
1165
|
+
for (let j = 0; j < uint8Array.length; j += chunkSize) {
|
|
1166
|
+
const chunk = uint8Array.slice(j, j + chunkSize);
|
|
1167
|
+
binaryString += String.fromCharCode.apply(null, chunk);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const base64String = btoa(binaryString);
|
|
1171
|
+
const mimeType = fileName.toLowerCase().endsWith('.png')
|
|
1172
|
+
? 'image/png'
|
|
1173
|
+
: fileName.toLowerCase().endsWith('.gif')
|
|
1174
|
+
? 'image/gif'
|
|
1175
|
+
: 'image/jpeg';
|
|
1176
|
+
const dataURL = `data:${mimeType};base64,${base64String}`;
|
|
1177
|
+
|
|
1178
|
+
images.push({ fileName, dataURL, fileData });
|
|
1179
|
+
} else {
|
|
1180
|
+
otherFiles.push({ fileName, fileData });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Allow UI to update between batches
|
|
1185
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Build the final content
|
|
1189
|
+
let content = `<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
1190
|
+
field.required ? ' *' : ''
|
|
1191
|
+
}</label>`;
|
|
1192
|
+
|
|
1193
|
+
// Display images
|
|
1194
|
+
if (images.length > 0) {
|
|
1195
|
+
content += '<div style="margin-top: 8px;">';
|
|
1196
|
+
content += '<div style="font-weight: bold; margin-bottom: 8px;">Bilder:</div>';
|
|
1197
|
+
images.forEach((img, index) => {
|
|
1198
|
+
const downloadId = `download-img-${field.id}-${index}`;
|
|
1199
|
+
content += `
|
|
1200
|
+
<div style="display: inline-block; margin: 8px; text-align: center; vertical-align: top;">
|
|
1201
|
+
<img src="${img.dataURL}" alt="${img.fileName}"
|
|
1202
|
+
style="max-width: 300px; max-height: 200px; border: 1px solid #ccc; display: block; cursor: pointer;"
|
|
1203
|
+
onclick="document.getElementById('${downloadId}').click();" />
|
|
1204
|
+
<div style="margin-top: 4px; font-size: 0.9em; color: #666; max-width: 300px; word-break: break-word;">
|
|
1205
|
+
${img.fileName}
|
|
1206
|
+
</div>
|
|
1207
|
+
<a id="${downloadId}" href="${img.dataURL}" download="${img.fileName}" style="display: none;"></a>
|
|
1208
|
+
</div>
|
|
1209
|
+
`;
|
|
1210
|
+
});
|
|
1211
|
+
content += '</div>';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Display other files as list
|
|
1215
|
+
if (otherFiles.length > 0) {
|
|
1216
|
+
content +=
|
|
1217
|
+
'<div style="margin-top: 12px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
|
|
1218
|
+
content += '<div style="font-weight: bold; margin-bottom: 8px;">Weitere Dateien:</div>';
|
|
1219
|
+
otherFiles.forEach((file, index) => {
|
|
1220
|
+
const downloadId = `download-file-${field.id}-${index}`;
|
|
1221
|
+
const uint8Array = new Uint8Array(file.fileData.file.data);
|
|
1222
|
+
let binaryString = '';
|
|
1223
|
+
|
|
1224
|
+
// Process in chunks for download
|
|
1225
|
+
const chunkSize = 1024;
|
|
1226
|
+
for (let j = 0; j < uint8Array.length; j += chunkSize) {
|
|
1227
|
+
const chunk = uint8Array.slice(j, j + chunkSize);
|
|
1228
|
+
binaryString += String.fromCharCode.apply(null, chunk);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const base64String = btoa(binaryString);
|
|
1232
|
+
const dataURL = `data:application/octet-stream;base64,${base64String}`;
|
|
1233
|
+
|
|
1234
|
+
content += `
|
|
1235
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; padding: 4px; border-radius: 3px; cursor: pointer;"
|
|
1236
|
+
onclick="document.getElementById('${downloadId}').click();"
|
|
1237
|
+
onmouseover="this.style.backgroundColor='#e6e6e6';"
|
|
1238
|
+
onmouseout="this.style.backgroundColor='transparent';">
|
|
1239
|
+
<span style="font-size: 1.2em;">📎</span>
|
|
1240
|
+
<span style="flex: 1; word-break: break-word;">${file.fileName}</span>
|
|
1241
|
+
<span style="font-size: 0.8em; color: #007bff;">Download</span>
|
|
1242
|
+
<a id="${downloadId}" href="${dataURL}" download="${file.fileName}" style="display: none;"></a>
|
|
1243
|
+
</div>
|
|
1244
|
+
`;
|
|
1245
|
+
});
|
|
1246
|
+
content += '</div>';
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Update the container with the final content
|
|
1250
|
+
const container = document.getElementById(containerId);
|
|
1251
|
+
if (container) {
|
|
1252
|
+
container.innerHTML = content;
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
processInChunks().catch((error) => {
|
|
1257
|
+
const container = document.getElementById(containerId);
|
|
1258
|
+
if (container) {
|
|
1259
|
+
container.innerHTML = `
|
|
1260
|
+
<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
|
|
1261
|
+
field.required ? ' *' : ''
|
|
1262
|
+
}</label>
|
|
1263
|
+
<div style="margin-top: 8px; padding: 20px; text-align: center; color: #d32f2f;">
|
|
1264
|
+
<div style="font-size: 1.2em; margin-bottom: 8px;">⚠️</div>
|
|
1265
|
+
<div>Fehler beim Laden der Dateien</div>
|
|
1266
|
+
</div>
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
},
|
|
1078
1271
|
toggleCollapse() {
|
|
1079
1272
|
this.collapsed = !this.collapsed;
|
|
1080
1273
|
},
|
|
@@ -1091,13 +1284,7 @@ export default {
|
|
|
1091
1284
|
return style;
|
|
1092
1285
|
},
|
|
1093
1286
|
fields() {
|
|
1094
|
-
|
|
1095
|
-
const fieldMap = aFields.map((field) => ({
|
|
1096
|
-
...field,
|
|
1097
|
-
items: mapItems(field.type, field),
|
|
1098
|
-
}));
|
|
1099
|
-
|
|
1100
|
-
return fieldMap;
|
|
1287
|
+
return this.computedFields;
|
|
1101
1288
|
},
|
|
1102
1289
|
/*
|
|
1103
1290
|
widget-action just sends a msg to Node-RED, it does not store the msg state server-side
|
|
@@ -1110,7 +1297,6 @@ export default {
|
|
|
1110
1297
|
},
|
|
1111
1298
|
init(msg) {
|
|
1112
1299
|
this.msg = msg;
|
|
1113
|
-
// Clear component cache when form data changes for performance
|
|
1114
1300
|
this.clearComponentCache();
|
|
1115
1301
|
|
|
1116
1302
|
if (!msg) {
|
|
@@ -1126,18 +1312,23 @@ export default {
|
|
|
1126
1312
|
} else {
|
|
1127
1313
|
this.userTask = null;
|
|
1128
1314
|
this.formData = {};
|
|
1315
|
+
// Reset lazy loading state
|
|
1316
|
+
this.visibleFileFields.clear();
|
|
1129
1317
|
return;
|
|
1130
1318
|
}
|
|
1131
1319
|
|
|
1132
1320
|
const formFields = this.userTask.userTaskConfig.formFields;
|
|
1133
1321
|
const formFieldIds = formFields.map((ff) => ff.id);
|
|
1134
|
-
const initialValues = this.userTask.startToken;
|
|
1322
|
+
const initialValues = this.userTask.startToken.formData;
|
|
1135
1323
|
const finishedFormData = msg.payload.formData;
|
|
1136
1324
|
this.formIsFinished = !!msg.payload.formData;
|
|
1137
1325
|
if (this.formIsFinished) {
|
|
1138
1326
|
this.collapsed = this.props.collapse_when_finished;
|
|
1139
1327
|
}
|
|
1140
1328
|
|
|
1329
|
+
// Reset lazy loading state for new task
|
|
1330
|
+
this.visibleFileFields.clear();
|
|
1331
|
+
|
|
1141
1332
|
if (formFields) {
|
|
1142
1333
|
formFields.forEach((field) => {
|
|
1143
1334
|
this.formData[field.id] = field.defaultValue;
|
|
@@ -1177,13 +1368,11 @@ export default {
|
|
|
1177
1368
|
}
|
|
1178
1369
|
|
|
1179
1370
|
if (initialValues) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
.
|
|
1184
|
-
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1371
|
+
Object.keys(initialValues)
|
|
1372
|
+
.filter((key) => formFieldIds.includes(key))
|
|
1373
|
+
.forEach((key) => {
|
|
1374
|
+
this.formData[key] = initialValues[key];
|
|
1375
|
+
});
|
|
1187
1376
|
}
|
|
1188
1377
|
|
|
1189
1378
|
if (this.formIsFinished) {
|
|
@@ -1194,8 +1383,11 @@ export default {
|
|
|
1194
1383
|
});
|
|
1195
1384
|
}
|
|
1196
1385
|
|
|
1197
|
-
|
|
1386
|
+
// Force update of computed properties by triggering reactivity
|
|
1387
|
+
this.$nextTick(() => {
|
|
1198
1388
|
this.focusFirstFormField();
|
|
1389
|
+
// Re-observe lazy elements after DOM update
|
|
1390
|
+
this.observeLazyElements();
|
|
1199
1391
|
});
|
|
1200
1392
|
},
|
|
1201
1393
|
actionFn(action) {
|
|
@@ -1261,7 +1453,6 @@ export default {
|
|
|
1261
1453
|
const result = func(this.formData, this.userTask, this.msg);
|
|
1262
1454
|
return Boolean(result);
|
|
1263
1455
|
} catch (err) {
|
|
1264
|
-
console.error('Error while evaluating condition: ' + err);
|
|
1265
1456
|
return false;
|
|
1266
1457
|
}
|
|
1267
1458
|
},
|
|
@@ -1290,8 +1481,6 @@ export default {
|
|
|
1290
1481
|
|
|
1291
1482
|
if (inputElement) {
|
|
1292
1483
|
inputElement.focus();
|
|
1293
|
-
} else {
|
|
1294
|
-
console.warn('Could not find a focusable input element for the first form field.');
|
|
1295
1484
|
}
|
|
1296
1485
|
}
|
|
1297
1486
|
},
|
|
@@ -1313,4 +1502,29 @@ function mapItems(type, field) {
|
|
|
1313
1502
|
<style>
|
|
1314
1503
|
/* CSS is auto scoped, but using named classes is still recommended */
|
|
1315
1504
|
@import '../stylesheets/ui-dynamic-form.css';
|
|
1505
|
+
|
|
1506
|
+
/* Lazy loading styles */
|
|
1507
|
+
.lazy-file-preview {
|
|
1508
|
+
transition: opacity 0.3s ease-in-out;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.lazy-file-preview .lazy-placeholder {
|
|
1512
|
+
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
|
1513
|
+
background-size: 200% 200%;
|
|
1514
|
+
animation: shimmer 2s infinite;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
@keyframes shimmer {
|
|
1518
|
+
0% {
|
|
1519
|
+
background-position: -200% -200%;
|
|
1520
|
+
}
|
|
1521
|
+
100% {
|
|
1522
|
+
background-position: 200% 200%;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.lazy-file-preview:hover {
|
|
1527
|
+
transform: translateY(-1px);
|
|
1528
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1529
|
+
}
|
|
1316
1530
|
</style>
|