@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.
@@ -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
- componentCache: new Map(), // Add caching for components
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.userTaskConfig.formFields.some((field) => field.type === 'confirm');
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
- // Performance optimized component caching
357
+ // Simplified component getter - now just returns from computed cache
374
358
  getFieldComponent(field) {
375
- const cacheKey = `${field.id}_${JSON.stringify(this.formData[field.id])}_${this.formIsFinished}_${
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
- this.componentCache.clear();
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.hint;
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.find(
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 = field.customForm ? JSON.parse(field.customForm).step : undefined;
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
- id: field.id,
465
- name,
466
- label: field.label,
467
- required: field.required,
468
- value: this.formData[field.id],
469
- help: hint,
470
- wrapperClass: '$remove:formkit-wrapper',
471
- labelClass: 'ui-dynamic-form-input-label',
472
- inputClass: `input-${this.theme}`,
473
- innerClass: `ui-dynamic-form-input-outlines ${
474
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
475
- }`,
476
- readonly: isReadOnly,
477
- validation,
478
- validationVisibility: 'live',
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
- // Separate images from other files
595
- const images = [];
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
- // Process in chunks to avoid call stack overflow
608
- const chunkSize = 1024;
609
- for (let i = 0; i < uint8Array.length; i += chunkSize) {
610
- const chunk = uint8Array.slice(i, i + chunkSize);
611
- binaryString += String.fromCharCode.apply(null, chunk);
612
- }
613
-
614
- const base64String = btoa(binaryString);
615
- const mimeType = fileName.toLowerCase().endsWith('.png')
616
- ? 'image/png'
617
- : fileName.toLowerCase().endsWith('.gif')
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
- // Display other files as list
654
- if (otherFiles.length > 0) {
655
- content +=
656
- '<div style="margin-top: 12px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
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
- const base64String = btoa(binaryString);
671
- const dataURL = `data:application/octet-stream;base64,${base64String}`;
672
-
673
- content += `
674
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; padding: 4px; border-radius: 3px; cursor: pointer;"
675
- onclick="document.getElementById('${downloadId}').click();"
676
- onmouseover="this.style.backgroundColor='#e6e6e6';"
677
- onmouseout="this.style.backgroundColor='transparent';">
678
- <span style="font-size: 1.2em;">📎</span>
679
- <span style="flex: 1; word-break: break-word;">${file.fileName}</span>
680
- <span style="font-size: 0.8em; color: #007bff;">Download</span>
681
- <a id="${downloadId}" href="${dataURL}" download="${file.fileName}" style="display: none;"></a>
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
- return {
689
- type: 'div',
690
- props: {
691
- innerHTML: content,
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
- const aFields = this.userTask.userTaskConfig?.formFields ?? [];
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
- if (initialValues) {
1181
- Object.keys(initialValues)
1182
- .filter((key) => formFieldIds.includes(key))
1183
- .forEach((key) => {
1184
- this.formData[key] = initialValues[key];
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
- nextTick(() => {
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>